Effective Modern C++翻译(4)-条款3

条款3 了解decltype

decltype是一个有趣的东西,给它一个变量名或是一个表达式,decltype会告诉你这个变量名或是这个表达式的类型,通常,告诉你的结果和你预测的是一样的,但是偶尔的结果也会让你挠头思考,开始找一些参考资料进行研究,或是在网上寻找答案。

 

我们从典型的例子开始,因为它的结果都是在我们预料之中的,和模板类型推导与auto类型推导相比(参见条款1和条款2),decltype几乎总是总是返回变量名或是表达式的类型而不会进行任何的修改

const int i = 0;           // decltype(i)是const int
 bool f(const Widget& w);  // decltype(w)是const Widget&
                           // decltype(f) 是 bool(const Widget&)
struct Point {
int x, y;                  // decltype(Point::x)是int
};                         // decltype(Point::y)是int
Widget w;                  // decltype(w) 是 Widget

if (f(w)) …                // decltype( f(w)) 是 bool
template<typename T>       // std::vector的简单版本
class vector {
public:
…
T& operator[](std::size_t index);
…
};
vector<int> v;            // decltype(v)是vector<int>
…
if (v[0] == 0) …          // decltype(v[i]) 是 int&

 

看,这没有什么令人惊讶的。

在C++11中,decltype的主要用处在当函数模板的返回类型取决于参数类型的时候。例如,我们想要写一个函数,它的参数有支持下标运算的容器和一个索引值,函数先对用户进行认证,然后返回下标运算的结果,所以函数的返回类型应该和下标运算的结果类型是一样的。

 

[]运算符作用在一个以T为元素的容器上时,通常返回T&,std::deque就是这样的,std::vector也几乎一样,唯一的例外是对于std::vecotr<bool>,[]运算符不返回一个bool&,相反的,它返回一个全新的对象,条款6将解释这是为什么,但是重要的是记住作用在容器上的[]运算符的返回类型取决于这个容器本身。

 

decltype让这件事变得简单,这里是我们写的第一个版本,显示了使用decltype推导返回类型的方法,这个模板还可以再精简一些,但是我们暂时先不考虑这个:

template<typename Container, typename Index> //可以工作
auto authAndAccess(Container& c, Index i)    // 但是能再精简
-> decltype(c[i])                            // 一些
{
authenticateUser();
return c[i];
}

函数名字前的auto和类型推导没有任何的关系,它暗示了C++11的追踪返回类型(trailing return type)语义正被使用,例如:函数的返回类型将在参数列表的后面声明(在->之后),追踪返回类型

的优势是函数的参数能在返回类型的声明中使用,例如,在authAndAccess中,我们用c和i来指定函数的返回类型,如果我们想要将返回类型声明在函数名在的前面,就像传统的函数一样,c和i是不能被使用的,因为他们还没有被声明。

 

使用这个声明,authAndAccess返回[]运算符作用在容器上时的返回类型,和我们想要的一样。

 

C++11允许推导单一语句的lambda的返回类型,C++14扩展了这个,使得lambda和所有函数(包括含有多条语句的函数)的返回类型都可以推导,这意味着在C++14中我们可以省略掉追踪返回类型(trailing return type),只留下auto,在这种形式下的声明中,auto意味着类型推导将会发生,详细的说,它意味着编译器将会从函数的实现来推导函数的返回类型:

template<typename Container, typename Index> // C++14支持
auto authAndAccess(Container& c, Index i)    // 并不是十分
{                                            // 正确
authenticateUser();
return c[i];                                 // 从c[i]推导返回类型
}

但是哪一种C++的类型推导规则将会被使用呢?模板的类型推导规则还是auto的,或者是decltype的?

 

也许答案会有些让人惊讶,带有auto返回类型的函数使用模板类型推导规则,尽管看起来auto的类型推导规则会更符合这个语义,但是模板类型推导规则和auto类型推导规则几乎是一模一样的,唯一的不同是模板类型推导规则在面对大括号的初始化式(braced initializer)时会失败。

 

既然这样的话,使用模板类型推导规则推导authAndAccess的返回类型是有问题的,但是auto类型推导规则也好不了多少,困难源自他们对左值表达式的处理。

 

像我们之前讨论过的,大多数[]运算符作用在以T为元素的容器上时返回一个T&,但是条款1解释了在模板类型推导期间,初始化表达式的引用部分将被忽略掉,考虑下面的客户代码,使用了带有auto返回类型(使用模板类型推导来推导它的返回类型)的authAndAccess:

std::deque<int> d;
…
authAndAccess(d, 5) = 10; //验证用户,返回d[5],
                          // 并将10赋值给它;
                          // 不会通过编译!

这里,d[5]返回了一个int&,但是对于authAndAccess函数,auto返回类型的推导将会去掉引用部分,因此产生的返回类型是int,作为函数的返回类型,int是一个右值,而上面的代码尝试把10赋给一个int类型的右值,这在C++中是禁止的,所以上面的代码无法通过编译。

 

问题源于我们使用的是模板类型推导规则,它会丢弃初始化表达式中的引用限定符。所以在这种情况下,我们想要的是decltype类型规则,decltype类型推导能允许我们确保authAndAccess返回的类型和表达式c[i]类型是完全一致的。

 

C++规则的制定者(The guardians of C++),预料到了在某种情况下类型推导需要使用decltype类型推导规则,所以在C++14中出现了decltype(auto)说明符,这个刚开始看起来可能会有些矛盾(decltype和auto?),但事实上他们是完全合理的,auto说明了类型需要被推导,decltype说明了decltype类型推导应该在推导中被使用,因此authAndAccess的代码会是下面这样:

template<typename Container, typename Index> //C++14支持;
decltype(auto)                               //能工作, 但是
authAndAccess(Container& c, Index i)         //仍需要
{                                            //改进
authenticateUser();

return c[i];
}

现在authAndAccess返回的类型将会和c[i]返回的类型完全一致,是当c[i]返回一个T&时,authAndAccess也会返回一个T&,而当c[i]返回一个对象时,authAndAccess也会返回一个对象。

 

decltype(auto)的使用并不局限于函数的返回类型,当你想要用decltype类型推导来推导初始化式时,你也可以很方便的使用它来声明一个变量。

Widget w;
const Widget& cw = w;
auto myWidget1 = cw;           // auto推导出的:
                               // myWidget1类型是Widget
decltype(auto) myWidget2 = cw; // decltype推导出的:
                               // myWidget2类型是
                               // const Widget&

 

但我知道有两件事会困扰你,一个是为什么authAndAccess仍需要改进,现在让我们补上这一段吧。

 

我们再看一次C++14版本下的authAndAccess函数声明:

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i);

容器是以一个左值的非常量引用传入的,因为返回一个容器中元素的引用允许我们来修改这个容器,但这意味着我们不可能传递一个右值的容器到这个函数中去,右值是无法绑定到一个左值的引用上的(除非是一个的常量左值引用,但本例中不是这样的)

 

无可否认,传递一个右值的容器给authAndAccess是一个边界情况,一个右值的容器,作为一个临时对象将会在包含authAndAccess的函数调用的语句结束后被摧毁(would typically be destroyed at the end of the statement containing the call to authAndAccess),这意味着容器中的一个元素的引用(这通常是authAndAccess函数返回的)将会在调用语句的结束时悬空,(and that means that a reference to an element in that container (which is typically what authAndAccess would return) would dangle at the end of the statement that created it)。然而,传递一个临时对象到authAndAccess中是有道理的,一个客户可能只是想要拷贝这个临时容器中的一个元素,例如:

std::deque<std::string> makeStringDeque(); // 工厂函数
                                           //从makeStringDeque的函数值中拷贝
                                           //容器的第五个元素
auto s = authAndAccess(makeStringDeque(), 5);

支持这种使用方法意味着我们需要修改c的声明,使得他可以同时接受左值和右值,这意味着c需要成为一个万能引用(universal reference)(见条款26)

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i);

在这个模板里,我们不知道我们操作的容器是什么类型的,这同时意味着我们忽略了容器下标所对应的元素的类型。利用传值方式传递一个未知的对象,通常需要忍受不必要的拷贝,对象被分割的问题(见条款17),还有来自同事的嘲笑,但是根据标准库中的例子(例如 std::string,std::vector和std::deque),这种情况下看起来也是合理的,所以我们坚持按值传递。

 

现在要做的就是更新模板的实现,结合条款27中的警告,使用std::forward来完成

template<typename Container, typename Index> //C++14的
decltype(auto)                               // 最终
authAndAccess(Container&& c, Index i)        // 版本
{
authenticateUser();
return std::forward<Container>(c)[i];
}

这个版本能完成任何我们想要完成的,但是需要一个支持C++14的编译器,如果你没有的话,你需要使用一个C++11的版本,这和C++14版本相似,除了你需要自己标注出返回的类型

template<typename Container, typename Index> //C++11的
auto                                         // 的最终
authAndAccess(Container&& c, Index i)        // 版本

-> decltype(std::forward< Container>(c)[i])
{
authenticateUser();
return std::forward<Container>(c)[i];
}

另一个值得对你唠叨的问题我已经标注在了这一条款的开始处了,decltype的结果几乎和你所期待的一样,这已经不足为奇了,说实话,你几乎不太可能遇到这个规则的例外情况,除非你是一个非常大的库的实现者。

 

为了完全理解decltype的行为,你需要让你自己熟悉一些特殊的情况,大多数在这本书里证明讨论起来会非常的晦涩,但是其中一条能让我们更加理解decltype的使用。

 

对一个变量名使用decltype产生声明这个变量时的类型,但是就像我说的,有名字的是左值表达式,但这没有影响decltype的行为,因为对于比变量名更复杂的左值表达式,decltype确保推导出的类型总是一个左值的引用,这意味着如果一个左值表达式不同于变量名的类型T(That is, if an lvalue expression other than a name has type T),decltype推导出的类型将会是T&,这几乎不会照成什么影响,因为大多数左值表达式的类型内部通常包含了一个左值引用的限定符,例如,返回左值的函数总是返回一个引用。

 

这里有一个值得注意的地方,在

int x=0;

x是一个变量的名字,所以decltype(x)的结果是int,但是将名字x用括号包裹起来,”(x)”产生了一个比名字更复杂的表达式,作为一个变量名,x是一个左值,C++同时定义了(x)也是一个左值,因此decltype((x))结果是int&,将一个变量用括号包裹起来改变了decltype最初的结果。

 

在C++11中,这仅仅会会让人有些奇怪,但是结合C++14中对decltype(auto)的支持后,你对返回语句的一些简单的变化会影响到函数最终推导出的结果。

decltype( auto) f1()
{
int x = 0;
…
return x;   // decltype(x) 是 int, 所以f1返回int
}
decltype(auto) f2()
{
int x = 0;
…
return (x); // decltype((x)) 是int&, 所以f2返回int&
}

注意到f2和f1不仅仅是返回类型上的不同,f2返回的是一个局部变量的引用,这种代码的结果是未定义的,你当然不希望发生这种情况。

 

你需要记住的是当你使用decltype(auto)的时候,需要格外注意,一些看起来无关紧要的细节会影响到decltype(auto)推导出的结果,为了确保被推导出的类型是你期待的, 可以使用条款4中描述的技术。

 

但同时不要失去对大局的注意,decltype(无论是独立使用还是和auto一起使用)推导的结果可能偶尔让人惊讶,但是这并不会经常发生,通常,decltype的结果和你所期待的类型一样,尤其是当decltype应用在变量名的时候,因为在这种情况下,decltype做的就是提供变量的声明类型。

 

请记住

  • decltype几乎总是返回变量名或是表达式的类型而不会进行任何的修改。
  • 对于不同于变量名的左值表达式,decltype的结果总是T&。
  • C++14提供了decltype(auto)的支持,比如auto,从它的初始化式中推导类型,但使用decltype的推导规则。
时间: 2024-08-01 22:42:22

Effective Modern C++翻译(4)-条款3的相关文章

Effective Modern C++翻译(5)-条款4

条款4:了解如何观察推导出的类型 那些想要知道编译器推导出的类型的人通常分为两种,第一种是实用主义者,他们的动力通常来自于软件产生的问题(例如他们还在调试解决中),他们利用编译器进行寻找,并相信这个能帮他们找到问题的源头(they're looking for insights into compilation that can help them identify the source of the problem.).另一种是经验主义者,他们探索条款1-3所描述的推导规则,并且从大量的推导情

Effective Modern C++翻译(2)-条款1

第一章 类型推导 C++98有一套单一的类型推导的规则:用来推导函数模板,C++11轻微的修改了这些规则并且增加了两个,一个用于auto,一个用于decltype,接着C++14扩展了auto和decltype可以使用的语境,类型推导的普遍应用将程序员从必须拼写那些显然的,多余的类型的暴政中解放了出来,它使得C++开发的软件更有弹性,因为在某处改变一个类型会自动的通过类型推导传播到其他的地方.   然而,它可能使产生的代码更难观察,因为编译器推导出的类型可能不像我们想的那样显而易见.   想要在

Effective Modern C++翻译(3)-条款2

条款2 明白auto类型推导 如果你已经读完了条款1中有关模板类型推导的内容,那么你几乎已经知道了所有关于auto类型推导的事情,因为除了一个古怪的例外,auto的类型推导规则和模板的类型推导规则是一样的,但是为什么会这样呢?模板的类型推导涉及了模板,函数和参数,但是auto的类型推导却没有涉及其中的任何一个. 这确实是对的,但这无关紧要,在auto类型推导和template之间存在一个直接的映射,可以逐字逐句的将一个转化为另外一个. 在条款1中,模板类型推导是以下面的模板形式进行举例讲解的:

Effective Modern C++翻译(7)-条款6:当auto推导出意外的类型时,使用显式的类型初始化语义

条款6:当auto推导出意外的类型时,使用显式的类型初始化语义 条款5解释了使用auto来声明变量比使用精确的类型声明多了了很多的技术优势,但有的时候,当你想要zag的时候,auto可能会推导出了zig.例如,我有一个函数,它以const Widget&作为参数,并且返回std::vector<bool>,每一个bool暗示了Widget是否提供了一个特殊的特性. std::vector<bool> features(const Widget& w); 进一步假设第

决定干点事儿--翻译一下《effective modern c++》

写了很多关于C++11的博客,总是觉得不踏实,很多东西都是东拼西凑.市场上也很少有C++11的优秀书籍,但幸运的是Meyers老爷子并没有闲赋,为我们带来了<effective modern c++>. 我们都要认清,一个人很难超越自我,超越自我的巅峰之作.因为不同的时代,也会早就不同的伟大作品. 说上面这段话的意思就是,我们不能期待<effective modern c++>能达到<effective c++>给我们带来的惊喜,但是也是出自大师之手. Learn ho

Effective Modern C++ 读书笔记 Item 1

最近发现了<Effective Modern C++>这本书,作者正是大名鼎鼎的Scott Meyers——<Effective C++>.<Effective STL>的作者. 而就在C++11逐渐普及,甚至是C++14的新特性也进入大家的视野的时候,<Effective Modern C++>一书应运而生.此书与其前辈一样,通过数十个条款来展开,只不过这次是集中于C++11和C++14的新特性.auto.decltype.move.lambda表达式……

[C++11] Effective Modern C++ 读书笔记

本文记录了我读Effective Modern C++时自己的一些理解和心得. item1:模板类型推导 1)reference属性不能通过传值参数传入模板函数.这就意味着如果模板函数需要一个reference类型的参数,必须在模板声明中将其声明为reference,否则,即使使用一个reference类型的变量调用模板函数,类型推导的结果将不带reference属性. 2)constant和volatile属性也不能通过传值参数传入模板函数,但是可以通过reference参数传入这些属性. 3

Effective C++_笔记_条款08_别让异常逃离析构函数

(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) C++并不禁止析构函数吐出异常,但它不鼓励你这样做.考虑如下代码: 1: class Widget{ 2: public: 3: ... 4: ~Widget() {...} //假设这个可能吐出一个异常 5: }; 6:  7: void doSomething() 8: { 9: vector<Widget> v ; //v在这里被自动销毁 10: ...

Effective C++_笔记_条款12_复制对象时勿忘其每一个成分

(整理自Effctive C++,转载请注明.整理者:华科小涛@http://www.cnblogs.com/hust-ghtao/) 编译器会在必要时候为我们的classes创建copying函数,这些“编译器生成版”的行为:将被烤对象的所有成员变量都做一份拷贝. 如果你声明自己的copying函数,意思就是告诉编译器你并不喜欢缺省实现中的某些行为.编译器仿佛被冒犯似的,会以一种奇怪的方式回敬:当你的实现代码几乎必然出错时却不告诉你.所以自己实现copying函数时,请遵循一条规则:如果你为c