C имеет другой вид красоты, чем C ++, и безопасность типов и возможность всегда видеть, что есть, при трассировке кода без участия приведения в отладчике, как правило, не относятся к ним.
Красота C во многом объясняется отсутствием безопасности типов, работой вокруг системы типов и необработанным уровнем битов и байтов.Из-за этого есть некоторые вещи, которые он может сделать более легко без борьбы с языком, например, структуры переменной длины, использование стека даже для массивов, размеры которых определяются во время выполнения и т. Д. Это также имеет тенденцию быть намного прощесохраняйте ABI, когда вы работаете на этом более низком уровне.
Таким образом, здесь задействован другой вид эстетики, а также различные проблемы, и я бы рекомендовал изменить мышление, когда вы работаете в C.цените это, я бы предложил делать то, что многие люди считают само собой разумеющимся в наши дни, например, реализовывать свой собственный распределитель памяти или драйвер устройства.Когда вы работаете на таком низком уровне, вы не можете не рассматривать все как схемы памяти битов и байтов, в отличие от «объектов» с прикрепленным поведением.Кроме того, в таком низкоуровневом коде управления битами / байтами может возникнуть ситуация, когда C становится легче для понимания, чем код C ++, усеянный reinterpret_casts
, например
Что касается вашего примера связанного списка, я бы предложилнеинтрузивная версия связанного узла (который не требует хранения указателей списка на сам тип элемента T
, позволяющий отделить логику и представление связанного списка от самого T
), например:
struct ListNode
{
struct ListNode* prev;
struct ListNode* next;
MAX_ALIGN char element[1]; // Watch out for alignment here.
// see your compiler's specific info on
// aligning data members.
};
Теперь мы можем создать узел списка следующим образом:
struct ListNode* list_new_node(int element_size)
{
// Watch out for alignment here.
return malloc_max_aligned(sizeof(struct ListNode) + element_size - 1);
}
// create a list node for 'struct Foo'
void foo_init(struct Foo*);
struct ListNode* foo_node = list_new_node(sizeof(struct Foo));
foo_init(foo_node->element);
Чтобы извлечь элемент из списка как T *:
T* element = list_node->element;
Поскольку это C,нет никакой проверки типов при приведении указателей таким образом, и это, вероятно, также даст вам неприятное ощущение, если вы пришли из C ++ фона.
Сложная часть здесь - убедиться, что этот член,element
, правильно выровнен для любого типа, который вы хотите сохранить.Когда вы сможете решить эту проблему настолько мобильно, насколько это необходимо, у вас будет мощное решение для создания эффективных макетов и распределителей памяти.Зачастую это приводит к тому, что вы просто используете максимальное выравнивание для всего, что может показаться расточительным, но обычно это не так, если вы используете соответствующие структуры данных и распределители, которые не платят эти накладные расходы за многочисленные мелкие элементы в отдельности.
Теперь это решение все еще включает приведение типов.Вы мало что можете сделать, если не иметь отдельную версию кода этого узла списка и соответствующую логику для работы с ним для каждого типа T, который вы хотите поддерживать (если не считать динамического полиморфизма).Однако он не требует дополнительного уровня косвенности, как вы могли подумать, и все равно выделяет весь узел списка и элемент за одно выделение.
И я бы порекомендовал этот простой способ достижения универсальности вС во многих случаях.Просто замените T
буфером, длина которого соответствует sizeof(T)
и выровнена правильно.Если у вас есть достаточно переносимый и безопасный способ обобщения для обеспечения правильного выравнивания, у вас будет очень мощный способ работы с памятью, который часто улучшает попадания в кэш, уменьшает частоту выделения / освобождения кучи, количествотребуется перенаправление, время сборки и т. д.
Если вам нужна дополнительная автоматизация, например автоматическая инициализация list_new_node
, struct Foo
, я бы порекомендовал создать структуру таблицы общего типа, которую вы можете передавать, которая содержит информацию о том, насколько великаT - это указатель функции, указывающий на функцию для создания экземпляра T по умолчанию, другой для копирования T, клонирования T, уничтожения T, компаратора и т. Д. В C ++ вы можете генерировать эту таблицу автоматически, используя шаблоны и встроенный язык.такие понятия, как копировать конструкторы и деструкторы.C требует немного больше ручного усилия, но вы все равно можете немного уменьшить его с помощью макросов.
Еще одна хитрость, которая может быть полезна, если вы выбираете более макроориентированный маршрут генерации кода, заключается в том, чтобы воспользоваться соглашением об именовании идентификаторов на основе префикса или суффикса. Например, CLONE (Type, ptr) может быть определен для возврата Type##Clone(ptr)
, поэтому CLONE(Foo, foo)
может вызвать FooClone(foo)
. Это своего рода обман, чтобы получить что-то похожее на перегрузку функций в C, и он полезен при генерации кода массовым образом (когда CLONE используется для реализации другого макроса) или даже при копировании и вставке кода типа шаблона, по крайней мере, улучшить равномерность шаблона.