Top-Level Const Doesn't Influence a Function Signature

Top-level const doesn't influence a function signature

allow these two functions simultaneously as different function since they are really different as to "whether parameter can be written or not". Intuitively, it should be!

Overloading of functions is based on the parameters the caller provides. Here, it's true that the caller may provide a const or non-const value but logically it should make no difference to the functionality that the called function provides. Consider:

f(3);
int x = 1 + 2;
f(x);

If f() does different thing in each of these situations, it would be very confusing! The programmer of this code calling f() can have a reasonable expectation of identical behaviour, freely adding or removing variables that pass parameters without it invalidating the program. This safe, sane behaviour is the point of departure that you'd want to justify exceptions to, and indeed there is one - behaviours can be varied when the function's overloaded ala:

void f(const int&) { ... }
void f(int&) { ... }

So, I guess this is what you find non-intuitive: that C++ provides more "safety" (enforced consistent behaviour through supporting only a single implementation) for non-references than references.

The reasons I can think of are:

  • So when a programmer knows a non-const& parameter will have a longer lifetime, they can select an optimal implementation. For example, in the code below it may be faster to return a reference to a T member within F, but if F is a temporary (which it might be if the compiler matches const F&) then a by-value return is needed. This is still pretty dangerous as the caller has to be aware that the returned reference is only valid as long as the parameter's around.

T f(const F&);
T& f(F&); // return type could be by const& if more appropriate
  • propagation of qualifiers like const-ness through function calls as in:

const T& f(const F&);
T& f(F&);

Here, some (presumably F member-) variable of type T is being exposed as const or non-const based on the const-ness of the parameter when f() is called. This type of interface might be chosen when wishing to extend a class with non-member functions (to keep the class minimalist, or when writing templates/algos usable on many classes), but the idea is similar to const member functions like vector::operator[](), where you want v[0] = 3 allowed on a non-const vector but not a const one.

When values are accepted by value they go out of scope as the function returns, so there's no valid scenario involving returning a reference to part of the parameter and wanting to propagate its qualifiers.

Hacking the behaviour you want

Given the rules for references, you can use them to get the kind of behaviour you want - you just need to be careful not to modify the by-non-const-reference parameter accidentally, so might want to adopt a practice like the following for the non-const parameters:

T f(F& x_ref)
{
F x = x_ref; // or const F is you won't modify it
...use x for safety...
}

Recompilation implications

Quite apart from the question of why the language forbids overloading based on the const-ness of a by-value parameter, there's the question of why it doesn't insist on consistency of const-ness in the declaration and definition.

For f(const int) / f(int)... if you are declaring a function in a header file, then it's best NOT to include the const qualifier even if the later definition in an implementation file will have it. This is because during maintenance the programmer may wish to remove the qualifier... removing it from the header may trigger a pointless recompilation of client code, so it's better not to insist they be kept in sync - and indeed that's why the compiler doesn't produce an error if they differ. If you just add or remove const in the function definition, then it's close to the implementation where the reader of the code might care about the constness when analysing the function behaviour. If you have it const in both header and implementation file, then the programmer wishes to make it non-const and forgets or decides not to update the header in order to avoid client recompilation, then it's more dangerous than the other way around as it's possible the programmer will have the const version from the header in mind when trying to analyse the current implementation code leading to wrong reasoning about the function behaviour. This is all a very subtle maintainence issue - only really relevant to commercial programming - but that's the basis of the guideline not to use const in the interface. Further, it's more concise to omit it from the interface, which is nicer for client programmers reading over your API.

how to assign a top-level const from a promise when top-level await is not available

You need to define you var with "let" instead of const, then assign the value in the "then".

let value;
f(x).then(val => value = val)

Maybe you have a better way to do this, but with the contexte we have, I would do it this way.

Top-level or low-level constness or neither?

No, not really.

The distinction you're making is between an object with const-qualified type, and an expression with const-qualified type.

Since (from a usage perspective) it very rarely matters which one is in play, there is no meaningful terminology to distinguish them in any given case.

This does admittedly make it a little cumbersome to explain why the following program is totally valid and well-defined (if really, really bad style), despite casting away the const:

void foo(const int& x)
{
// the expression `x` has type `const int&` (before decay),
// but after casting away the constness we can modify the
// referent, because it happens to actually be non-`const`.
const_cast<int&>(x) = 66;
}

int main()
{
int x = 42; // object is not const!
foo(x);
}

For what it's worth, although "top-level const" is standard terminology, I'm not so sure about your "low-level const". The terms aren't even properly symmetrical! Pfft.

In foo() above we wouldn't actually write the const_cast because we would assume that all inputs are references to objects that really are const.

Why does a function declaration with a const argument allow calling of a function with a non-const argument?

Because it doesn't matter to the caller of the foo function whether foo modifies its copy of the variable or not.

Specifically in the C++03 standard, the following 2 snippets explain exactly why:

C++03 Section: 13.2-1

Two function declarations of the same name refer to the same function if they are in the same scope and
have equivalent parameter declarations (13.1).

C++03 Section: 13.1-3

Parameter declarations that differ only in the presence or absence of const and/or volatile are equivalent. Only the const and volatile type-specifiers at the outermost level of the parameter type specification are ignored in this fashion; const and volatile type-specifiers buried within a parameter type specification are significant and can be used to distinguish overloaded function declarations.

Defining a function with different signature

For the purposes of determining a function signature, any top level const qualifier is ignored. This is because it does not affect function callers. Function parameters are passed by value in any case so the function cannot affect the arguments passed in.

The top level const does affect the body of the function. It determines whether or not the parameter can be changed in the body of the function. It is the same function as the declaration though.

So yes, it is legal and the declaration and definition refer to the same function and not an overload.

Standard reference: 8.3.5 [dcl.fct] / 3: "[...] The type of a function is determined using the following rules. [...] Any cv-qualifier modifying a parameter type is deleted. [...] Such cv-qualifiers affect only the definition of the parameter within the body of the function; they do not affect the function type. [...]"

Use of 'const' for function parameters

The reason is that const for the parameter only applies locally within the function, since it is working on a copy of the data. This means the function signature is really the same anyways. It's probably bad style to do this a lot though.

I personally tend to not use const except for reference and pointer parameters. For copied objects it doesn't really matter, although it can be safer as it signals intent within the function. It's really a judgement call. I do tend to use const_iterator though when looping on something and I don't intend on modifying it, so I guess to each his own, as long as const correctness for reference types is rigorously maintained.



Related Topics



Leave a reply



Submit