为什么参数类型左侧带有 'const' 的模板函数的行为不符合指针参数的类型推导规则?

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

考虑类型推导案例的伪代码:

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

函数调用将是:f(expr);

根据 ParamType 不是引用、指针或通用引用的类型推导情况 (参见 S. Meyers“Effective Modern C++”,第 14 页),但按值传递,要确定类型 T,首先需要 忽略 'expr' 的引用和 const 部分,然后 pattern-match exprs 类型来确定 T.

driver 将是:

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);
}

现在考虑这些函数,它们使用此推导规则,在使用指针作为参数调用时显示一些 counter-intuitive 结果:

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 *' ?
}

考虑后果 1.:

让我们创建一个等效的 non-template 函数:

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.
}

为了实验的完整性,我们还考虑另一对函数,但是 'const' 放在 T 的右侧:一个模板函数及其 non-template 等价物:

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.
}

最后,non-template 函数从右边输入'const':

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
}

有人可以解释为什么模板和 non-template 函数之间存在这种不一致吗? 为什么左边有'const'的模板函数的行为不符合推导规则?

(参考 C++14 标准)

您的 f_non_template_* 函数不完全正确。

因为 T 是一个模板参数,它的行为就好像它是一个独特的类型:

14.5.6.2 函数模板的部分排序

(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.

因此,要正确测试它,您的非模板函数需要像这样定义:

using TT = int*;

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

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

godbolt example

此时您将获得与模板函数完全相同的行为。


为什么会这样

在这种情况下,T 将推导为 int*,作为唯一类型,它将是 复合类型 :

3.9.2 复合类型

(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
[...]

复合类型的cv规则如下:

3.9.3 CV 限定符

(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).

所以你的模板函数中 T 的 cv 限定符在你的两种情况下都指的是复合类型 T 的顶级常量,所以

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

实际上是等价的。

唯一的例外是 T 是数组类型,在这种情况下,cv 限定符将改为应用于数组的元素。


如果你想指定指向值的常量,你可以这样做:

//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 *'.

不,它不会(不仅仅是因为代码中没有使用 ParamType。为了这个答案的目的,让我们假设你的意思是 T,它在那里)。

int * 是两个标记的序列。其中一个标记是类型名称 (int),另一个是 *。当以这种方式组合时,这两个标记命名为一个类型:指向 int.

的指针

const int 是两个标记的序列。合并时,它们将类型命名为:const int.

const int* 是 3 个标记的序列。当这样组合时,它们命名为单一类型。 C++ 类型命名的规则是 const 将“const-ness”应用于由紧靠其左侧的任何内容指定的类型。如果它的左边没有任何东西,它适用于紧靠右边的东西。因此,如果您将其视为一个表达式,它会被读作 (const int)*。因此,此标记序列将类型命名为:指向 const int.

的指针

const T,其中 T 是类型名称(可以是模板参数、类型别名或类型名称),是两个标记的序列。组合时,它们命名为单一类型:const(T 指定的任何类型)。

如果名称 T 恰好是 int*,那么 T 指定的类型是 'pointer to int',如前所述。因此,const T 指定 'const (pointer to int)' 请注意, 指针 是常量,而不是指向的 int

类型别名和模板参数的替换不是通过复制和粘贴标记完成的。这是通过将类型名规则作为一个单元应用于别名来完成的。无论 T 是什么,像 const 这样的限定符都适用于 T 作为一个单位。

int * const 是三个标记的序列。根据上述规则,const 应用于其左侧的任何内容(如果有的话)。所以 const 适用于 *。因此,这些标记将类型命名为:const pointer to int.

T const 是两个标记的序列。当这样组合时,它们命名为单一类型:const(无论类型 T 指定)。

这就是为什么 T constint * const 表现相同,但 const Tconst int * 不同的原因。