条款九:优先考虑别名声明而非typedef
Item 9: Prefer alias declarations to typedef
我相信每个人都同意使用STL容器是个好主意,并且我希望Item18能说服你让你觉得使用std:unique_ptr
也是个好主意,但我猜没有人喜欢写上几次 std::unique_ptr<std::unordered_map<std::string, std::string>>
这样的类型,它可能会让你患上腕管综合征的风险大大增加。
避免上述医疗悲剧也很简单,引入typedef
即可:
typedef
std::unique_ptr<std::unordered_map<std::string, std::string>>
UPtrMapSS;
但typedef
是C++98的东西。虽然它可以在C++11中工作,但是C++11也提供了一个别名声明(alias declaration):
using UPtrMapSS =
std::unique_ptr<std::unordered_map<std::string, std::string>>;
由于这里给出的typedef
和别名声明做的都是完全一样的事情,我们有理由想知道会不会出于一些技术上的原因两者有一个更好。
这里,在说它们之前我想提醒一下很多人都发现当声明一个函数指针时别名声明更容易理解:
//FP是一个指向函数的指针的同义词,它指向的函数带有
//int和const std::string&形参,不返回任何东西
typedef void (*FP)(int, const std::string&); //typedef
//含义同上
using FP = void (*)(int, const std::string&); //别名声明
当然,两个结构都不是非常让人满意,没有人喜欢花大量的时间处理函数指针类型的别名(译注:指FP
),所以至少在这里,没有一个吸引人的理由让你觉得别名声明比typedef
好。
不过有一个地方使用别名声明吸引人的理由是存在的:模板。特别地,别名声明可以被模板化(这种情况下称为别名模板alias templates)但是typedef
不能。这使得C++11程序员可以很直接的表达一些C++98中只能把typedef
嵌套进模板化的struct
才能表达的东西。考虑一个链表的别名,链表使用自定义的内存分配器,MyAlloc
。使用别名模板,这真是太容易了:
template<typename T> //MyAllocList<T>是
using MyAllocList = std::list<T, MyAlloc<T>>; //std::list<T, MyAlloc<T>>
//的同义词
MyAllocList<Widget> lw; //用户代码
使用typedef
,你就只能从头开始:
template<typename T> //MyAllocList<T>是
struct MyAllocList { //std::list<T, MyAlloc<T>>
typedef std::list<T, MyAlloc<T>> type; //的同义词
};
MyAllocList<Widget>::type lw; //用户代码
更糟糕的是,如果你想使用在一个模板内使用typedef
声明一个链表对象,而这个对象又使用了模板形参,你就不得不在typedef
前面加上typename
:
template<typename T>
class Widget { //Widget<T>含有一个
private: //MyAllocLIst<T>对象
typename MyAllocList<T>::type list; //作为数据成员
…
};
这里MyAllocList<T>::type
使用了一个类型,这个类型依赖于模板参数T
。因此MyAllocList<T>::type
是一个依赖类型(dependent type),在C++很多讨人喜欢的规则中的一个提到必须要在依赖类型名前加上typename
。
如果使用别名声明定义一个MyAllocList
,就不需要使用typename
(同时省略麻烦的“::type
”后缀):
template<typename T>
using MyAllocList = std::list<T, MyAlloc<T>>; //同之前一样
template<typename T>
class Widget {
private:
MyAllocList<T> list; //没有“typename”
… //没有“::type”
};
对你来说,MyAllocList<T>
(使用了模板别名声明的版本)可能看起来和MyAllocList<T>::type
(使用typedef
的版本)一样都应该依赖模板参数T
,但是你不是编译器。当编译器处理Widget
模板时遇到MyAllocList<T>
(使用模板别名声明的版本),它们知道MyAllocList<T>
是一个类型名,因为MyAllocList
是一个别名模板:它一定是一个类型名。因此MyAllocList<T>
就是一个非依赖类型(non-dependent type),就不需要也不允许使用typename
修饰符。
当编译器在Widget
的模板中看到MyAllocList<T>::type
(使用typedef
的版本),它不能确定那是一个类型的名称。因为可能存在一个MyAllocList
的它们没见到的特化版本,那个版本的MyAllocList<T>::type
指代了一种不是类型的东西。那听起来很不可思议,但不要责备编译器穷尽考虑所有可能。因为人确实能写出这样的代码。
举个例子,一个误入歧途的人可能写出这样的代码:
class Wine { … };
template<> //当T是Wine
class MyAllocList<Wine> { //特化MyAllocList
private:
enum class WineType //参见Item10了解
{ White, Red, Rose }; //"enum class"
WineType type; //在这个类中,type是
… //一个数据成员!
};
就像你看到的,MyAllocList<Wine>::type
不是一个类型。如果Widget
使用Wine
实例化,在Widget
模板中的MyAllocList<Wine>::type
将会是一个数据成员,不是一个类型。在Widget
模板内,MyAllocList<T>::type
是否表示一个类型取决于T
是什么,这就是为什么编译器会坚持要求你在前面加上typename
。
如果你尝试过模板元编程(template metaprogramming,TMP), 你一定会碰到取模板类型参数然后基于它创建另一种类型的情况。举个例子,给一个类型T
,如果你想去掉T
的常量修饰和引用修饰(const
- or reference qualifiers),比如你想把const std::string&
变成std::string
。又或者你想给一个类型加上const
或变为左值引用,比如把Widget
变成const Widget
或Widget&
。(如果你没有用过模板元编程,太遗憾了,因为如果你真的想成为一个高效C++程序员,你需要至少熟悉C++在这方面的基本知识。你可以看看在Item23,27里的TMP的应用实例,包括我提到的类型转换)。
C++11在type traits(类型特性)中给了你一系列工具去实现类型转换,如果要使用这些模板请包含头文件<type_traits>
。里面有许许多多type traits,也不全是类型转换的工具,也包含一些可预测接口的工具。给一个你想施加转换的类型T
,结果类型就是std::
transformation<T>::type
,比如:
std::remove_const<T>::type //从const T中产出T
std::remove_reference<T>::type //从T&和T&&中产出T
std::add_lvalue_reference<T>::type //从T中产出T&
注释仅仅简单的总结了类型转换做了什么,所以不要太随便的使用。在你的项目使用它们之前,你最好看看它们的详细说明书。
尽管写了一些,但我这里不是想给你一个关于type traits使用的教程。注意类型转换尾部的::type
。如果你在一个模板内部将他们施加到类型形参上(实际代码中你也总是这么用),你也需要在它们前面加上typename
。至于为什么要这么做是因为这些C++11的type traits是通过在struct
内嵌套typedef
来实现的。是的,它们使用类型同义词(译注:根据上下文指的是使用typedef
的做法)技术实现,而正如我之前所说这比别名声明要差。
关于为什么这么实现是有历史原因的,但是我们跳过它(我认为太无聊了),因为标准委员会没有及时认识到别名声明是更好的选择,所以直到C++14它们才提供了使用别名声明的版本。这些别名声明有一个通用形式:对于C++11的类型转换std::
transformation<T>::type
在C++14中变成了std::
transformation_t
。举个例子或许更容易理解:
std::remove_const<T>::type //C++11: const T → T
std::remove_const_t<T> //C++14 等价形式
std::remove_reference<T>::type //C++11: T&/T&& → T
std::remove_reference_t<T> //C++14 等价形式
std::add_lvalue_reference<T>::type //C++11: T → T&
std::add_lvalue_reference_t<T> //C++14 等价形式
C++11的的形式在C++14中也有效,但是我不能理解为什么你要去用它们。就算你没办法使用C++14,使用别名模板也是小儿科。只需要C++11的语言特性,甚至每个小孩都能仿写,对吧?如果你有一份C++14标准,就更简单了,只需要复制粘贴:
template <class T>
using remove_const_t = typename remove_const<T>::type;
template <class T>
using remove_reference_t = typename remove_reference<T>::type;
template <class T>
using add_lvalue_reference_t =
typename add_lvalue_reference<T>::type;
看见了吧?不能再简单了。
请记住:
typedef
不支持模板化,但是别名声明支持。- 别名模板避免了使用“
::type
”后缀,而且在模板中使用typedef
还需要在前面加上typename
- C++14提供了C++11所有type traits转换的别名声明版本