C++11,14,17auto和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类型说明符,用它就能让编译器替我们去分析表达式所属的类型。
//由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的类型:
int i = 0,&r = i; auto a = r; // a的类型为int(r是i的别名,而i是一个整数)
其次,auto一般会忽略掉
顶层const
,同时底层const
则会保留下来,比如当初始值是一个指向常量的指针时:const 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)
如果希望推断出的auto类型是一个
顶层const
,需要明确指出:const auto f = ci; // ci的推演类型是int,f是const int
还可以将引用的类型设为auto,此时原来的初始化规则仍然适用:
auto &g = ci; // g是一个整型常量引用,绑定到ci auto &h = 42; //错误:不能为非常量引用绑定字面值(42的类型为int,左值引用不能绑定到右值) const auto &j=42; //正确:可以为常量引用绑定字面值(42发生临时物化,产生一个xvalue的临时对象让常量引用绑定)
设置一个类型为auto的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果我们给初始值绑定一个引用(
auto &g = ci;
),则此时的const就不是顶层const
了(g的类型为const int &
,此处的const为底层const
)。
要在一条语句中定义多个变量,切记,符号&
和*
只从属于某个声明符,而非基本数据类型的一部分,因此初始值必须是同一种类型:auto 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类型说明符的基础部分,大部分例子为《c++primer》p61页的,我在其基础上添加了注释以及对其进行部分修改,以便读者能快速领悟其含义。其他基础部分若有不懂请自行补充,不再赘述。
进一步探讨auto类型说明符
auto类型说明符可用于许多地方(主要是namespace作用域和local作用域),以从其初始化器推导变量的类型。在这种情况下,auto被称为占位符类型(a placeholder type)(另一种占位符类型
deltype(auto)
,文章后面将会描述)。例如,您可以使用:template<typename Container> void useContainer(Container const& container) { auto pos = container.begin(); //auto示例一 while (pos != >container.end()) { auto& element = *pos++; //auto示例二 … // operate on the element } }
上面示例中auto的两种使用消除了编写两种长且可能复杂的类型,即容器的迭代器类型和迭代器的值类型:
typename Container::const_iterator pos = container.begin(); … typename std::iterator_traits<typename Container::iterator>::reference element = *pos++;
自动推导使用与模板参数推导相同的机制。auto类型说明符可以被一个虚构的模板类型参数
T
取代,然后推导继续进行,就好像该变量是一个函数形参,它的初始化器相当于函数实参。对于第一个auto示例,它对应于以下情况:template<typename T> void deducePos(T pos); deducePos(container.begin());
这里
T
看作auto
,是要被推导出的类型。这样做的直接后果之一是,auto类型的变量永远不会是引用类型。
在第二个auto示例中使用auto&
说明了如何生成一个引用类型的推导。其推导相当于以下函数模板和调用:template<typename T> deduceElement(T& element); deduceElement(*pos++);
在这里,
element
总是引用类型,并且它的初始化器不能生成一个临时对象。
auto与右值引用
也可以将auto与右值引用组合起来,但这样做使其行为像一个转发的引用(a forwarding reference),例如:
auto&& fr = …;
我们还是基于函数模板来看待它:template<typename T> void f(T&& fr);// auto replaced by template parameter T
解释如下:
int 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&)
这种技术经常用于代码中绑定函数或操作符调用的结果对象,且不知道结果对象的值类别(lvalue vs.rvarue),进而不必复制该结果对象。
例如,它通常是在基于范围的循环中声明迭代值的首选方法:template<typename Container> void g(Container c) { for (auto&& x: c) { … } }
这里我们不知道容器迭代接口的签名( the signatures of the container’s iteration interfaces),但是通过使用
auto&&
,我们可以确信我们正在遍历的值没有产生额外的副本。如果需要完美转发绑定值,则可以像往常一样在变量上调用std::forward<T>()
。这使得一种“延迟”的完美转发成为可能。有关示例,请参见《c++ template 2nd》p167。
除了引用之外,还可以组合auto说明符来创建const对象、指针、成员指针等等,但auto必须声明成“main”类型说明符(基本数据类型)。它不能嵌套在模板参数中或跟在基本数据类型后面的声明部分中( part of the declarator that follows the type specifier)。具体请看下面的示例:template<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++委员会认为,额外的实施成本和滥用潜力超过了好处 😦。
推导返回类型 c++14
C++14增加了另一种情况,其中可推导auto占位符类型可以出现在函数返回类型中。例如:
auto f() { return 42; }
定义一个返回类型为int(42)的函数。这也可以使用后置返回类型的语法来表示:
auto f() -> auto { return 42; }
在后一种情况下,第一个auto宣告有尾置返回类型,而第二个auto是要推导出的占位符类型。但是直接这么写会显得很冗长。
默认情况下,lambda
也存在相同的机制:如果没有明确指定返回类型,则lambda的返回类型会像auto一样被推导出来:auto lm = [] (int x) { return f(x); }; // same as:auto lm = [] (int x) -> auto { return f(x); };
函数可以单独声明。对于返回类型是被推导出来的函数也一样:
auto f(); // forward declaration auto f() { return 42; }
但是,在这样的情况下,forward declaration的用途非常有限,因为该定义必须在使用该函数的任何地方都可见。也许令人惊讶的是,提供带有“resolved”返回类型的forward declaration是无效的。例如:
int known(); auto known() { return 42; } //ERROR: incompatible return type
大多数情况下,提前声明一个具有推导返回类型的函数,只有在能够将成员函数定义移动到类定义之外时才有用:
struct S { auto f(); // the definition will follow the class definition }; auto S::f() { return 42; }
可推导的非类型参数(Deducible Nontype Parameter)until c++17
在C++17之前,非类型模板参数必须用特定的类型来声明。但是,该类型可以是一个模板参数类型。例如:
template<typename T, T V> struct S; S<int, 42>* ps;
在本例中,必须指定非类型模板参数的类型,即在42之外指定int,可能非常繁琐。因此,C++17增加了声明非类型模板参数的能力,这些参数的实际类型是从相应的模板参数推导出来的。它们声明如下:
template<auto V> struct S; S<42>* ps;//这样的话就简洁多了
这里
S<42>
的V类型被推断为int,因为42
的类型为int
。如果我们写的是S<42u>
,V
的类型就会被推导为无符号int
(《c++ template 2nd》p294)。
请注意,对非类型模板参数的类型的一般约束仍然有效。例如:S<3.14>* pd;// ERROR: floating-point nontype argument
最后auto只在c++17中被允许用在模板参数中,c++20开始被移除,原因是弊大于利,滥用它会让程序变得更难读懂。
用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
:auto 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的知识。
这种细微差别可以通过以下例子来证明:
void 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:decltype(s) //declared type of entity e designated by s
这意味着deltype产生了
s
的声明类型std::sting&&
。在最后四个表达式中,decltype类型构造的操作数不仅仅是一个名称,因为在每种情况下,该表达式都是(s)
,它是一个用括号表示的名称。在这种情况下,该类型将反映(s)
的值类别:decltype((s)) //check the value category of (s)
我们的表达式指的是一个变量,因此是一个
lvalue
:根据上面的规则,这意味着decltype((s))
是对std::string
的普通引用(lvalue reference)。这是C++中为数不多的用括号表示表达式会改变程序含义的地方之一,而不是影响操作符的关联性。
decltype类型计算任意表达式e的类型,这一事实在不同的地方都有帮助。具体来说,deltype(e)
保留了关于表达式的足够信息,可以使它描述返回表达式e
本身的函数的返回类型:deltype计算该表达式的类型,但它也将表达式的值类别传播给函数的调用者。例如,考虑一个简单的转发函数g(),它返回调用f()的结果:??? f(); decltype(f()) g() { return f(); }
g()
的返回类型取决于f()
的返回类型。如果f()
返回int&
,那么计算g()
的返回类型将首先确定表达式f()
具有类型int
。这个表达式是一个lvalue
,因为f()
返回一个左值引用,因此声明的返回类型变为int&
。类似地,如果f()
的返回类型是右值引用类型,则调用f()
将是xvalue
,decltype将生成与f()
返回的类型完全匹配的右值引用类型。本质上,这种形式的decltype类型采用了任意表达式的主要特征——它的类型和值类别——并以一种能够完美转发返回值的方式在类型系统中对它们进行编码。
当 value-producingauto
的推导不足时,decltype也可能很有用。例如,假设我们有一个未知迭代器类型的变量pos
,并且我们希望创建一个变量元素来引用pos
存储的元素。我们可以使用:auto element = *pos;//这将始终有元素的副本产生。 auto& element = *pos;//引用pos存储的元素!
我们将总是会收到一个对元素的引用,但是如果迭代器的操作符
*
返回一个值,那么程序将会失败。为了解决这个问题,我们可以使用decltype类型,以保持迭代器的运算符*
的值或引用性: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直接应用于表达式来确定的。举例说明了这一点:int 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 i
y
的类型是通过将decltype应用到初始化器表达式中得到的,这里是ref
,它是int const&
。相比之下,auto类型推导的规则产生的则是类型int
。
另一个示例显示了索引std::vector
(索引出来的是一个lvalue《c++primer》p121)时的差异:std::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)
这很好地解决了前面示例中的冗余写法:
decltype(*pos) element = *pos;//Redundant writing decltype(auto) element = *pos;//which can now be rewritten as
它通常用在返回类型上。参考以下示例:
template<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)
时,在模板实例化过程中会遇到无限递归。template<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)
不允许修改其类型的说明符或声明符操作符。例如:decltype(auto)* p = (void*)nullptr; // invalid int const N = 100; decltype(auto) const NN = N*N; // invalid
还要注意,初始化器中的圆括号可能很重要:
int x; decltype(auto) z = x; // object of type int decltype(auto) r = (x); // reference of type int&
这尤其意味着括号可能会严重影响
return statements
的有效性:int g(); … decltype(auto) f() { int r = g(); return (r); // run-time ERROR: returns reference to temporary }
until C++17,
decltype(auto)
也可以用于可推导的非类型参数
,由于后面c++20将其剔除,所以这里就不继续深入探讨了,原因是一样,弊大于利,滥用将导致程序难以理解。
最后问大家一个小问题,auto会不会计算出表达式结果的值?,decltype会不会计算出表达式结果的值?,decltype(auto)呢?相信大家看完本篇文章后,这些问题就会不攻自破了 😃 。如果本篇文章帮您解决了理论上难以理解的问题,记得点个赞哦!