C++11,14,17中auto和decltype相关知识及拓展
前言
这篇文章是上篇文章《C++prvalue,xvalue和lvalue的相关知识》的续作,上次我们已经把prvalue,xvalue和lvalue说清楚了,本篇文章就来探讨一下prvalue,xvalue和lvalue与decltype之间的联系。顺便咱们也把auto类型说明符也都拓展一下。
从初始化器和表达式中推导( Deduction from Initializers and Expressions)
C++11包括声明一个类型是从其初始化器推导出变量类型的能力(auto)。它还提供了一种机制来表示已命名对象(变量或函数)或表达式的类型(decltype)。这些设施原来非常方便,而C++14和C++17在这个主题上增加了额外的变体。
auto类型说明符
编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。c++11新标准引入了auto类型说明符,用它就能让编译器替我们去分析表达式所属的类型。
1
2//由val1和val2相加的结果可以推断出item的类型
auto item = val1 + val2; // item初始化为val1和val2相加的结果val1
和
val2
相加的结果来推断item
的类型。如果val1
和val2
是类sales_item
的对象,则item
的类型就是sales_item
;如果这两个变量的类型是double
,则item
的类型就是double
,以此类推。
复合类型,常量和auto
编译器推断出来的auto类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则。
首先,正如我们所熟知的,使用引用其实是使用引用的对象,特别是当引用被用作初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为auto的类型:
1
2
3int i = 0,&r = i;
auto a = r;
// a的类型为int(r是i的别名,而i是一个整数)顶层const
,同时底层const
则会保留下来,比如当初始值是一个指向常量的指针时:
1
2
3
4
5const int ci = i, &cr = ci;
auto b = ci;// b的类型为int(ci的顶层const 特性被忽略掉了)
auto c = cr;// c的类型为int( cr是ci的别名,ci本身是一个顶层const )
auto d = &i;// d是一个整型指针(int *)
auto e = &ci;// e是一个指向整数常量的指针(const int *)(对常量对象取完地址后,常量对象的顶层const在auto处转变为底层const)顶层const
,需要明确指出:
1
const auto f = ci; // ci的推演类型是int,f是const int
1
2
3auto &g = ci; // g是一个整型常量引用,绑定到ci
auto &h = 42; //错误:不能为非常量引用绑定字面值(42的类型为int,左值引用不能绑定到右值)
const auto &j=42; //正确:可以为常量引用绑定字面值(42发生临时物化,产生一个xvalue的临时对象让常量引用绑定)auto &g = ci;
),则此时的const就不是顶层const
了(g的类型为const int &
,此处的const为底层const
)。
要在一条语句中定义多个变量,切记,符号&
和*
只从属于某个声明符,而非基本数据类型的一部分,因此初始值必须是同一种类型:
1
2
3auto k = ci, &l = i;// k是int,l是int&
auto &m = ci,*p = &ci;// m是对整型常量的引用(const int &),p是指向整型常量的指针(const int *)。
auto &n= i, *p2 = &ci;//错误:i的基本数据类型是int而&ci的基本数据类型是const int
进一步探讨auto类型说明符
auto类型说明符可用于许多地方(主要是namespace作用域和local作用域),以从其初始化器推导变量的类型。在这种情况下,auto被称为占位符类型(a placeholder
type)(另一种占位符类型deltype(auto)
,文章后面将会描述)。例如,您可以使用:
1
2
3
4
5
6
7
8
9
10template<typename Container>
void useContainer(Container const& container)
{
auto pos = container.begin(); //auto示例一
while (pos != >container.end())
{
auto& element = *pos++; //auto示例二
… // operate on the element
}
}
1
2
3typename Container::const_iterator pos = container.begin();
…
typename std::iterator_traits<typename Container::iterator>::reference element = *pos++;T
取代,然后推导继续进行,就好像该变量是一个函数形参,它的初始化器相当于函数实参。
对于第一个auto示例,它对应于以下情况:
1
2
3template<typename T>
void deducePos(T pos);
deducePos(container.begin());T
看作auto
,是要被推导出的类型。这样做的直接后果之一是,auto类型的变量永远不会是引用类型。
在第二个auto示例中使用auto&
说明了如何生成一个引用类型的推导。其推导相当于以下函数模板和调用:
1
2template<typename T> deduceElement(T& element);
deduceElement(*pos++);element
总是引用类型,并且它的初始化器不能生成一个临时对象。
auto与右值引用
也可以将auto与右值引用组合起来,但这样做使其行为像一个转发的引用(a
forwarding reference),例如:
auto&& fr = …;
我们还是基于函数模板来看待它:
1
template<typename T> void f(T&& fr);// auto replaced by template parameter T
1
2
3
4
5int x;
auto&& rr = 42; // OK: rvalue reference binds to an rvalue
个(auto = int)
auto&& lr = x; // Also OK:reference collapsing makes. lr an lvalue reference
个(auto = int&)
例如,它通常是在基于范围的循环中声明迭代值的首选方法:
1
2
3
4
5template<typename Container>
void g(Container c)
{
for (auto&& x: c) { … }
}auto&&
,我们可以确信我们正在遍历的值没有产生额外的副本。如果需要完美转发绑定值,则可以像往常一样在变量上调用std::forward<T>()
。这使得一种“延迟”的完美转发成为可能。有关示例,请参见《c++
template 2nd》p167。
除了引用之外,还可以组合auto说明符来创建const对象、指针、成员指针等等,但auto必须声明成“main”类型说明符(基本数据类型)。它不能嵌套在模板参数中或跟在基本数据类型后面的声明部分中(
part of the declarator that follows the type
specifier)。具体请看下面的示例:
1
2
3
4
5
6
7
8template<typename T> struct X { T const m; };
auto const N = 400u; // OK: constant of type
auto* gp = (void*)nullptr; // OK: gp has type void*
X<auto> xa = X<int>(); // ERROR: auto in template
int const auto::*pm2 = &X<int>::m; // ERROR: auto is >part of the “declarator”
推导返回类型 c++14
C++14增加了另一种情况,其中可推导auto占位符类型可以出现在函数返回类型中。例如:
1
auto f() { return 42; }
1
auto f() -> auto { return 42; }
默认情况下,lambda
也存在相同的机制:如果没有明确指定返回类型,则lambda的返回类型会像auto一样被推导出来:
1
2auto lm = [] (int x) { return f(x); };
// same as:auto lm = [] (int x) -> auto { return f(x); };
1
2auto f(); // forward declaration
auto f() { return 42; }
1
2int known();
auto known() { return 42; } //ERROR: incompatible return type
1
2
3
4struct S {
auto f(); // the definition will follow the class definition
};
auto S::f() { return 42; }
可推导的非类型参数(Deducible Nontype Parameter)until c++17
在C++17之前,非类型模板参数必须用特定的类型来声明。但是,该类型可以是一个模板参数类型。例如:
1
2template<typename T, T V> struct S;
S<int, 42>* ps;
1
2template<auto V> struct S;
S<42>* ps;//这样的话就简洁多了S<42>
的V类型被推断为int,因为42
的类型为int
。如果我们写的是S<42u>
,V
的类型就会被推导为无符号int
(《c++
template 2nd》p294)。
请注意,对非类型模板参数的类型的一般约束仍然有效。例如:
1
S<3.14>* pd;// ERROR: floating-point nontype argument
用decltype表示表达式的类型(Expressing the Type of an Expression with decltype)
虽然auto避免了写出变量的类型的需要,但它不容易允许人们使用该变量的类型(不能确定为某一指定类型)。deltype关键字解决了这个问题:它允许程序员表达表达式或声明的精确类型。但是,程序员应该小心decltype类型产生的细微差别
,这取决于传递的参数是一个声明定义出的实体(eg:int a;
//这里a属于被定义出的实体)还是一个表达式(eg:1+1;
//这里1+1这个整体是一个表达式):
如果e
是实体的名称(如变量、函数签名、枚举器或数据成员)或类成员访问过程,则delltype(e)
生成该实体的声明类型或表示类成员的类型。因此,decltype类型可以用来检查变量的类型。
当希望精确匹配现有声明的类型时,这一点很有用。例如,请考虑以下变量y1
和y2
:
1
2
3auto x = …;
auto y1 = x + 1;
decltype(x) y2 = x + 1;x
的初始化器,y1
可能具有也可能不具有与x
相同的类型:它取决于+
的行为。如果x
被推导出为int
,y1
也会是int
。如果x
被推导为char
,那么y1
将是int
,因为char
与1
的和是int
。在y2
类型中使用deltype(x)
确保它始终具有与x
相同的类型。
prvalue,xvalue和lvalue与decltype的关系:
- 如果
e
是任何其他表达式,则deltype(e)
将生成一个反映该表达式的类型(type)和值类别(value category),如下所示:
If e is an lvalue of typeT
, decltype(e) producesT&
. If e is an xvalue of typeT
, decltype(e) producesT&&
. If e is a prvalue of typeT
, decltype(e) producesT
. 参考《c++ template 2nd》的附录B可以了解更多value category的知识。
这种细微差别可以通过以下例子来证明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14void g (std::string&& s)
{
// check the type of s:
std::is_lvalue_reference<decltype(s)>::value; // false
std::is_rvalue_reference<decltype(s)>::value; // true (s as declared)
std::is_same<decltype(s),std::string&>::value; // false
std::is_same<decltype(s),std::string&&>::value; // true
// check the value category of s used as expression:
std::is_lvalue_reference<decltype((s))>::value; // true (s is an lvalue)
std::is_rvalue_reference<decltype((s))>::value; // false
std::is_same<decltype((s)),std::string&>::value; // true (T& signals an lvalue)
std::is_same<decltype((s)),std::string&&>::value; // false
}s
调用delctype:
1
decltype(s) //declared type of entity e designated by s
s
的声明类型std::sting&&
。在最后四个表达式中,decltype类型构造的操作数不仅仅是一个名称,因为在每种情况下,该表达式都是(s)
,它是一个用括号表示的名称。在这种情况下,该类型将反映(s)
的值类别:
1
decltype((s)) //check the value category of (s)
lvalue
:根据上面的规则,这意味着decltype((s))
是对std::string
的普通引用(lvalue
reference)。这是C++中为数不多的用括号表示表达式会改变程序含义的地方之一,而不是影响操作符的关联性。
decltype类型计算任意表达式e的类型,这一事实在不同的地方都有帮助。具体来说,deltype(e)
保留了关于表达式的足够信息,可以使它描述返回表达式e
本身的函数的返回类型:
deltype计算该表达式的类型,但它也将表达式的值类别传播给函数的调用者。例如,考虑一个简单的转发函数g(),它返回调用f()的结果:
1
2
3
4??? f();
decltype(f()) g() {
return f();
}g()
的返回类型取决于f()
的返回类型。如果f()
返回int&
,那么计算g()
的返回类型将首先确定表达式f()
具有类型int
。这个表达式是一个lvalue
,因为f()
返回一个左值引用,因此声明的返回类型变为int&
。类似地,如果f()
的返回类型是右值引用类型,则调用f()
将是xvalue
,decltype将生成与f()
返回的类型完全匹配的右值引用类型。本质上,这种形式的decltype类型采用了任意表达式的主要特征——它的类型和值类别——并以一种能够完美转发返回值的方式在类型系统中对它们进行编码。
当 value-producing
auto
的推导不足时,decltype也可能很有用。例如,假设我们有一个未知迭代器类型的变量pos
,并且我们希望创建一个变量元素来引用pos
存储的元素。我们可以使用:
1
2auto element = *pos;//这将始终有元素的副本产生。
auto& element = *pos;//引用pos存储的元素!*
返回一个值,那么程序将会失败。为了解决这个问题,我们可以使用decltype类型,以保持迭代器的运算符*
的值或引用性:
1
decltype(*pos) element = *pos;
pos
时使用引用
(*pos表达式的结果为左值,则decltype推断出来的类型为左值引用!),是迭代器的操作符*
返回一个值时复制值
。它的主要缺陷是它需要将初始化器表达式写入两次:一次在decltype中(不计算它),另一次在实际的初始化器中。C++14引入了decltype(auto)
来解决这个问题,我们接下来将讨论这个问题。
decltype(auto) c++14
C++14增加了一个功能,它是auto和decltype的组合:decltype(auto)
。与auto类型说明符一样,它是一个占位符类型
,并且变量、返回类型或模板参数的类型是由相关表达式的类型(initializer,
return value, or template
argument)确定的。但是,与仅使用auto不同,它使用模板参数推断的规则来确定感兴趣的类型,实际的类型是通过将decltype直接应用于表达式来确定的。举例说明了这一点:
1
2
3
4int i = 42; // i has type int
int const& ref = i; // ref has type int const& and refers to i
auto x = ref; // x has type int and is a new independent object
decltype(auto) y = ref; // y has type int const& and also refers to iy
的类型是通过将decltype应用到初始化器表达式中得到的,这里是ref
,它是int const&
。相比之下,auto类型推导的规则产生的则是类型int
。
另一个示例显示了索引std::vector
(索引出来的是一个lvalue《c++primer》p121)时的差异:
1
2
3std::vector<int> v = { 42 };
auto x = v[0]; // x denotes a new object of type int
decltype(auto) y = v[0]; // y is a reference (type int&)(Because [ ] operator produces an lvalue)
1
2decltype(*pos) element = *pos;//Redundant writing
decltype(auto) element = *pos;//which can now be rewritten as
1
2
3
4
5
6
7
8template<typename C> class Adapt
{
C container;
…
decltype(auto) operator[] (std::size_t idx) {
return container[idx];
}
};container[idx]
生成一个lvalue
,我们希望将该左值传递给调用者(他可能希望获取其地址或修改它):这需要一个左值引用类型,这正是decltype(auto)
解析到的类型。如果生成出来的是prvalue
(内置下表运算符只可能产生lvalue
和xvalue
,这里说的是如果,原因在《C++prvalue,xvalue和lvalue的相关知识》中已阐述),则引用类型将导致悬空引用(dangling
references),但幸运的是,deltype(auto)
将生成这种情况下的对象类型(而不是引用类型)。
在递归模板中延迟返回类型推断(Delaying return type deduction in recursive templates)
当模板的返回类型被指定为decltype(iter(Int<i-1>{}))
而不是decltype(auto)
时,在模板实例化过程中会遇到无限递归。
1
2
3
4
5
6
7
8
9
10
11
12template<int i>
struct Int {};
constexpr auto iter(Int<0>) -> Int<0>;//递归模板结束地方的声明
template<int i>//非类型参数(Nontype Parameter)《c++primer》p580
constexpr auto iter(Int<i>) -> decltype(auto) {
return iter(Int<i-1>{});
}
int main(){
decltype(iter(Int<10>{})) a;
}decltype(auto)
来延迟返回类型模板实例化的推断,从而解决返回类型实例化过早引发无限递归的问题。
与auto不同,delltype(auto)
不允许修改其类型的说明符或声明符操作符。例如:
1
2
3decltype(auto)* p = (void*)nullptr; // invalid
int const N = 100;
decltype(auto) const NN = N*N; // invalid
1
2
3int x;
decltype(auto) z = x; // object of type int
decltype(auto) r = (x); // reference of type int&return statements
的有效性:
1
2
3
4
5
6int g();
…
decltype(auto) f() {
int r = g();
return (r); // run-time ERROR: returns reference to temporary
}decltype(auto)
也可以用于可推导的非类型参数
,由于后面c++20将其剔除,所以这里就不继续深入探讨了,原因是一样,弊大于利,滥用将导致程序难以理解。
最后问大家一个小问题,auto会不会计算出表达式结果的值?,decltype会不会计算出表达式结果的值?,decltype(auto)呢?相信大家看完本篇文章后,这些问题就会不攻自破了 🙂 。如果本篇文章帮您解决了理论上难以理解的问题,记得点个赞哦!