码迷,mamicode.com
首页 > 编程语言 > 详细

《Effective Modern C++》翻译--条款3: 理解decltype

时间:2016-01-11 23:59:20      阅读:464      评论:0      收藏:0      [点我收藏+]

标签:

条款3:理解decltype

decltype 是一个非常有趣的怪兽。如果提供了一个名字或是表达式,decltype关键字将会告诉你这个名字或这个表达式的类型。通常情况下,结果与你的期望吻合。然而有些时候,decltype产生的结果领你挠头,使你去翻阅参考书或在网上问答中寻求答案。

我们先从通常的情况开始—这里没有暗藏惊喜。联系到在模板类型推导和auto类型推导中发生了什么,decltype关键字就像鹦鹉学舌一样,对于变量名称或表达式类型的推导跟模板类型推导和auto类型推导没有任何变化:

const int i = 0;           //decltype(i) is const int

bool f(const Widget& w);   //decltype(w) is const Widget&
                           //decltype(f) is bool(const Widget&)

struct Point{
  int x, y;                //decltype(Point::x) is int
};                         //decltype(Point::y) is int

Widget w;                  //decltype(w) is Widget

if (f(W)) ...              //decltype(f(w)) is bool

template<typename T>       //simplified version fo std::vector
class vector{
public:
  ...
  T& operator[](std::size_t index);
  ...
};

vector<int>v;               //decltype(v) is vector<int>
...
if(v[0] == 0)...            //decltype(v[i]) is int&

看到了吧,毫无惊喜。

在C ++11,对于decltype的主要用途是声明函数模板,其中函数的返回类型依赖于它的参数类型。例如,假设我们想编写一个函数,它支持通过方括号(即使用“[]”)加一个索引,然后进行身份验证,然后再重新打开索引的结果,用户的容器操作。该函数的返回类型应该与索引操作返回的类型相同。

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

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

template<typename Container, typename Index>     //works, but
auto autoAnadAccess(Container& c, Index i)       //requires
  -> decltype(c[i])                              //refinement
{
  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 only, and
auto authAndAccess(Container&c, Index i)       //not quite
{
  authenticateUser();
  return c[i];                                 //return type deduced from c[i]
}

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

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

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

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

std::deque<int> d;
...
authAndAccess(d, 5) = 10;    //authenticate user, return d[5], then assign 10 to it;
                             //this won‘t compile

这里,d[5]会返回int&,但是用auto类型推导来推导函数authAndAccess,将会去掉引用,变成返回一个int类型。这样,这个int返回值就成为了函数的返回值,是一个右值,然后上面的代码试图将10赋给一个右值。在C++中,这样的赋值是被拒绝的,所以上面的代码不会编译成功。

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

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

template<typename Container, typename Index>    //C++14 only;
decltype(auto)                                  //works, but
autoAndAccess(Container&c, Index i)             //still requires
{                                               //refinement
  authenticateUser();
  return c[i];
}

这样,函数autoAndAccess的返回类型与c[i]保持一致。特别强调的是,当c[i]返回一个T&时,authAndAccess也会返回一个T&,而当c[i]返回一个对象时,authAndAccess也会返回一个对象。

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

Widget w;

const Widget& cw = w;

auto myWidget1 = cw;            //auto type deduction;
                                //myWidget1‘s type is Widget

decltype(auto) myWidget2 = cw;  //decltype type deduction:
                                //myWidget2‘s type is
                                // const Widget&

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

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

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

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

无可否认,传递一个右值的容器给authAndAccess是一个边界情况,一个右值的容器,作为一个临时对象将会在包含authAndAccess的函数调用的语句结束后被摧毁。这意味着容器中的一个元素的引用(这通常是authAndAccess函数返回的)将会在调用语句的结束时悬空。然而,传递一个临时对象到authAndAccess中是有道理的,一个客户可能只是想要拷贝这个临时容器中的一个元素,例如:

std::deque<std::string> makeStringDeque();     //factory function

//make copy of 5th element of deque returned
//from 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来实现universal reference:


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

上面代码完成了我们想要的一切,但前提是需要支持C++14的编译器。如果你没有支持C++14的编译器,你就应该使用C++11中的模板类型。除了你需要自己明确出返回类型外,其他的与C++14没有区别:


template<typename Container, typename Index>       // final 
auto                                               // C++11  authAndAccess(Container&& c, Index i)              // version   
-> decltype(std::forward<Container>(c)[i]) 
{ 
  authenticateUser(); 
  return std::forward<Container>(c)[i]; 
}

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

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

但是就像我说的,对一个变量名使用decltype产生声明这个变量时的类型。有名字的是左值表达式,但这没有影响decltype的行为。因为对于比变量名更复杂的左值表达式,decltype确保推导出的类型总是一个左值的引用。这意味着如果一个左值表达式不同于变量名的类型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) is int, so f1 returns int 
} 

decltype(auto) f2() 
{ 
  int x = 0;
  … 
  return (x);      // decltype((x)) is int&, so f2 returns int& 
}

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

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

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

请记住:

?decltype一般情况下总是返回变量名或是表达式的类型而不会进行任何的修改。

?对于不同于变量名的左值表达式,decltype的结果总是T&。

?C++14提供了decltype(auto)的支持,比如auto,从它的初始化式中推导类型,但使用的是decltype的推导规则。

《Effective Modern C++》翻译--条款3: 理解decltype

标签:

原文地址:http://blog.csdn.net/wangshubo1989/article/details/50499610

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!