Идиоматический C для двойных указателей const
Мне известно, что в C вы не можете неявно преобразовать, например, char**
в const char**
(cf C-Faq, SO вопрос 1, ВОПРОС 2).
С другой стороны, если я вижу функцию, объявленную так:
void foo(char** ppData);
Я должен предположить, что функция может изменить переданные данные.
Поэтому, если я пишу функцию, которая будет не изменять данные, лучше, на мой взгляд, объявить:
void foo(const char** ppData);
или даже:
void foo(const char * const * ppData);
Но это ставит пользователей этой функции в неудобное положение.
Они могут иметь:
int main(int argc, char** argv)
{
foo(argv); // Oh no, compiler error (or warning)
...
}
И для того, чтобы чисто называть мою функцию, им нужно будет вставить бросок.
Я исхожу из основного фона С++, где это меньше проблемы из-за более сложных С++-правил С++.
Что такое идиоматическое решение в C?
-
Объявить foo как взятие char**
и просто документировать факт, что он не изменит свои входы? Это кажется немного грубым, особенно. так как он наказывает пользователей, у которых может быть const char**
, которые они хотят передать ему (теперь им нужно указать прочь const-ness)
-
Заставляет пользователей вводить свой ввод, добавляя константу.
-
Что-то еще?
Ответы
Ответ 1
2 лучше, чем 1. 1 довольно распространено, поскольку огромные объемы кода C вообще не используют const. Поэтому, если вы пишете новый код для новой системы, используйте 2. Если вы пишете код обслуживания для существующей системы, где const является редкостью, используйте 1.
Ответ 2
Хотя вы уже приняли ответ, я бы хотел пойти на 3) именно макросы. Вы можете записать их так, чтобы пользователь вашей функции просто написал вызов foo(x);
, где x может быть const
-qualified или нет. Идея была бы иметь один макрос CASTIT
, который выполняет бросок и проверяет, имеет ли аргумент допустимый тип, а другой - пользовательский интерфейс:
void totoFunc(char const*const* x);
#define CASTIT(T, X) ( \
(void)sizeof((T const*){ (X)[0] }), \
(T const*const*)(X) \
)
#define toto(X) totoFunc(CASTIT(char, X))
int main(void) {
char * * a0 = 0;
char const* * b0 = 0;
char *const* c0 = 0;
char const*const* d0 = 0;
int * * a1 = 0;
int const* * b1 = 0;
int *const* c1 = 0;
int const*const* d1 = 0;
toto(a0);
toto(b0);
toto(c0);
toto(d0);
toto(a1); // warning: initialization from incompatible pointer type
toto(b1); // warning: initialization from incompatible pointer type
toto(c1); // warning: initialization from incompatible pointer type
toto(d1); // warning: initialization from incompatible pointer type
}
Макрос CASTIT
выглядит немного сложнее, но все, что он делает, это сначала проверить, совместимо ли X[0]
присвоение char const*
. Для этого используется составной литерал. Это затем скрывается внутри sizeof
, чтобы гарантировать, что фактически составной литерал никогда не создается, а также что X
не оценивается этим тестом.
Затем следует простой литой, но который сам по себе был бы слишком опасным.
Как вы можете видеть по примерам в main
, это точно определяет ошибочные случаи.
Многие из этих вещей возможны с помощью макросов. Недавно я приготовил сложный пример с const
-qualified arrays.
Ответ 3
Go с опцией 2. Вариант 1 имеет тот недостаток, о котором вы упоминали, и менее безопасный тип.
Если я увидел функцию, которая принимает аргумент char **
, и у меня есть char *const *
или подобное, я бы сделал копию и передал это на всякий случай.