为什么参数类型左侧带有 '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) {
/* ... */
}
此时您将获得与模板函数完全相同的行为。
为什么会这样
在这种情况下,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 const
和 int * const
表现相同,但 const T
和 const int *
不同的原因。
考虑类型推导案例的伪代码:
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) {
/* ... */
}
此时您将获得与模板函数完全相同的行为。
为什么会这样
在这种情况下,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 const
和 int * const
表现相同,但 const T
和 const int *
不同的原因。