Универсальность и безопасность типов? Использование void * в C
Начиная с OO (С#, Java, Scala), я очень высоко ценю принципы повторного использования кода и безопасности типов. Аргументы типа на указанных языках выполняют задание и разрешают создание общих структур данных, которые являются безопасными по типу и не являются "ненужными".
Как я застрял в C, я знаю, что я должен пойти на компромисс, и я бы хотел, чтобы он был правильным. Либо мои структуры данных имеют void *
в каждом элементе node/, и я теряю безопасность типа, или мне приходится переписывать свои структуры и код для каждого типа, с которым я хочу их использовать.
Сложность кода является очевидным фактором: итерация через массив или связанный список тривиальна, и добавление *next
к структуре не требует дополнительных усилий; в этих случаях имеет смысл не пытаться повторно использовать структуры и код. Но для более сложных структур ответ не столь очевиден.
Кроме того, модульность и проверяемость: разделение типа и его операций с кодом, использующим структуру, упрощает тестирование. Обратное также верно: тестирование итерации какого-либо кода по структуре, пока он пытается сделать другие вещи, становится беспорядочным.
Так что твой совет? void *
и повторное использование или тип безопасности и дублированный код? Существуют ли общие принципы? Я пытаюсь заставить OO на процедурный, когда он не подходит?
Изменить. Пожалуйста, не рекомендуем С++, мой вопрос о C!
Ответы
Ответ 1
Я бы сказал, используйте void *
, чтобы вы могли повторно использовать код. Это больше работает для повторной реализации, например. связанный список, чем убедиться, что вы правильно загружаете/устанавливаете данные в списке.
Возьмите как можно больше советов от glib, я считаю, что их структуры данных очень приятные и удобные в использовании, и у них были небольшие проблемы из-за потери безопасности типа.
Ответ 2
Я думаю, вам придется найти баланс между ними, как вы предлагаете. Если код всего несколько строк и тривиальный, я бы дублировал его, но если бы он был более сложным, я бы подумал о работе с void*
, чтобы избежать необходимости исправлять и исправлять ошибки в нескольких местах, а также уменьшить размер кода.
Если вы посмотрите на библиотеку времени выполнения C, есть несколько "общих" функций, которые работают с void*
, один общий пример сортируется с помощью qsort
. Было бы безумием дублировать этот код для каждого типа, который вы хотите сортировать.
Ответ 3
Нет ничего плохого в использовании указателей void. Вам даже не нужно бросать их при назначении им переменной типа указателя, поскольку преобразование выполняется внутри. Это должно выглядеть так: http://www.cpax.org.uk/prg/writings/casting.php
Ответ 4
Ответ на этот вопрос такой же, как получение эффективных шаблонов для списка ссылок в С++.
a) Создайте абстрактную версию алгоритма, который использует void * или некоторый абстрактный тип
b) Создайте легкий публичный интерфейс для вызова алгоритмов 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);
и т.д.
Ответ 5
Вы можете использовать макросы, они будут работать с любым типом, а компилятор будет статически проверять расширенный код. Недостатком является то, что плотность кода (в двоичном формате) ухудшится, и их сложнее отладить.
Я задал этот вопрос об общих функциях некоторое время назад, и ответы могли помочь вам.
Ответ 6
Вы можете эффективно добавлять информацию о типе, наследование и полиморфизм в структуры данных C, что делает С++. (http://www.embedded.com/97/fe29712.htm)
Ответ 7
Мы используем OO в C здесь много, но только для инкапсуляции и абстракции, никакого полиморфизма или так.
Это означает, что у нас есть определенные типы, такие как FooBar (Foo a,...), но для нашей коллекции "classes" мы используем void *. Просто используйте void *, где можно использовать несколько типов, НО, сделав это, убедитесь, что вам не нужен аргумент определенного типа. В соответствии с коллекцией наличие void * в порядке, потому что коллекция не заботится о типе. Но если ваша функция может принимать тип a и тип b, но ни один другой, сделайте два варианта: один для a и один для b.
Главное - использовать void * только тогда, когда вам не нужен тип.
Теперь, если у вас есть 50 типов с одной и той же базовой структурой (скажем, int a; int b; как первые члены всех типов) и хотите, чтобы функция воздействовала на эти типы, просто сделайте общих первых членов тип сам по себе, затем заставьте функцию принять это, и передать объект object → ab или (AB *) - ваш тип непрозрачен, оба будут работать, если ab - это первое поле в вашей структуре.
Ответ 8
Определенно общий void*
, никогда не дублируйте код!
Учтите, что эта дилемма рассматривалась многими программистами C и многими крупными проектами C. Все серьезные проекты C, с которыми я когда-либо сталкивался, будь то open-source или commercial, выбрали общий void*
. При использовании тщательно и завернутый в хороший API, это едва ложится на пользователя библиотеки. Более того, void*
является идиоматическим C, рекомендованным непосредственно в K & R2. Это то, как люди ожидают, что код будет написан, и все остальное будет удивительно и плохо принято.
Ответ 9
Вы можете создать (вид) OO-рамки с помощью C, но вы пропустите много преимуществ... как система типа OO, которую понимает компилятор. Если вы настаиваете на выполнении OO на языке C, С++ - лучший выбор. Это сложнее, чем ваниль C, но по крайней мере вы получаете правильную лингвистическую поддержку OO.
EDIT: Хорошо... если вы настаиваете на том, что мы не рекомендуем С++, я рекомендую вам не делать OO на C. Happy? Что касается ваших привычек OO, вы, вероятно, должны думать о "объектах", но оставляйте наследование и полиморфизм из своей стратегии реализации. Обобщение (используя указатели функций) следует использовать экономно.
EDIT 2: На самом деле, я думаю, что использование void *
в общем списке C является разумным. Он просто пытается создать фальшивую OO-инфраструктуру с использованием макросов, указателей функций, диспетчеризации и таких глупостей, которые, по моему мнению, являются плохими идеями.
Ответ 10
В Java все коллекции из пакета java.util
фактически содержат эквивалент указателя void*
(Object
).
Да, generics (введенные в 1.5) добавляют синтаксический сахар и препятствуют кодированию небезопасных назначений, однако тип хранения остается Object
.
Итак, я думаю, что преступление OO не совершается, когда вы используете void*
для универсального типа рамки.
Я бы также добавил встроенные строки или макро-обертки, которые назначают/извлекают данные из общих структур, если вы часто это делаете в своем коде.
P.S. Единственное, что вы НЕ должны делать, это использовать void**
для возврата выделенных/перераспределенных общих типов. Если вы проверите подписи malloc/realloc
, вы увидите, что вы можете добиться правильного выделения памяти без страшного указателя void**
. Я только говорю это, потому что я видел это в каком-то проекте с открытым исходным кодом, который я не хочу называть здесь.
Ответ 11
Общий контейнер можно обернуть небольшим количеством работы, чтобы его можно было создать в безопасных версиях. Ниже приведен пример полных заголовков, связанных ниже:
/* общая реализация */
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);
}