C complains about passing char** value to function taking char const*const*const but C++ doesn't

1.8k views Asked by At

I'm having difficulties in understanding why C++ behaves in a more "relaxed" way than C when it comes to interpreting and creating types for the parameters of a function.

C does the simplest thing in the world, it sticks with what you write and that's it, C++ on the other hand operates in a twisted way that I can't really comprehend.

for example the popular argv which is a char* [] when passed to a function becomes char** and I really don't get why, what I expect and "want" is char * const * but I got this behaviour instead.

You can also read this article in PDF that talks about this differences between C and C++, the article also ends with this phrase:

Although C++ ignores top-level cv-qualifiers in parameter declarations when determining function signatures, it does not ignore those cv-qualifiers entirely.

and since I can't find this issue online ( Embedded System Programming - February 2000 , and this old issues are free ), I'm wondering what this phrase could possibly mean.

Someone can explain why this behaviour is the way it is in C++ ?

EDIT:

One of my examples is

#include <stdio.h>

void foo(int argc, const char *const *const argv) {
  printf("%d %s\n", argc, argv[0]);
}

int main(int argc, char *argv[]) {
  foo(argc, argv);
  return (0);
}

and if you compile this with gcc 4.8.1 you get the expected error

gcc cv_1.c 
cv_1.c: In function ‘main’:
cv_1.c:8:3: warning: passing argument 2 of ‘foo’ from incompatible pointer type [enabled by default]
   foo(argc, argv);
   ^
cv_1.c:3:6: note: expected ‘const char * const* const’ but argument is of type ‘char **’
 void foo(int argc, const char *const *const argv) {
      ^

this output makes implicit the fact that argv is interpreted as char**

5

There are 5 answers

2
David Rodríguez - dribeas On BEST ANSWER

Function arguments can be passed by value or by reference. In the case of by reference there is no top-level qualifier so we can ignore that case.

In the case of by-value parameters, the top-level qualifier affects only the copy, and is completely independent of the original that is used to copy-construct that argument. If the top level qualifier was not dropped from the signature, the following two functions would be valid and different overloads:

void f(int       i);
void f(int const i);

Now the question is, given a call to f(1) which of the two overloads should be selected? The problem here is that whether the argument is const or not does not affect what it can be constructed from, so the compiler would never be able to resolve which is the correct overload. The solution is simple: in the signature the top level qualifier is dropped and both are the same function.

2
AnT stands with Russia On

The PDF article you linked contains a number of incorrect statements about differences between C and C++ in their treatment of top-level cv-qualifiers. These differences either do not exist or have different nature from what is implied in the article.

In reality both C and C++ effectively ignore top-level cv-qualifiers in function parameter declarations when it comes to determining function signature and function type. The wording in C and C++ language standards (and the underlying mechanisms) can be conceptually different, but the end result is the same in both languages.

C++ does indeed directly ignore top-level cv-qualifiers on parameters when determining function type, as described in 8.3.5/5: "After producing the list of parameter types, any top-level cv-qualifiers modifying a parameter type are deleted when forming the function type."

Instead of directly ignoring such qualifiers, C relies on the C-specific notion of compatible type. It says that function types that differ only in top level cv-qualifiers on parameters are compatible, which for all means and purposes means that they are the same. The definition of the function type compatibility in 6.7.5.3/15 says: "In the determination of type compatibility and of a composite type, [...] each parameter declared with qualified type is taken as having the unqualified version of its declared type."

The linked PDF article states that in C the following sequence of declarations is illegal

void foo(int i);
void foo(const int i);

In reality it is perfectly legal in C. C simply requires that all declarations of the same entity in the same scope use compatible types (6.7/4). The two declarations above are compatible, which means that they simply legally redeclare the same function. (In C++ the above declarations are also legal and they also redeclare the same function.)

For additional examples, in both C and C++ the following initializations are valid

void foo(const int i);
void bar(int i);

void (*pfoo)(int) = foo;       // OK
void (*pbar)(const int) = bar; // OK

At the same time both C and C++ identically take into account top-level cv-qualifiers when it comes to determining local variable type of function parameter. For example, in both C and C++ the following code is ill-formed

void foo(const int i) {
  i = 5; // ERROR!
}

In both C and C++ a function declared with one top-level cv-qualification on its parameters can be later defined with a completely different cv-qualification of its parameters. Any differences in top-level cv-qualification do not constitute function overloading in C++.


Also, you repeatedly mentioned that char *[] is interpreted as char ** as something relevant. I don't see the relevance. In function parameter lists T [] declarations are always equivalent to T * declarations. But this has absolutely nothing to do with top-level cv-qualifiers.

Meanwhile the code sample in your edit fails to compile for reason that have nothing to do with top-level cv-qualifiers either. It fails to compile because there's no implicit conversion from char ** to const char *const * in C language. Note that this conversion does not involve and does not care about any top-level cv-qualifiers at all. The const qualifiers that affect this conversion are present only on the first and second levels of indirection.

This does indeed involve a difference between C and C++. In both C and C++ implicit conversion from char ** to const char ** is not allowed (see here for example). However, C++ allows implicit conversion from char ** to const char *const * while C still doesn't. You can read more about it here. But note, again, that in all these cases top-level cv-qualifiers are completely irrelevant. They play no role at all.

1
Eric Postpischil On

This is something of a guess, but the reason is that a function parameter with qualifiers is only a copy of the argument. Consider:

void foo(int * const a, volatile int b) { … }

What these qualifiers say is that the code in the function definition will not modify a (because it is const) and that the value of b may be accessed in ways unknown to the C++ implementation. (That is pretty odd; volatile objects are usually things like hardware registers or perhaps data shared between processes. But let‘s say we are debugging a problem, so we have temporarily marked b volatile to ensure that we can access it in the debugger.)

The C++ implementation must honor these qualifiers on a and b when it is compiling and executing the code that defines foo, so it cannot ignore these qualifiers.

However, consider the view of a caller to foo. The fact that foo treats a as const or b as volatile is irrelevant to the caller. Whatever arguments it specified were copied (e.g., to registers or to the stack) to be passed to foo. All it did was pass the values. If the declaration of foo had no qualifiers:

void foo(int *a, int b) { … }

then the behavior of the caller would not change: Either way, it simply passes the values of the arguments and calls foo. Therefore, these two declarations of foo are identical from the caller’s view, so they have the same signature.

7
bames53 On
void foo( char const * const * const) {}
void bar( char *x[]) {
  foo(x); // warning in C, nothing in C++
}

The reason compiling this example as C produces a warning but C++ doesn't produce any diagnostic is not because C and C++ are treating char *[] as different types, or because they're discarding or inserting const in different places, but simply because C and C++ define 'compatible pointer types' differently; C++ relaxes the rules because C's strict rules are not preventing real errors.

Consider: what exactly can you do with a char const * const * const that is not legal to do with a char **? Since no modifications can be made it's not possible to introduce any errors, and so such a restriction is of little value.

However this isn't to say that inserting consts doesn't permit code that might produce an error. For example:

void foo(char const **c) { *c = "hello"; }

void bar(char **c) {
  foo(c);
  **c = 'J';
}

The above code, if permitted, would write to a string constant, which is illegal.

C++ carefully defines incompatible pointer types such that the above is not allowed, while still relaxing the rules from C in order to permit more safe programs than C.

One advantage to C's rules is that they are very simple. Basically:

For two pointer types to be compatible, both shall be identically qualified and both shall be pointers to compatible types.

and

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.

On the other hand C++'s rules go on for several paragraphs and use complex definitions in order to specify exactly what pointer conversions are allowed. The rabbit hole starts at C++11 4.4 [conv.qual] paragraph 4.


I'm wondering what this phrase could possibly mean.

He's most likely referring to the fact that if a parameter is declared const when the function is being defined then the compiler will not allow the function definition to perform non-const operations on the parameter.

void foo(int x);
void bar(int x);

void foo(int const x) {
  ++x; // error, parameter is const
}

void bar(int x) {
  ++x; // okay, parameter is modifiable.
}
3
chux - Reinstate Monica On

Observations too big for a comment.

Only the first const in char const * const * const x elicits a C warning.
C++ (Visual) complains about 2 of the 8. Not sure why?

IMHO: neither language differentiates on the 3rd const with appears superfluous from the calling functions point-of-view.

void fooccc( char const * const * const x) { if(x) return; }
void foocc_( char const * const *       x) { if(x) return; }
void fooc_c( char const *       * const x) { if(x) return; }
void fooc__( char const *       *       x) { if(x) return; }
void foo_cc( char       * const * const x) { if(x) return; }
void foo_c_( char       * const *       x) { if(x) return; }
void foo__c( char       *       * const x) { if(x) return; }
void foo___( char       *       *       x) { if(x) return; }

int g(char *x[]) {
  fooccc(x); // warning in C passing argument 1 of 'fooccc' from incompatible pointer type
  foocc_(x); // warning in C "
  fooc_c(x); // warning in C "   error in C++ cannot convert parameter 1 from 'char *[]' to 'const char **const ' Conversion loses qualifiers
  fooc__(x); // warning in C "   error in C++ cannot convert parameter 1 from 'char *[]' to 'const char **'       Conversion loses qualifiers
  foo_cc(x); // no problem in C  no problem in C++
  foo_c_(x); // no problem in C  no problem in C++
  foo__c(x); // no problem in C  no problem in C++
  foo___(x); // no problem in C  no problem in C++
  return 0;
  }

Notes: Eclipse, gcc -std=c99 -O0 -g3 -Wall
C++ Visual Studio 10.0