Универсальность и безопасность типов? Использование void * в C

Исходя из OO (C #, Java, Scala), я очень ценю принципы повторного использования кода и безопасности типов. Аргументы типа на вышеуказанных языках выполняют задание и позволяют создавать общие структуры данных, которые являются безопасными по типу и не «теряют» код.

Поскольку я застрял в C, я знаю, что я должен пойти на компромисс, и я бы хотел, чтобы он был правильным. Либо мои структуры данных имеют void * в каждом узле / элементе, и я теряю безопасность типа, или мне приходится переписывать свои структуры и код для каждого типа, с которым я хочу их использовать.

Сложность кода является очевидным фактором: итерация через массив или связанный список тривиальна, и добавление *next с структурой не требует дополнительных усилий; в этих случаях имеет смысл не пытаться повторно использовать структуры и код. Но для более сложных структур ответ не столь очевиден.

Существует также модульность и проверяемость: разделение типа и его операций с кодом, использующим структуру, упрощает его тестирование. Обратное также верно: тестирование итерации некоторого кода по структуре, пока он пытается делать другие вещи, становится беспорядочным.

Так что твой совет? void * и повторное использование или тип безопасности и дублированный код? Существуют ли общие принципы? Я пытаюсь заставить OO на процедурный, когда он не подходит?

Редактировать : Пожалуйста, не рекомендуем C ++, мой вопрос о C!

Я бы сказал, использую void * чтобы вы могли повторно использовать код. Это больше работы по повторной реализации, например, связанного списка, чем для того, чтобы убедиться, что вы правильно загружаете данные в список.

Возьмите как можно больше подсказок из glib , я считаю, что их структуры данных очень приятные и простые в использовании, и у них были небольшие проблемы из-за потери безопасности типов.

Я думаю, вам придется найти баланс между ними, как вы предлагаете. Если код всего несколько строк и тривиальный, я бы дублировал его, но если он более сложный, я бы рассмотрел возможность работы с void* чтобы избежать необходимости в любой попытке исправления ошибок и обслуживания в нескольких местах, а также для уменьшения размера кода.

Если вы посмотрите на библиотеку времени выполнения C, существует несколько «общих» функций, которые работают с void* , один общий пример сортирует с qsort . Было бы безумием дублировать этот код для каждого типа, который вы хотите сортировать.

Нет ничего плохого в использовании указателей void. Вам даже не нужно бросать их при назначении им переменной типа указателя, поскольку преобразование выполняется внутри. Это может стоить взглянуть на это: http://www.cpax.org.uk/prg/writings/casting.php

Ответ на этот вопрос такой же, как получение эффективных шаблонов для списка ссылок на C ++.

a) Создайте абстрактную версию алгоритма, который использует void * или некоторый абстрактный тип

б) Создать легкий публичный интерфейс для вызова алгоритмов Abstracted Type и касты между ними.

Например.

 typedef struct simple_list { struct simple_list* next; } SimpleList; void add_to_list( SimpleList* listTop, SimpleList* element ); SimpleList* get_from_top( SimpleList* listTop ); // the rest #define ListType(x) \ void add_ ## x ( x* l, x* e ) \ { add_to_list( (SimpleList*)l, (SimpleList*)x ); } \ void get_ ## x ( x* l, x* e ) \ { return (x*) get_from_to( (SimpleList*)l ); } \ /* the rest */ typedef struct my_struct { struct my_struct* next; /* rest of my stuff */ } MyStruct; ListType(MyStruct) MyStruct a; MyStruct b; add_MyStruct( &a, &b ); MyStruct* c = get_MyStruct(&a); 

и т.д.

Мы используем OO в C здесь много, но только для инкапсуляции и абстракции, никакого polymorphismа или так.

Это означает, что у нас есть определенные типы, такие как FooBar (Foo a, …), но для нашей коллекции «classes» мы используем void *. Просто используйте void *, где можно использовать несколько типов, НО, сделав это, убедитесь, что вам не нужен аргумент определенного типа. В соответствии с коллекцией наличие void * в порядке, потому что коллекция не заботится о типе. Но если ваша функция может принимать типы a и type b, но ни один другой, сделайте два варианта: один для a и один для b.

Главное – использовать void * только тогда, когда вам не нужен тип.

Теперь, если у вас есть 50 типов с одинаковой базовой структурой (скажем, int a; int b; как первые члены всех типов) и хотите, чтобы функция действовала на эти типы, просто сделайте общие первые члены самим типом , затем заставьте функцию принять это, и передать объект object -> ab или (AB *) – ваш тип непрозрачен, оба будут работать, если ab – первое поле в вашей структуре.

Вы можете использовать macros, они будут работать с любым типом, а компилятор будет статически ставить расширенный код. Недостатком является то, что плотность кода (в двоичном формате) ухудшится, и их сложнее отладить.

Некоторое время назад я задал этот вопрос об общих функциях, и ответы могли помочь вам.

Вы можете эффективно добавлять информацию о типе, наследование и polymorphism в структуры данных C, это то, что делает C ++. ( http://www.embedded.com/97/fe29712.htm )

Определенно общий void* , никогда не дублируйте код!

Учтите, что эта дилемма рассматривалась многими программистами C и многими крупными проектами C. Все серьезные проекты C, с которыми я когда-либо сталкивался, независимо от того, являются ли они открытыми или коммерческими, выбрали общую void* . При использовании тщательно и завернутый в хороший API, это едва ложится на пользователя библиотеки. Более того, void* является идиоматическим C, рекомендованным непосредственно в K & R2. Это то, как люди ожидают, что код будет написан, и все остальное будет удивительно и плохо принято.

Вы можете создать (вид) OO-frameworks с помощью C, но вы упускаете много преимуществ … как система OO-типа, которую понимает компилятор. Если вы настаиваете на выполнении OO на языке C, C ++ – лучший выбор. Это сложнее, чем ваниль C, но по крайней мере вы получаете правильную лингвистическую поддержку OO.

EDIT: Хорошо … если вы настаиваете на том, что мы не рекомендуем C ++, я рекомендую вам не делать OO на C. Happy? Что касается ваших привычек OO, вы, вероятно, должны думать о «объектах», но оставляйте наследование и polymorphism из своей страtagsи реализации. Общую универсальность (используя указатели функций) следует использовать экономно.

EDIT 2: На самом деле, я считаю, что использование void * в общем списке C является разумным. Он просто пытается создать фальшивую OO-структуру с использованием макросов, указателей функций, диспетчеризации и таких глупостей, которые я считаю плохими.

В Java все коллекции из пакета java.util фактически эквивалентны указателю void* ( Object ).

Да, генерики (введенные в 1.5) добавляют синтаксический сахар и препятствуют кодированию небезопасных назначений, однако тип хранилища остается Object .

Итак, я думаю, что преступление OO не совершается, когда вы используете void* для универсального типа frameworks.

Я бы также добавил встроенные строки или макрообъекты, которые назначают / извлекают данные из общих структур, если вы часто это делаете в своем коде.

PS Единственное, что вы не должны делать, это использовать void** для возврата выделенных / перераспределенных общих типов. Если вы проверите подписи malloc/realloc вы увидите, что вы можете добиться правильного распределения памяти без страшного указателя void** . Я только говорю это, потому что я видел это в каком-то проекте с открытым исходным кодом, которого я не хочу называть здесь.

Общий контейнер можно обернуть небольшим количеством работы, чтобы его можно было создать в безопасных версиях. Ниже приведен пример полных заголовков, приведенных ниже:

/ * общая реализация * /

 struct deque *deque_next(struct deque *dq); void *deque_value(const struct deque *dq); /* Prepend a node carrying `value` to the deque `dq` which may * be NULL, in which case a new deque is created. * O(1) */ void deque_prepend(struct deque **dq, void *value); 

Из заголовка, который может использоваться для создания экземпляров определенных завернутых типов deque

 #include "deque.h" #ifndef DEQUE_TAG #error "Must define DEQUE_TAG to use this header file" #ifndef DEQUE_VALUE_TYPE #error "Must define DEQUE_VALUE_TYPE to use this header file" #endif #else #define DEQUE_GEN_PASTE_(x,y) x ## y #define DEQUE_GEN_PASTE(x,y) DEQUE_GEN_PASTE_(x,y) #define DQTAG(suffix) DEQUE_GEN_PASTE(DEQUE_TAG,suffix) #define DQVALUE DEQUE_VALUE_TYPE #define DQREF DQTAG(_ref_t) typedef struct { deque_t *dq; } DQREF; static inline DQREF DQTAG(_next) (DQREF ref) { return (DQREF){deque_next(ref.dq)}; } static inline DQVALUE DQTAG(_value) (DQREF ref) { return deque_value(ref.dq); } static inline void DQTAG(_prepend) (DQREF *ref, DQVALUE val) { deque_prepend(&ref->dq, val); }