Effective Modern C++翻译(4)-条款3:了解decltype

时间:2022-04-27
本文章向大家介绍Effective Modern C++翻译(4)-条款3:了解decltype,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

条款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的推导规则。