Типобезопасные контейнеры в GNU C11 без кодогенерации
Всем привет!
Я решаю проблему про которую никто не знает, на языке, на котором никто не пишет. Это пост про язык Си.
Я сам не пишу на Си уже очень давно. Профессионально последние 7 лет только на C++ и C#. Но Си очень привлекает своей простотой. Но отталкивает отсутствием множества удобных штук.
Есть интересное открытие, которым я хотел бы поделиться, но не знаю с кем, так как мало кто пишет на Си и вообще задумывается о нем. Поэтому напишу тут, вдруг кому-то будет интересно. Это не готовое решение, а лишь идея, которая пришла в голову вчера утром и обросла деталями в течении дня.
Не хочу чтобы идея пылилась, хочу скорее от нее освободиться и вернуться к работе. Поэтому в посте мало деталей, пожалуйста, посмотрите пример, там вроде все довольно понятно.
Вот сразу ссылка на пример в Wandbox - https://wandbox.org/permlink/pOwkHRfWyipp8P89
1. Проблема
Если вы хотите универсальные контейнера на Си, можно пойти двумя путями:
1. Макросы, которые работают почти как шаблоны в C++, генерировать функции для доступа к контейнерам для тех типов, которые вы хотите использовать. Вот пример: https://github.com/stefanct/sglib
Это быстро, но требует объявить все используемые типы в одном месте, что усложняет разделение проекта на модули. Поправьте, если я не прав.
2. Типы данных, хранящие всю инфорацию внутри как это сделано в Glib https://developer.gnome.org/glib/2.66/ в данном случае, невозможно организовать типобезопасность, повсеместно используюется void* . Меняешь тип контейнера и не знаешь, что после этого отвалится. Моя цель - решить именно этот класс проблем.
2. Как выглядит решение
Так вот, слыхали что существует C11? Это почти как C++11, только C11.
Так вот, там появилось такое ключевое слово _Generic, которое позволяет выбрать функцию, в зависимости от типа и позволяет сделать какую ни какую перегрузку функций (которой нет в Си из коробки)
Оказалось, что используя _Generic + typeof() из GNU + указатели на функции можно сделать типобезопасные контейнеры прямо в Си! Эти контейнеры не используют кодогенерацию, однако так же удобны в использовании (хотя будут чуть медленней работать, но в большинстве случаев удобство важнее)
Вот короткий пример создания вектора:
ABVECTOR(int) numbers = CREATE_ABVECTOR(int);
for (int i = 0; i < 20; i++)
*PUSH( numbers) = i; // обратите внимание, тип известен!
В данном примере ABVECTOR(int) это аналог std::vector<int>
Если поменять тип контейнера, или тип переменной хоть в одном месте, вы получите ошибку компиляции. А это ровно то, чего я хочу!
Другой пример, передача контейнера в качестве аргумента в функцию:
void printValues(ABVECTOR(float) numbers)
{
for (float* iter = BEGIN(numbers); iter != END(numbers); iter = NEXT(numbers, iter))
printf("Val: %f\n", *iter);
for (int i = 0; i < LENGTH(numbers); i++)
printf("Val: %f\n", *GET(numbers, i));
}
Если передать в функцию неправильный тип контейнера, например List, вместо Vector - будет ошибка. Если тип элемента контейнера будет отличаться - то же ошибка.
3. Как этого добиться?
1. Использовать указатель на функцию чтобы хранить сразу два типа, тип контейнера + тип элемента. Например int (*) (ABVector*)
2. Использовать typeof, чтобы достать тип элемента контейнера typeof( X(0) ) - получение типа результата вызова функции
3. Использовать _Generic, чтобы проверить тип контейнера
_Generic(( X ), typeof(X(0)) (*) (ABVector*) : __my_container_func__ )(X)
На этом пожалуй все ) Спасибо за внимание!
Не забудьте потыкать пример в Wandbox - https://wandbox.org/permlink/pOwkHRfWyipp8P89
Лига программистов C/C++
60 постов4.8K подписчика
Правила сообщества
Соблюдайте правила Pikabu:
Помимо этого ЗАПРЕЩЕНО:
- Размещать в сообществе посты стиля "Подскажите как удалить вирус", "Подскажите как установить программу", "Подскажите как починить монитор/телевизор/мышь/тостер/стиральную машину" или "Напишите за меня лабу в универ". Пожалуйста размещайте такие посты вне этого сообщества или в соответствующих для этого сообществах.