17

I know implicit conversion from char ** to const char ** cannot be done and why, and that the conversion to char *const * works. See bottom for links to explanation on that.

It all makes sense apart from one particular thing. So I have the following code:

#include <stdio.h>

void
print(const char *const*param)
{
    printf("%s\n", param[0]);
}

int
main(int argc, char **argv)
{
    print(argv);
    return 0;
}

If I compile this as a C++ code, it compiles quite fine. However, if the same code is compiled as a C code only, I get an error (well, a warning, but let's suppose -Werror, i.e. treat warnings as errors).

gcc:

test.c: In function ‘main’:
test.c:12:11: warning: passing argument 1 of ‘print’ from incompatible pointer type [-Wincompatible-pointer-types]
     print(argv);
           ^
test.c:4:1: note: expected ‘const char * const*’ but argument is of type ‘char **’
 print(const char *const*param)
 ^

clang:

test.c:12:11: warning: passing 'char **' to parameter of type 'const char *const *' discards qualifiers in nested pointer types [-Wincompatible-pointer-types-discards-qualifiers]
    print(argv);
          ^~~~
test.c:4:25: note: passing argument to parameter 'param' here
print(const char *const*param)
                        ^

Both behaviours are standard-independent and also compiler-independent. I tried various standards with both gcc and clang.

There are two reasons for this inquiry. Firstly, I want to understand whether there is a difference and, secondly, I have a function that does nothing with any layer of the pointers and I need it to be able to work with const char ** as well as char *const * and char **. Explicitly casting each call is not maintainable. And I have no idea how should the function prototype look like.


This is the question that started my curiosity: Implicit conversion from char** to const char**

And here is another nice explanation for the char ** => const char** problem: http://c-faq.com/ansi/constmismatch.html

If the links are confusing related to this question, feel free to edit them out.

nert
  • 942
  • 6
  • 17
  • Warnings that aren't useful can be suppressed, with `-Wno-incompatible-pointer-types` (for gcc) and `-Wno-incompatible-pointer-types-discards-qualifiers` (for clang) in this case (that's why the warning message shows the warning flag that activates it: so you can suppress it if it isn't useful to you). Then you can continue with `-Werror` without problems. – C. K. Young Feb 10 '16 at 15:58
  • 2
    @chrisjester-young That's not his question how to suppress warning. – SwiftMango Feb 10 '16 at 16:02
  • @texasbruce I'm aware. I can't tell the OP why the warning is shown by default in C but not in C++, since I'm not a gcc or clang developer and I didn't implement the warning criteria. – C. K. Young Feb 10 '16 at 16:04
  • 2
    C has a simpler, less flexible type system. C++ has a more powerful type system. That's all. There's no technical reason why C *cannot* allow this conversion. It can. It simply does not. – n. m. could be an AI Feb 10 '16 at 16:06
  • 2
    @Olaf No, C++ allows cast from `Foo**` to `const Foo* const*`. – 2501 Feb 10 '16 at 16:13
  • @user2079303: I hate to appologise, but I do if I was on the wrong track. I somehow added an `f` in my mind to the `print` call, thus read `printfd(argv);`. – too honest for this site Feb 10 '16 at 16:54
  • @rici: Thanks, I somehow mess it up. – too honest for this site Feb 10 '16 at 16:55
  • 1
    @nert: The question of "why?" has been around for a very long time. I've been searching for the answer too, but it appears that the only answer here is "It just so happened historically, and no one cares to correct this issue". I don't know why C does not want to adopt the C++ approach to const-correctness in such conversions. It would break no legacy code. – AnT stands with Russia Feb 10 '16 at 18:48

1 Answers1

15

C and C++ are different in this respect. I don't have an answer to why C++ is more generous, other than that the C++ behaviour seems to me to be correct.

C simply doesn't allow indirect const conversion. That is a conservative, easy-to-implement restriction, with the unfortunate consequence that you cannot provide char*[] to a function expecting char const* const*. The restriction is in §6.3.2.3, paragraph 2, and it is simply not recursive:

For any qualifier q, a pointer to a non-q-qualified type may be converted to a pointer to the q-qualified version of the type; the values stored in the original and converted pointers shall compare equal.

C++ allows conversions according to a somewhat complex formulation in §4.4 [conv.qual], paragraph 3. It is permitted to convert

T cvn Pn-1cvn-1 … P1cv1 P0cv0T cv'n Pn-1cv'n-1 … P1cv'1 P0cv'0

(where T is a type; P1…Pn are pointer/array type constructors, and each cv0…cvn is some possibly empty subset of const and volatile)

provided that:

  1. For every k > 0, cvk is a subset of cv'k (so you can't remove a const or a volatile), and

  2. If cvk and cv'k differ for some k > 0, all the following cv'i>k include const.

In the actual standard, that expression is reversed; I put it in the order of declaration, whereas in the standard it is in order of application of the pointer/array constructors. I didn't change the direction of the numbering, though, which is why they are numbered right to left. I also left out some details -- for example, it's not strictly necessary for the two Ts to be identical -- but I think it gives an idea of the intention.

The explanation for the first restriction is reasonably obvious. The second restriction prevents the problem described in the C FAQ, where a const pointer might be stored into a non-const pointer object, and then subsequently used to mutate the const object it points to.

The bottom line is that in C++, your prototype const char *const * param will work with arguments of type char**, const char**, or even char*const*, but in C only the last one will work without warning, and it is the least useful. The only workaround I know of (other than switching to C++) is to ignore the warning.

For what it's worth, there is a note in the Rationale section of the Posix specification of the exec* interfaces about the problem this causes for these prototypes, and the workaround selected by Posix, which is to use char*[] as the prototype and textually note that these are constant: (emphasis added)

The statement about argv[] and envp[] being constants is included to make explicit to future writers of language bindings that these objects are completely constant. Due to a limitation of the ISO C standard, it is not possible to state that idea in standard C. Specifying two levels of const-qualification for the argv[] and envp[] parameters for the exec functions may seem to be the natural choice, given that these functions do not modify either the array of pointers or the characters to which the function points, but this would disallow existing correct code. Instead, only the array of pointers is noted as constant.

There's a useful compatibility chart following that paragraph, which I didn't quote because of the formatting limitations of this site.

Community
  • 1
  • 1
rici
  • 234,347
  • 28
  • 237
  • 341
  • 17
    @Olaf: The question asks for a comparison. I'm comparing. You don't have to like the comparison. – rici Feb 10 '16 at 16:15
  • 19
    @Olaf: Oranges have more vitamin C than apples. Apples have more fibre. – rici Feb 10 '16 at 16:20
  • The reason for the recursive C++ definition is likely that due to the special properties of const references the situation is much more likely in C++, so it needed treating there. – Jan Hudec Feb 10 '16 at 17:06
  • 1
    C and C++ are different languages, but they are still closely related, because C++ was designed to be able to directly include standard C library headers and call the functions defined therein. – Jan Hudec Feb 10 '16 at 17:08
  • @JanHudec: The problem with prototypes and C is noted in the Posix standard as a "limitation of the ISO C standard"; it affects the prototypes for the `exec*` system interfaces (which is where the note is) and possibly others. So the issue has been considered problematic for some time, but evidently not so problematic that the standard was updated. – rici Feb 10 '16 at 17:58
  • 1
    @nert: That's award enough for me :) The standards themselves are paywalled, but the committees make drafts available for free; the closest draft to the standard generally differs only in small editorial corrections, and that's what you will see quoted here. For C: http://www.open-std.org/jtc1/sc22/wg14/www/standards (link says: WG14 N1570) For C++: https://isocpp.org/std/the-standard – rici Feb 10 '16 at 19:53
  • Is it possible to write in C a function whose prototype affirms "I guarantee not a single bit, nested or not, of this `char **` pointer will be modified"? Or is it impossible and it's better to just write the function as `char **` ? – cesss Mar 27 '17 at 11:46
  • @cesss: you can easily write the prototype. The problem is that you make it really annoying for the caller, so much so that it's not worth the effort. That is really what this discussion is about. – rici Mar 27 '17 at 14:13
  • @rici: I meant writing a function prototype in a way that it won't issue a warning on current C compilers if you call it with a `char **`argument. I tried everything with clang, even with `char const *const *const` (which to my knowledge shouldn't issue a warning, as any cast from `char **` to `char const *const *const` is obviously safe, no matter how picky you are, as you are affirming that neither the pointed to data, nor the final pointer, nor any intermediate pointers, shall be modified), but, however, clang (when compiling C code) still issues a warning when casting from `char **` – cesss Mar 27 '17 at 15:51
  • @cesss: yes, that is exactly what my answer says. No, there is no solution in C although it works fine in C++. If there were a C solution, Posix would use it; they are people with a lot more experience in C than either of us so I think they can be trusted when they say there is no solution. – rici Mar 27 '17 at 15:55