3

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**

bames53
  • 86,085
  • 15
  • 179
  • 244
user2485710
  • 9,451
  • 13
  • 58
  • 102
  • 7
    Your example of argv is actually something that C++ inherited from C. Your PDF talks about *signatures*, they matter only for overload resolution. Doesn't have anything to do with cv checking. – Hans Passant Dec 03 '13 at 18:51
  • @HansPassant for what I care, this mechanism still drops my `const` if it is present at the top level and I would like to know what is the rationale behind this weird behaviour. `argv` is just a toy in this example. – user2485710 Dec 03 '13 at 18:52
  • 3
    Humor us with a concrete example, please. – Hans Passant Dec 03 '13 at 18:56
  • @HansPassant about what ? are you about to say that I can always declare an internal variable or pass it by reference and that even following this twisted logic there is no change in the observable behaviour outside the function ? – user2485710 Dec 03 '13 at 19:04
  • 1
    @user2485710: There's absolutely no difference between C and C++ in that regard. In both languages top level cv qualifiers have no effect of function signature. In both languages `char *[]` becomes `char **` in function parameter declarations. Where did you get the idea that there's any? – AnT stands with Russia Dec 03 '13 at 19:48
  • 1
    @user2485710: Oh, I see, it is stated in the linked PDF article... That article is absolutely incorrect. – AnT stands with Russia Dec 03 '13 at 19:50

5 Answers5

6

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.

David Rodríguez - dribeas
  • 204,818
  • 23
  • 294
  • 489
  • I don't get it, and the key step for me is, a named variable can possibly have a cv-qualifier, from what you are saying looks like it's something done just to save temporary values from creating problems, looks like the tradeoff is there; but can you refer to some specific paragraph from the C++ standard ? – user2485710 Dec 04 '13 at 11:46
  • @user2485710: No, the problem is not with temporaries, but with overload resolution. If you want, replace `1` by a named variable above. The top-level qualifier applies to the argument to the function. Objects that are either const or not const are created in exactly the same way, the same set of arguments for the constructor (or conversion function). The top-level `const` has no relevance for the caller and cannot be used by the compiler to distinguish one call to another. Pick a function, add/remove top level qualifiers, consider what calls are valid with either signature: the same. – David Rodríguez - dribeas Dec 04 '13 at 14:02
5

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.

Community
  • 1
  • 1
AnT stands with Russia
  • 312,472
  • 42
  • 525
  • 765
  • what is the paragraph in the standard that describes this ? the same it's true for templates ? – user2485710 Dec 04 '13 at 11:57
  • I bet it is. As for the article, it's quite old. In the years a bit before 2000, the people around me still happily used IIRC "Turbo C++ 32" compiler that worked in DOS environment and had lots of issues when it came to all the important bits.. Maybe the author of the article simply used something like that.. – quetzalcoatl Dec 04 '13 at 17:16
4

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.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • so, in a nutshell, you are saying that there is no change in the observable behaviour ( for what is out of `foo` ) and this is enough to reconsider the magnitude of this problem and saying that maybe it's good enough the way it is ? – user2485710 Dec 04 '13 at 11:54
3
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.
}
bames53
  • 86,085
  • 15
  • 179
  • 244
  • It's still not clear to me why `argv` is interpreted as `char**` by the compiler ( see my edit to the first post with an example ) since it is `char* []`, which is pretty much the point where I started experimenting with this. – user2485710 Dec 04 '13 at 12:08
  • @user2485710 What difference between `char *[]` and `char**` parameters in C are you concerned with? – bames53 Dec 04 '13 at 15:25
  • where `a` is `int a[]`, i see `a` as `int * const a`, so `char**` for `argv` it's really not what I expect to see. – user2485710 Dec 04 '13 at 15:32
  • `void foo(int a[]) { ++a; }` is legal in C AFAICT, whereas `void foo(int * const a) { ++a; }` is not legal. Can you write a complete program that shows the difference you are concerned with by producing different output when compiled as C++ vs C? It's okay if the different output is a compiler error in one language or the other. Be sure that the programs produce the same output when you replace the `int []` parameter with an explicit `int * const` or `int *`. – bames53 Dec 04 '13 at 15:56
  • since it all started from that compiler message basically, could you just explain why an array of `char *` becomes `char**` ? – user2485710 Dec 04 '13 at 16:07
  • @user2485710 Ah, the example you've added to your question is exactly what I was looking for. I've expanded my answer to address it. – bames53 Dec 04 '13 at 17:12
  • 1
    +1 for a good answer and presenting OP code as a nicely simplified `foo() bar()` in the top of the answer. Helps to focus on _the_ issue. – chux - Reinstate Monica Dec 04 '13 at 18:12
1

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

chux - Reinstate Monica
  • 143,097
  • 13
  • 135
  • 256
  • I believe the C++ rules boil down to: inserting const at any level requires const to be inserted at all higher levels, except the top level which doesn't matter. – bames53 Dec 04 '13 at 19:34
  • @bames53 Would appreciate if you have ready access to another C++ compiler, please report your findings and compiler info. – chux - Reinstate Monica Dec 04 '13 at 19:56
  • There are several online compilers listed on the [isocpp.org](http://isocpp.org/get-started) website, however rather than testing implementations extensively I'm just reading the C++ spec. your findings here match with my reading and with a couple quick tests (clang and vc++) – bames53 Dec 04 '13 at 20:08