优先选择别名声明(alias declaration)而不是typedef
我很确信我们都同意使用STL容器是个好主意,并且我希望Item 18能够使你坚信使用std::unique_ptr也是一个好主意,但是我猜我们都不喜欢频繁写出类似于”std::unique_ptr<std::unpordered_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也提供了别名引用:
using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string,std::string>>;
考虑到typedef和别名声明做的事情一模一样,很自然就会想到优先使用一个是否有充足的技术原因。
的确有,但是在解释之前,我想说很多都发觉当处理涉及到函数指针的类型时,别名声明更加好理解:
// FP is a synonym for a pointer to a function taking an int and
// a const std::string& and returning nothing
typedef void (*FP)(int,const std::string&); //typedef
// same meaning as above
using FP = void (*)(int,const std::string&); // alias declaration
当然啦,哪种类型其实都不是那么容易理解,而且也很少有人花大量时间去处理函数指针类型的别名,所以这很难算是优先选择别名声明的一个充分理由。
但是充分的理由的确是存在的:模板。特殊地,别名声明可以被模板化(称之为别名模板),而typedef则不行。这就给C++11程序员一个直观的机制去表达事物,而这些事物在C++98里得和内嵌于结构体模板内的typedef结合起来才能解决。例如,考虑给一个使用定制分配器,MyAlloc,的一个链表类型定义一个别名。使用别名声明,小菜一碟:
template<typename T> // MyAllocList<T> is synonym for
using MyAllocList = std::list<T,MyAlloc<T>>;// std::list<T,MyAlloc<T>>
MyAllocList<Widget> lw; //client code
而使用typedef,你估计不得不从头做起:
template<typename T> //MyAllocList<T>::type is synonym for
struct MyAllocList{ // std::list<T,MyAlloc<T>>
typedef std::list<T,MyAlloc<T>> type;
}
MyAllocList<Widget>::type lw; //client code
情况变得更糟,如果你想在模板里面使用typedef定义的类型创建一个链表,链表包含的元素类型由模板参数指定,那么你必须得在该别名前面加上typename:
template<typename T>
class Widget{ //Widget<T> contains a
private: // MyAllocList<T>
typename MyAllocList<T>::type list; // as a data member
...
}
这里,MyAllocList<T>::type指的是一个依赖于模板类型参数(T)的类型。MyAllocList<T>::type因此是一个依赖类型(dependent type),C++众多招人喜欢的规则中有一条就是说依赖类型的前面必须加上typename。
如果MyAllocList是作为别名声明被定义的,就不需要typename了(还有那笨重的”::type”后缀):
template<typename T>
using MyAllocList = std::list<T,MyAlloc<T>>; //as before
template<typename T>
class Widget{
private:
MyAllocList<T> list; //no "typename", no "::type"
...
};
在你看来,MyAllocList<T>(也就是别名模板的使用)可能看起来和MyAllocList<T>::type(也就是内嵌typedef的使用)一样,都依赖于模板类型参数T,但是你又不是编译器。当编译器处理Widget模板并且遇到了MyAllocList<T>的使用(也就是别名模板的使用),它们知道MyAllocList<T>是类型的名字,因为MyAllocList是一个别名模板:它必须是一个类型的别名。MyAllocList<T>也因此是非依赖类型,typename修饰符既不需要也不允许。
另一方面,当编译器在Widget模板里看到MyAllocList<T>::type(也就是内嵌typedef的使用),它们是没法确保它是一个类型的别名,因为也许还存在它们目前还没发现的MyAllocList的全特化,在那里MyAllocList<T>::type指的是其他某种不是类型的东西。这听起来很疯狂,但是不要责备编译器考虑到这种可能性,而应该责备产生这种代码的我们人类。
例如,某些被误导的灵魂可能会撰写下面这种代码:
class Wine {...};
template<> //MyAllocList specialization
class MyAllocList<Wine>{ // for when T is Wine
private:
enum class WineType {White, Red, Rose}; //See Item 10 for info on "enum class"
WineType type; //in this class,type is a data member
...
}
正如你所见,MyAllocList<Wine>::type并不指一个类型。如果Widget要用Wine来实例化,那么Widget模板里的MyAllocList<T>::type就指一个数据成员,而不是类型。那么在Widget模板里面,MyAllocList<T>::type是否指一个类型就取决于T是什么。这也就是为什么编译器坚持让你在类型前面加上一个typename。
如果你接触过元模板编程,你几乎肯定遇到过这种情形,想基于模板参数类型,然后对其进行修改,得出自己想要的类型。例如,对于某种类型T,你想去除它包含的的const或者引用修饰符,例如,你想把const std::string&转换成std::string。或者你想给一个类型加上const或者把它转换成一个左值引用,比如,把Widget转换成const Widget或者Widget&。(如果你没接触过元模板编程,那太糟糕了,因为如果你想成为一名真正有效率的C++程序员,你至少需要对C++这方面的的基础有所了解。你可以在Item 23和Item 27里面看到模板元编程的例子,包括我刚刚提到的类型转换)
C++11以型别特性(type traits),一种包含在头文件<type_traits>中的模板,的形式向你提供了执行这种类型的转换的工具。在那个头文件里包含了许多型别特性,并不是所有的都执行类型转换,但是它们都提供了一个可预测的接口。对于类型T,你想要对其执行转换,结果类型是std::transformation<T>类型。例如:
std::remove_const<T>::type //yield T from const T
std::remove_reference<T>::type //yield T from T& and T&&
std::add_lvalue_reference<T>::type //yield T& from T
评论只是概括了这些转换做了啥,所以不必太较真。在将它们应用于工程上之前,你最好在看一下它们的准确定义。
毕竟,我这里的动机并不是给你们做一个关于型别特性的教程。注意这里使用这些转换的语句最后都有”::type”。如果你准备将它们应用于一个模板内部的类型参数(这基本上就是你在实际代码中如何应用它们的),你还得在每个使用到的地方前面加上typename。针对上面这两个语法减速带,其原因就是C++11的型别特性是以结构体模板里面的嵌套typedef实现的。那就对了,它们的实现使用了一种类型别名技术,而这种技术也正是我一直尝试说服你们要比别名模板要差的那个!
这里面是有历史原因的,但是我们打算跳过它(它很无聊,我保证),因为标准化委员会后来承认别名模板是一种更好的选择,并且他们在C++14中对所有C++11中的类型转换加入了该技术。别名有一个常见形式:对于每一个C++11的转换std::transformation<T>::type,有一个相对应的名叫std::transformation_t的C++14别名模板。例子会解释我所说的:
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;
看到了没?不能再简单了。
要点记忆
- typedef不支持模板化,但是别名声明支持
- 别名模板避免了”::type”后缀,并且,在模板中,typedef定义的别名类型需要在前面加上”typename”
- C++14对于C++11中所有的型别特性都加入了别名模板。