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

StackOverflow https://stackoverflow.com/questions/1899906

Вопрос

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

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

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

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

Так какой твой совет? void * и повторное использование или безопасность типов и дублированный код?Есть ли какие-то общие принципы?Пытаюсь ли я навязать ОО процедурному принципу, когда он не подходит?

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

Это было полезно?

Решение

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

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

Другие советы

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

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

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

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

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

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

Например.

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);

и т. д. и т. д.

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

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

Суть в том, чтобы использовать void * только в том случае, если тип вас не волнует.

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

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

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

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

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

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

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

РЕДАКТИРОВАТЬ:Хорошо ...если вы настаиваете на том, что мы не рекомендуем C++, я рекомендую вам не использовать объектно-ориентированный подход в C.Счастливый?Что касается ваших объектно-ориентированных привычек, вам, вероятно, следует думать с точки зрения «объектов», но исключите наследование и полиморфизм из своей стратегии реализации.Универсальность (с использованием указателей на функции) следует использовать с осторожностью.

РЕДАКТИРОВАТЬ 2:На самом деле, я думаю, что использование void * в общем списке C разумно.Он просто пытается создать ложную объектно-ориентированную структуру с использованием макросов, указателей на функции, диспетчеризации и подобной чепухи, что, на мой взгляд, является плохой идеей.

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

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

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

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

P.S.Единственное, что вам НЕ следует делать, это использовать 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); 

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

#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);
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top