25

I am aware that in C you can't implicitly convert, for instance, char** to const char** (c.f. C-Faq, SO question 1, SO Question 2).

On the other hand, if I see a function declared like so:

void foo(char** ppData);

I must assume the function may change the data passed in. Therefore, if I am writing a function that will not change the data, it is better, in my opinion, to declare:

void foo(const char** ppData);

or even:

void foo(const char * const * ppData);

But that puts the users of the function in an awkward position. They might have:

int main(int argc, char** argv)
{
    foo(argv); // Oh no, compiler error (or warning)
    ...
}

And in order to cleanly call my function, they would need to insert a cast.

I come from a mostly C++ background, where this is less of an issue due to C++'s more in-depth const rules.

What is the idiomatic solution in C?

  1. Declare foo as taking a char**, and just document the fact that it won't change its inputs? That seems a bit gross, esp. since it punishes users who might have a const char** that they want to pass it (now they have to cast away const-ness)

  2. Force users to cast their input, adding const-ness.

  3. Something else?

Community
  • 1
  • 1
jwd
  • 10,837
  • 3
  • 43
  • 67
  • 1) `const` is a useless compiler annotation in C. – Conrad Meyer Mar 04 '11 at 17:13
  • 1
    @Conrad: I dunno if I'd say *useless*, but it does seem a bit ... gimpy – jwd Mar 04 '11 at 17:30
  • @j.w.: You can cast it away, you can cast it on to non-const things... and you can have const pointers and non-const pointers referring to the same object in memory -- it's a wimpy compiler annotation that only stops the stupidest of accidental uses. The trickier and harder to detect mistakes go completely undiscovered. – Conrad Meyer Mar 04 '11 at 17:41
  • And for completeness, according to one of your links, note that in C++ you should be able to cast "char **" "char const * const *" because it doesn't fall foul of this bug, so that is probably the theoretically correct way to declare the function, if you really need it. Obviously that doesn't help if you're using strict C, but if you're using C mixed with C++ or a compiler which can turn that on as an extension (if there are any?) it may be a sufficient choice. – Jack V. May 11 '11 at 12:40

4 Answers4

10

Although you already have accepted an answer, I'd like to go for 3) namely macros. You can write these in a way that the user of your function will just write a call foo(x); where x can be const-qualified or not. The idea would to have one macro CASTIT that does the cast and checks if the argument is of a valid type, and another that is the user interface:

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
}

The CASTIT macro looks a bit complicated, but all it does is to first check if X[0] is assignment compatible with char const*. It uses a compound literal for that. This then is hidden inside a sizeof to ensure that actually the compound literal is never created and also that X is not evaluated by that test.

Then follows a plain cast, but which by itself would be too dangerous.

As you can see by the examples in the main this exactly detects the erroneous cases.

A lot of that stuff is possible with macros. I recently cooked up a complicated example with const-qualified arrays.

Jens Gustedt
  • 76,821
  • 6
  • 102
  • 177
  • This is an example of how C becomes obfuscated. A later maintainer is going to have a hard time understanding what is going on here. Unless the name is all caps, I would assume that `toto()` was a function call, unless there was a comment everywhere it was used. If the later maintainer received a warning, it would take an uncomfortably long time to find out why as the compiler doesn't tell you about the macro. – atlpeg Mar 04 '11 at 20:41
  • @atlpeg: If all that disturbs you is that one should use all capital letters, this is no big deals, take it as `TOTO` if you want. For the rest I see your point but I don't agree completely. The lack of possibility to cast `char**` to `char const*const*` is clearly a defect of the language. C++ gets away with it much better. Forcing users with solutions 1 or 2 to introduce a lot of cast is a nightmare to maintain and really dangerous, because C only nows one type of cast. So if a user changes the type from `char` to `int` the cast would just accept that. – Jens Gustedt Mar 04 '11 at 20:53
  • Definitely interesting. Not sure I'll use it, but interesting (: – jwd Mar 07 '11 at 17:59
  • reporting broken link! Also, unfortunately, this has the problem that you can't pass `toto` as a function pointer. – Shahbaz Dec 17 '12 at 15:33
  • @Shahbaz, thanks for the report, effectively it seems that doxygen has changed their namemangling. For the pointers, yes, correct, you can't have everything. In any case, in the mean time we have gained `_Generic` with C11, so nowadays I probably would use that for this task. – Jens Gustedt Dec 17 '12 at 15:43
  • @Shahbaz, and yes for the brave hearted, you may use the same name for the macro as for the function. So then you can even use it as function pointer: a function like macro that is not followed by a `(` is left as such by the preprocessing phase. – Jens Gustedt Dec 17 '12 at 15:45
7

2 is better than 1. 1 is pretty common though, since huge volumes of C code don't use const at all. So if you're writing new code for a new system, use 2. If you're writing maintenance code for an existing system where const is a rarity, use 1.

nmichaels
  • 49,466
  • 12
  • 107
  • 135
2

Go with option 2. Option 1 has the disadvantage that you mentioned and is less type-safe.

If I saw a function that takes a char ** argument and I've got a char *const * or similar, I'd make a copy and pass that, just in case.

Fred Foo
  • 355,277
  • 75
  • 744
  • 836
  • Good responses - I gave you both an upboat and flipped a coin for the answer (: Looks like there's no silver bullet. – jwd Mar 04 '11 at 17:29
1

Modern (C11+) way using _Generic to preserve type-safety and function pointers:

// joins an array of words into a new string;
// mutates neither *words nor **words
char *join_words (const char *const words[])
{
// ...
}

#define join_words(words) join_words(_Generic((words),\
          char ** : (const char *const *)(words),\
    char *const * : (const char *const *)(words),\
          default : (words)\
))

// usage :
int main (void)
{
    const char *const words_1[] = {"foo", "bar", NULL};
    char *const words_2[] =       {"foo", "bar", NULL};
    const char *words_3[] =       {"foo", "bar", NULL};
    char *words_4[] =             {"foo", "bar", NULL};

// none of the calls generate warnings:
    join_words(words_1);
    join_words(words_2);
    join_words(words_3);
    join_words(words_4);

// type-checking is preserved:
    const int *const numbers[] = { (int[]){1, 2}, (int[]){3, 4}, NULL };
    join_words(numbers);
// warning: incompatible pointer types passing
// 'const int *const [2]' to parameter of type 'const char *const *'

// since the macro is defined after the function's declaration and has the same name,
// we can also get a pointer to the function
    char *(*funcptr) (const char *const *) = join_words;
}
talentless
  • 61
  • 5