简体   繁体   中英

Why template function with 'const' from left of parameter type is misbehaving against the rule of type deduction for pointer argument?

Consider this pseudo code for a type deduction case:

template<typename T> void f(ParamType param);

Call to function will be: f(expr);

According to type deduction case where ParamType is not a reference, pointer, nor a universal reference (see S. Meyers "Effective Modern C++", p.14), but passed by value, to determine type T, one needs firstly to ignore the reference and const part of 'expr' and then pattern-match exprs type to determine T.

The driver will be:

void PerformTest() {

    int i = 42;
    int* pI = &i;
    f_const_left(pI);
    f_non_template_left(pI);
    f_const_right(pI);
    f_non_template_right(pI);
}

Now consider these functions, which, using this deduction rule, are showing some counter-intuitive results while being called with pointer as an argument:

template<typename T> void f_const_left(const T t) {
    // If 'expr' is 'int *' then, according to deduction rule for value parameter (Meyers p. 14),
    // we need to get rid of '&' and 'const' in exp (if they exist) to determine T, thus T will be 'int *'.
    // Hence, ParamType will be 'const int *'.
    // From this it follows that:
    //    1. This function is equivalent to function 'func(const int * t){}'
    //    2. If ParamType is 'const int *' then we have non-const pointer to a const object,
    //       which means that we can change what pointer points to but cant change the value
    //       of pointer address using operator '*'
    *t = 123;// compiler shows no error which is contradiction to ParamType being 'const int *'

    t = nullptr; // compiler shows error that we cant assign to a variable that is const

    // As we see, consequence 2. is not satisfied: 
    // T is straight opposite: instead of being 'const int *'
    // T is 'int const *'.
    // So, the question is:
    // Why T is not 'const int*' if template function is f(const T t) for expr 'int *' ?
}

Consider consequence 1.:

Lets create an equivalent non-template function:

void f_non_template_left(const int* t) {
    // 1. Can we change the value through pointer?
    *t = 123; // ERROR: expression must be a modifiable lvalue
    // 2. Can we change what pointers points to?
    t = nullptr; // NO ERROR

    // As we can see, with non-template function situation is quite opposite.
}

For for completeness of the experiment, lets also consider another pair of functions but with 'const' being placed from the right side of a T: one template function and its non-template equivalent:

template<typename T> void f_const_right(T const t) {
    // For expr being 'int *' T will be 'int *' and ParamType will be 'int * const',
    // which is definition of a constant pointer, which cant point to another address,
    // but can be used to change value through '*' operator.
    // Lets check it:

    // Cant point to another address:
    t = nullptr; // compiler shows error that we cant assign to a variable that is const

    // Can be used to change its value:
    *t = 123;
    // So, as we see, in case of 'T const t' we get 'int * const' which is constant pointer, which
    // is intuitive.
}

Finally, the non-template function with 'const' from the right side of type:

void f_non_template_right(int* const t) {
    // 1. Can we change the value through pointer?
    *t = 123; // No errors
    // 2. Can we change what pointers points to?
    t = nullptr; // ERROR: you cant assign to a variable that is const

    // As we can see, this non-template function is equivalent to its template prototype
}

Can someone explain why there is such insonsistency between template and non-template functions? And why template function with 'const' on the left is behaving not according to the rule of deduction?

(Referencing the C++14 Standard)

Your f_non_template_* functions aren't entirely correct.

Since T is a template parameter, it'll behave as if it is a unique type:

14.5.6.2 Partial ordering of function templates

(3) To produce the transformed template, for each type, non-type, or template template parameter (including template parameter packs (14.5.3) thereof) synthesize a unique type, value, or class template respectively and substitute it for each occurrence of that parameter in the function type of the template.

So to correctly test this your non-template functions would need to defined like this:

using TT = int*;

void f_non_template_left(const TT t) {
    /* ... */
}

void f_non_template_right(TT const t) {
    /* ... */
}

godbolt example

at which point you'll get exactly the same behaviour as with the templated functions.


Why it works that way

In this case T would be deduced to int* , which as a unique type would be a compound type :

3.9.2 Compound types

(1) Compound types can be constructed in the following ways:
[...]
(1.3) — pointers to void or objects or functions (including static members of classes) of a given type
[...]

And the cv-rules for compound types are as following:

3.9.3 CV-qualifiers

(1) A type mentioned in 3.9.1 and 3.9.2 [Compound Type] is a cv-unqualified type. Each type which is a cv-unqualified complete or incomplete object type or is void (3.9) has three corresponding cv-qualified versions of its type: a constqualified version, a volatile-qualified version, and a const-volatile-qualified version.
(2) A compound type (3.9.2) is not cv-qualified by the cv-qualifiers (if any) of the types from which it is compounded. Any cv-qualifiers applied to an array type affect the array element type, not the array type (8.3.4).

So your cv-qualifiers for T in your template function refer to the top-level constness of the compound-type T in both your cases, so

template<typename T> void f_const_left(const T t);
template<typename T> void f_const_right(T const t);

are actually equivalent.

The only exception to this would be if T would be an array-type, in which case the cv-qualifier would apply to the elements of the array instead.


If you want to specify the constness of the pointed-to value, you could do it like this:

//const value
template<class T>
void fn(const T* value);

// const pointer
template<class T>
void fn(T* const value);

// const value + const pointer
template<class T>
void fn(const T* const value);

// Hence, ParamType will be 'const int *'.

No, it will not.

int * is a sequence of two tokens. One of those tokens is a typename ( int ), and the other is * . When combined in this way, the two tokens name a single type: pointer to an int .

const int is a sequence of two tokens. When combined, they name the type: const int .

const int* is a sequence of 3 tokens. When combined as such, they name a single type. The rules of C++ type naming are such that the const applies "const-ness" to the type specified by whatever is to the immediate left of it. If nothing is to its left, it applies to what is immediately to the right. So if you consider this as an expression, it is read as (const int)* . Therefore, this token sequence names the type: pointer to a const int .

const T , where T is a typename (which could be a template parameter, a type-alias, or the name of a type), is a sequence of two tokens. When combined, they name a single type: const (whatever type T designates).

If the type T happens to name is int* , then the type T designates is 'pointer to int ', as previously discussed. Therefore, const T designates 'const (pointer to int )' Note that the pointer is what is const, not the int being pointed to.

Substitution of type aliases and template parameters is not done by copying-and-pasting tokens. It is done by applying the rules of typenames to the alias as a single unit. Whatever T is, the qualifiers like const apply to T as a unit.

int * const is a sequence of three tokens. Per the aforementioned rules, const is applied to whatever is to its left, if anything. So const applies to the * . Therefore, these tokens name the type: const pointer to int .

T const is a sequence of two tokens. When combined as such, they name a single type: const (whatever type T designates).

This is why T const and int * const behave the same, but const T and const int * don't.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM