Effective Modern C++: Item 9 -> 优先选择别名声明(alias declaration)而不是typedef

优先选择别名声明(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中所有的型别特性都加入了别名模板。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值