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类型说明符,用它就能让编译器替我们去分析表达式所属的类型。

//由val1和val2相加的结果可以推断出item的类型
auto item = val1 + val2; // item初始化为val1和val2相加的结果

此处编译器将根据val1val2相加的结果来推断item的类型。如果val1val2是类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类型可以用来检查变量的类型。
    当希望精确匹配现有声明的类型时,这一点很有用。例如,请考虑以下变量y1y2
 auto x =; 
 auto y1 = x + 1; 
 decltype(x) y2 = x + 1;

根据x的初始化器,y1可能具有也可能不具有与x相同的类型:它取决于+的行为。如果x被推导出为inty1也会是int。如果x被推导为char,那么y1将是int,因为char1的和是int。在y2类型中使用deltype(x)确保它始终具有与x相同的类型。

prvalue,xvalue和lvalue与decltype的关系:

  • 如果e是任何其他表达式,则deltype(e)将生成一个反映该表达式的类型(type)和值类别(value category),如下所示:
    一 If e is an lvalue of type T, decltype(e) produces T&.
    一 If e is an xvalue of type T, decltype(e) produces T&&.
    一 If e is a prvalue of type T, decltype(e) produces T.
    参考《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-producing auto的推导不足时,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(内置下表运算符只可能产生lvaluexvalue,这里说的是如果,原因在《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)呢?相信大家看完本篇文章后,这些问题就会不攻自破了 😃 。如果本篇文章帮您解决了理论上难以理解的问题,记得点个赞哦!

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值