Является ли хорошей идеей использовать varargs в C API для установки пар ключ-значение? - PullRequest
10 голосов
/ 15 мая 2009

Я пишу API, который обновляет много различных полей в структуре.

Я мог бы помочь в добавлении полей будущего, сделав функцию обновления переменной:

update(FIELD_NAME1, 10, FIELD_NAME2, 20);

, а затем добавить FIELD_NAME3 без изменения существующих вызовов:

update(FIELD_NAME1, 10, FIELD_NAME2, 20, FIELD_NAME3, 30);

Слова мудрости, пожалуйста?

Ответы [ 7 ]

11 голосов
/ 15 мая 2009

Как правило, нет.

Varargs выбрасывает много типов безопасности - вы можете передавать указатели, числа с плавающей запятой и т. Д. Вместо int, и он будет компилироваться без проблем. Неправильное использование varargs, такое как пропуск аргументов, может привести к странным сбоям из-за повреждения стека или чтения неверных указателей.

Например, следующий вызов скомпилируется и приведет к сбоям или другому странному поведению:

UpdateField(6, "Field1", 7, "Field2", "Foo");

Начальные 6 - это сколько параметров ожидать. Он преобразует строковый указатель «Foo» в тип int для помещения в Field2, и он попытается прочитать и интерпретировать два других параметра, которые отсутствуют, что, вероятно, вызовет сбой здесь из-за разыменования стека.

Я полагаю, что реализация varargs в C является ошибкой (учитывая современную среду - это, вероятно, имело смысл в 1972 году.) Реализация состоит в том, что вы передаете кучу значений в стек, а затем вызываемый будет проходить, поднимая стек параметры, основанные на его интерпретации некоторого начального управляющего параметра. Реализация такого типа в основном требует от вас сделать ошибку, что может быть очень трудно диагностировать. Реализация этого в C #, передавая массив объектов с атрибутом в методе, просто должна быть более разумной, хотя и не может напрямую отображаться в язык Си.

7 голосов
/ 15 мая 2009

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

С точки зрения читабельности (и это обычно то, что я предпочитаю необработанной скорости, за исключением очень специфических случаев), между этими двумя опциями нет реальной разницы (я добавил счетчик к версиям varargs, так как вам нужен либо счетчик или дозорный, чтобы определить конец данных):

update(2, FIELD_NAME1, 10, FIELD_NAME2, 20);
update(3, FIELD_NAME3, 10, FIELD_NAME4, 20, FIELD_NAME5, 30);
/* ========== */
update(FIELD_NAME1, 10);
update(FIELD_NAME2, 20);
update(FIELD_NAME3, 10);
update(FIELD_NAME4, 20);
update(FIELD_NAME5, 30);

На самом деле, поскольку версия varargs становится длиннее, вам все равно придется разделить ее для форматирования:

update(5,
    FIELD_NAME1, 10,
    FIELD_NAME2, 20,
    FIELD_NAME3, 10,
    FIELD_NAME4, 20,
    FIELD_NAME5, 30);

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

Если вы действительно должны быть в состоянии вызвать одну функцию, чтобы сделать это, я бы выбрал:

void update (char *k1, int v1) {
    ...
}
void update2 (char *k1, int v1, char *k2, int v2) {
    update (k1, v1);
    update (k2, v2);
}
void update3 (char *k1, int v1, char *k2, int v2, char *k3, int v3) {
    update (k1, v1); /* or these two could be a single */
    update (k2, v2); /*   update2 (k1, v1, k2, v2);    */
    update (k3, v3);
}
/* and so on. */

Вы можете даже выполнять высокоуровневые функции как макросы, если хотите, без потери безопасности типов.

Единственное место, где я склонен использовать функции varargs, - это предоставление тех же функций, что и printf() - например, мне иногда приходилось писать библиотеки журналов с такими функциями, как logPrintf(), обеспечивающими такие же функции. Я не могу думать о любом другом времени в моем долгом (а я имею в виду, долгом :-) времени на угольном заборе, которое мне нужно было использовать.

Кроме того, если вы решите использовать varargs, я склоняюсь к дозорным, а не к подсчетам, поскольку это предотвращает несоответствия при добавлении полей. Вы можете легко забыть настроить счет и в итоге:

update (2, k1, v1, k2, v2, k3, v3);

при добавлении, что коварно, потому что оно молча пропускает k3 / v3, или:

update (3, k1, v1, k2, v2);

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

Наличие стража предотвращает это (если, конечно, вы не забудете стража):

update (k1, v1, k2, v2, k3, v3, NULL);
5 голосов
/ 15 мая 2009

Одна проблема с varargs в C состоит в том, что вы не знаете, сколько передано аргументов, поэтому вам нужно это как другой параметр:

update(2, FIELD_NAME1, 10, FIELD_NAME2, 20);

update(3, FIELD_NAME1, 10, FIELD_NAME2, 20, FIELD_NAME3, 30);
3 голосов
/ 15 мая 2009

Почему бы не иметь один аргумент, массив. Еще лучше указатель на массив.

struct field {
  int val;
  char* name;
};

или даже ...

union datatype {
  int a;
  char b;
  double c;
  float f;
// etc;
}; 

тогда

struct field {
  datatype val;
  char* name;
};

union (struct* field_name_val_pairs, int len);

хорошо 2 аргумента. Я солгал и думал, что длина параметра будет хорошей.

2 голосов
/ 06 августа 2009

Причины, приведенные до сих пор, чтобы избежать вараггов, все хороши. Позвольте мне добавить еще один, который еще не был дан, поскольку он менее важен, но с ним можно столкнуться. Vararg требует передачи параметра в стек, что замедляет вызов функции. На некоторых архитектурах разница может быть существенной. На x86 это не очень важно из-за отсутствия регистра, на SPARC, например, это может быть важно. В регистры передается до 5 параметров, и если ваша функция использует несколько локальных объектов, никакая настройка стека не производится. Если ваша функция является конечной (то есть не вызывает другую функцию), то также нет настройки окна. Стоимость звонка очень мала. С помощью vararg выполняется нормальная последовательность передачи параметров в стеке, настройка стека и управление окнами, иначе ваша функция не сможет получить параметры. Это значительно увеличивает стоимость звонка.

2 голосов
/ 15 мая 2009

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

1 голос
/ 22 мая 2009

Многие здесь предложили передать # параметров, однако другие справедливо отмечают, что это приводит к коварным ошибкам, когда # полей изменяется, а число, передаваемое функции vararg - нет. Я решаю это в продукте, используя вместо этого нулевое завершение:

send_info(INFO_NUMBER,
          Some_Field,       23,
          Some_other_Field, "more data",
          NULL);

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

Оглядываясь назад на исходную проблему, у вас есть функция, которая должна обновлять структуру с большим количеством полей, и структура будет расти. Обычный метод (в классических API-интерфейсах Win32 и MacOS) передачи данных такого рода в функцию заключается в передаче другой структуры (может даже иметь ту же структуру, что и обновляемая вами), например:

void update (UPDATESTRUCTURE * update_info);

чтобы использовать его, вы должны заполнить поля:

UPDATESTRUCTURE my_update = {
    UPDATESTRUCTURE_V1,
    field_1,
    field_2
};
update( &my_update );

Позже, когда вы добавите новые поля, вы можете обновить определение UPDATESTRUCTURE и перекомпилировать. Добавив версию #, вы можете поддерживать старый код, который не использует новые поля.

Вариант для темы состоит в том, чтобы иметь значение для полей, которые вы не хотите обновлять, например, KEEP_OLD_VALUE (в идеале это будет 0) или NULL.

UPDATESTRUCTURE my_update = {
    field_1,
    NULL,
    field_3
};
update( &my_update);

Я не включаю версию, потому что использую тот факт, что когда мы увеличиваем количество полей в UPDATESTRUCTURE, дополнительные поля будут инициализированы равными 0 или KEEP_OLD_VALUE.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...