Передача стиля кодирования по ссылке или передаче по значению?

Чтобы упростить разработку будущих школьных заданий, я решил создать API (вы бы это так назвали?) для двух структур данных, которые я обычно использую — связанный список и хеш-таблица.

При разработке каждой из них я получил следующие две функции вставки:

int list_insert(list *l, char *data, unsigned int idx);
int hash_insert(hash_table **ht, char *data);

Функция list_insert() (и все функции списка) оказались передачей по значению, поскольку мне никогда не приходилось напрямую изменять саму list *, если только я не выделял или освобождал ее. Однако, поскольку я хотел включить автоматическое перефразирование в свою хэш-таблицу, я обнаружил, что мне нужно передавать таблицу по ссылке, а не по значению в любой функции, которая может вызвать перефразирование. Теперь я получаю синтаксис, подобный следующему:

list_insert(l, "foo", 3);
hash_insert(&ht, "foo");

Разница кажется мне немного странной, и я задался вопросом, должен ли я изменить функции списка, чтобы они также передавали по ссылке, а также ради согласованности - даже если ни одна из моих функций не должна была бы использовать это. Каков типичный консенсус здесь? Должен ли я передавать по ссылке только в том случае, если моей функции действительно нужно изменить свои аргументы, или я должен передавать по ссылке ради согласованности?

Определения структуры:

typedef struct list_node list_node;
struct list_node {
    char *data;
    list_node *next;
    list_node *prev;
};

typedef struct list list;
struct list {
    list_node *head;
    list_node *tail;
    size_t size;
};

typedef struct hash_table hash_table;
struct hash_table {
    list **table;
    size_t entries;
    size_t buckets;
    float maxLoad;
    unsigned int (*hash)(char*, unsigned int);
};

Список функций:

list *list_createList();
list_node *list_createNode();
void list_destroyList(list *l);
void list_destroyNode(list_node *n);
int list_append(list *l, char *data);
int list_insert(list *l, char *data, unsigned int idx);
int list_remove(list *l, char *data, int (*compar)(const void*, const void*));
void list_push(list *l, char *data);
char *list_pop(list *l);
int list_count(list *l, char *data, int (*compar)(const void*, const void*));
int list_reverse(list *l);
int list_sort(list *l, int (*compar)(const void*, const void*));
int list_print(list *l, void (*print)(char *data));

Хэш-функции:

hash_table *hash_createTable(size_t buckets, float maxLoad, unsigned int (*hash)(char*, unsigned int));
void hash_destroyTable(hash_table *ht);
list *hash_list(const hash_table **ht);
int hash_checkLoad(hash_table **ht);
int hash_rehash(hash_table **ht);
int hash_insert(hash_table **ht, char *data);
void hash_stats(hash_table *ht);
int hash_print(hash_table *ht, void (*print)(char*));

person user2506293    schedule 27.12.2014    source источник
comment
Какую информацию делает hash_table соотв. list содержать? Возможно, изменение list*, переданного по ссылке, имеет такой же смысл, как изменение hash_table* при дальнейшем рассмотрении...   -  person Deduplicator    schedule 27.12.2014
comment
Не могли бы вы сообщить, что именно делает ваш API и как выглядят ваши структуры?   -  person Gopi    schedule 27.12.2014
comment
Предлагайте Code Review, а не здесь.   -  person chux - Reinstate Monica    schedule 27.12.2014
comment
Почему вы выделяете корень в куче? Просто верните его по значению для создания (если вы настаиваете, что агрегатная инициализация с {0} не обрезает его) и всегда передавайте его по ссылке, и вы получите одинаковые API для обоих.   -  person Deduplicator    schedule 27.12.2014
comment
@gopi Я добавил структуры и прототипы функций. Я думаю, что они относительно понятны, хотя, если потребуется дополнительная информация, я буду рад ее предоставить.   -  person user2506293    schedule 27.12.2014
comment
@Deduplicator Я не уверен, почему именно - просто меня всегда учили это делать. Если я вас правильно понимаю, вы говорите, что я должен просто объявить корневую структуру list или hash_table в стеке, поскольку нет необходимости динамически распределять память?   -  person user2506293    schedule 27.12.2014
comment
да. На самом деле, может быть одна возможная причина, если вы хотите сделать list и hash_table непрозрачными типы, чтобы можно было полностью изменить их без перекомпиляции вызывающих программ.   -  person Deduplicator    schedule 27.12.2014
comment
Другими словами, таким образом я мог бы изменить размер list или hash_table без перекомпиляции вызывающей программы? Если бы это было так, как бы вы предложили решать разные вызовы API?   -  person user2506293    schedule 27.12.2014


Ответы (2)


Вот общее правило:

  • передать по значению, если его typedef является родным типом (char, short, int, long, long long, double или float)
  • передать по ссылке, если это объединение, структура или массив

Дополнительные соображения для передачи по ссылке:

  • используйте const, если он не будет изменен
  • используйте ограничение, если указатели не будут указывать на один и тот же адрес

Иногда структура/объединение кажется подходящим типом, но может быть заменена массивами, если типы похожи. Это может помочь с оптимизацией (например, векторизация цикла)

person technosaurus    schedule 27.12.2014
comment
Вы предполагаете, что структура списка, например, не нужна? И что лучше вместо этого объявить массив из трех указателей? Кроме того, что вы думаете о размещении корня списка/хеш-таблицы в куче, а не в стеке? - person user2506293; 27.12.2014
comment
Хотя я использовал массивы для списков, я больше имел в виду структуры с r, g, b, a или x, y, dx, dy, где все они одного типа. Что касается кучи и стека, я стараюсь организовать новый код для использования стека, где это возможно, поэтому я не могу забыть об освобождении, но иногда *alloc неизбежен, особенно с существующим кодом. В любом случае хорошо иметь возможность анализировать выходные данные сборки для сравнения, поскольку современные компиляторы могут оптимизировать вызовы malloc для использования эффективных встроенных встроенных функций. - person technosaurus; 28.12.2014

Это зависит от вас и требует немного интуиции. При передаче больших структур я передаю по ссылке, чтобы не занимать дополнительное пространство стека и не сжигать циклы при копировании структуры. Но с небольшими стойками, такими как ваша, может быть более эффективно использовать стек в зависимости от вашего целевого процессора, как часто вы используете значения и что делает ваш компилятор. Ваш компилятор может разбить эту структуру и поместить ее значения в регистры.

Но если вы передаете по ссылке и не собираетесь изменять значение, лучше всего передать указатель на константу, например: const list * l. Таким образом, нет риска случайного изменения значения, и это делает интерфейс чище — теперь вызывающая сторона знает, что значение не изменится.

Согласованность — это хорошо, и лично я бы склонялся в этом направлении, особенно на большом интерфейсе, потому что это может упростить задачу в долгосрочной перспективе, но я бы определенно использовал const. При этом вы позволяете компилятору обнаруживать любые случайные присваивания, так что позже вам не нужно отслеживать труднодоступную ошибку.

См. также: Передача структуры функции в C

person Nick    schedule 27.12.2014
comment
Однако он не передает структуры, а только указатели на структуры (список *) и указатели на указатели на структуры (хэш_таблица **). - person netcat; 27.12.2014
comment
@netcat он спрашивает, должен ли он передать список структур напрямую, если он не собирается его изменять, или ему следует передать указатель на структуру, как я понял вопрос. Разве это не правильно? - person Nick; 27.12.2014