Подтвердить что ты не робот

Идиоматический 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)

  • Заставляет пользователей вводить свой ввод, добавляя константу.

  • Что-то еще?

4b9b3361

Ответ 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 * или подобное, я бы сделал копию и передал это на всякий случай.