идиоматический C для константных двойных указателей - PullRequest
24 голосов
/ 04 марта 2011

Я знаю, что в C вы не можете неявно преобразовать, например, char** в const char** (ср C-Faq , SO вопрос 1 , SO Вопрос 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 ++, где это менее проблематично из-за более глубоких константных правил C ++.

Что такое идиоматическое решение в C?

  1. Объявить foo как char**, и просто задокументировать тот факт, что он не изменит свои входные данные? Это кажется немного грубым, особенно так как он наказывает пользователей, у которых может быть const char**, что они хотят передать его (теперь они должны отбросить прочь постоянство)

  2. Заставить пользователей вводить свои данные, добавляя постоянство.

  3. Что-то еще?

Ответы [ 3 ]

7 голосов
/ 04 марта 2011

Хотя вы уже приняли ответ, я бы хотел перейти к 3), а именно макросам. Вы можете написать их так, что пользователь вашей функции просто напишет вызов foo(x);, где x может быть const -квалифицированным или нет. Идея состоит в том, чтобы иметь один макрос 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 -квалифицированными массивами .

6 голосов
/ 04 марта 2011

2 лучше, чем 1. 1 довольно распространено, поскольку огромные объемы кода на C вообще не используют const. Поэтому, если вы пишете новый код для новой системы, используйте 2. Если вы пишете код обслуживания для существующей системы, где const является редкостью, используйте 1.

2 голосов
/ 04 марта 2011

Перейдите к варианту 2. У варианта 1 есть тот недостаток, который вы упомянули, и он менее безопасен для типов.

Если бы я увидел функцию, которая принимает аргумент char **, и у меня есть char *const * или подобное, я бы сделал копию и передал бы ее, на всякий случай.

...