用别名声明(alias declaration)代替typedef
我相信你像我一样觉得使用STL容器是个好主意,我希望在条款18中能让你相信使用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和别名声明的作用是一样,我们也有可靠的技术上的原因建议你使用别名声明。
在我们讲关于技术上的原因之前,我想说很多人觉得在处理函数指针时,使用别名声明比使用typedef简单:
// PF是一个参数为int和const std::string&,返回值为void的函数指针的同义词
typedef void (*PF)(int, const std::string &);
using PF = void (*)(int, const std::string &);
当然,这两种形式都是特别容易地被我们啃下,一些人还是需要时间来处理函数指针类型的同义词,所以说,这并不是强迫我们使用别名声明代替typedef的原因。
但强迫我们的原因还是存在的:模板。特别是别名声明可以实例化(这种情况叫做alias template),而typedef不可以。这给了C++11的开发者一种直截了当的方式来表达C++98必须把typedef嵌套在结构的事情。例如,试图定义一个链表的同义词,这个链表使用自定义的分配器MyAlloc。使用别名声明,这就是小case:
template <typename T>`
using MyAllocList = std::list<T, MyAlloc<T>>;
MyAllocList<Widget> lw; // 用户代码
使用typedef的话,就比较麻烦了:
template <typename T>
struct MyAllocList {
typedef std::list<T, MyAlloc<T>> type;
};
MyAllocList<Widget>::type lw; // 用户代码
这种方式不好。如果你想要在模板中使用这个typedef来创建一个链表,链表的对象是模板参数类型,那么你必须在typedef同义词之前使用typename:
template <typename T>
class Widget {
private:
typename MyAllocList<T>::type list;
...
};
这里的代码,MyAllocList<T>::type
指的类型依赖于模板类型的参数(T)。因此MyAllocList<T>
是一个依赖类型(dependent type),而C++一个受很多人喜欢的规则是,依赖类型之前必须紧挨着typename。
如果MyAllocList被定义为alias template(别名模板),就不需要typename啦:
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<T>
是个非依赖类型(non-dependent type),不需要也不允许关键字typename修饰。
另一方面,当编译器在Widget模板看到MyAllocList<T>::type
时,它不能确定这是不是一个类型的名字,因为可能有一个MyAllocList
的特例化完全没看到MyAllocList<T>::type
是一个类型。这听起来很疯狂,但你不能责怪编译器担心这个可能性,人们习惯写这种代码。
例如,一些误入歧途的灵魂可能在图谋着一些这样的事情:
class Wine {...};
template<> // 模板特例化
class MyAllocList<Wine> {
private:
enum class WineType // 关于枚举类,请看条款10
{ White, Red, Rose };
WineType type; // 在这个类中,type是一个成员变量
...
};
正如你所见,MyAllocList<Wine>::type
指代的不是类型。如果Widget用Wine实例化,那么Widget模板内的MyAllocList<T>::type
指代的是一个数据成员,而不是类型。然后在Widget模板内,MyAllocList<T>::type
是否指代一个类型,要依赖于参数T。这就是为什么编译器就要求你在它名字前加typename,这样做后编译器断言它是个类型,不用再等到模板实例化时才确定。
以下关于模板元编程方面的知识,由网友fesdobat翻译。
如果你做过模板元编程(templete meta programming, TMP),你肯定会遇到过要把模板类型转换为其他类型的需求。例如,对于类型T
,你可能想要去除T
含有的const 修饰或者引用修饰,具体来说,比如你可能会想把const string &
变成std::string
。或者你也有可能想把一个类型加上const修饰或者左值引用修饰,具体来说,比如把Widget
变成const Widget
或者Widget &
。(如果你还没有碰到过模板元编程,那可真是太糟糕了,因为如果你想成为真正高效的C++程序员,你就需要对c++的这一块至少有最基本的了解。你会在条款25和条款29中也看到TMP的例子,其中也包含了我在上面提及到的类型转换。)
C++11提供了实行这样转换的工具,他们叫做type traits,是一种变形的模板类,包含在头文件<type_traits>
中。这个头文件中有一系列这样的type traits,但并不是所有的都与处理类型转换相关,其中的一些提供了望文而知意的接口。假设你要转换的源类型为T
,你需要的转换结果就是std::transformation<T>::type
。例如:
std::remove_const<T>::type // yields T from const T
std::remove_reference<T>::type // yields T from T& and T&&
std::add_lvalue_reference<T>::type // yields T& from T
上面的注释只是简单说明代码做了什么。不要在意我的修辞手法。不过在把他们用到工程中之前,你一定去会仔细查阅他们的说明文档的,这我知道。
不过我在这里的原意也不是上一堂关于type traits的教学课。我请你注意到这些转换接口的末尾都带有“::type”的后缀。如果你用它们在模板内部作为参数传递(实际上你总是这样使用它们),你也不得不在每个前面加上一个“typename”。会出现这样像是马路两边的“路牙”的玩意,是因为c++11的type traits的实现,是基于模板化结构的内部嵌套typedef。没错,那就是我曾经极力向你推荐的类型变化技巧,现在他们输给了模板别名(alias template)!!(译注:即using关键字)
c++11这样做是有历史原因的,不过我们先跳过他们(说明很无趣,我保证)。长话短说,因为c++标准化委员会终于认识到模板别名是更好的实现方案,他们于是先在c++14里为这些c++11风格的类型转换做了一层包装。包装具有如下形式:对每个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 equivalent
std::remove_reference<T>::type // C++11: T&/T&& → T
std::remove_reference_t<T> // C++14 equivalent
std::add_lvalue_reference<T>::type // C++11: T →T&
std::add_lvalue_reference_t<T> // C++14 equivalent
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;
你瞧,简单得不能再简单了吧。
总结
需要记住的3点:
- typedef不支持模板化,但别名声明(alias declaration)支持。
- 别名声明(alias declaration)可以避免“::type”后缀,而且在模板中,typedef通常需求typename前缀。
- C++14 offers alias templates for all the C++11 type traits transformations.