Является ли хорошей идеей использовать varargs в C API для установки пары ключевых значений?

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

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

update(FIELD_NAME1, 10, FIELD_NAME2, 20);

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

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

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

Ответ 1

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

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

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

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

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

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

Ответ 2

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

С точки зрения удобочитаемости (и это обычно то, что я предпочитаю по сравнению с необработанной скоростью, за исключением очень конкретных случаев), нет никакой реальной разницы между двумя следующими вариантами (я добавил счет к версиям 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);

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

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

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

Ответ 3

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

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

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

Ответ 4

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

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

ok 2 args. Я соврал и подумал, что параметр длины будет хорошим.

Ответ 5

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

Ответ 6

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

Ответ 7

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

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

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

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

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.