C++Item 27: 精晓什么日期接纳重载,什么时择universal引用

本文翻译由《effective modern
C++》,由于水平有限,故不可以保证翻译完全正确,欢迎提议错误。谢谢!

博客已经搬到这里啦

Item
26曾经讲了,不管是指向全局函数仍然成员函数(尤其是构造函数)而言,对universal引用的重载会招致同多级的问题。到目前停止,我呢早就于有了某些独例子,假若其会呈现得及大家期待的同一,这种重载也可以大实用。此Item会探索咋样让这种重载能兑现我们所要求的一言一行。大家得计划有制止对universal引用举行重载的实现,也堪由此限制参数的种,来使其会兼容。

俺们的议论将持续建在Item
26介绍的例子上。假如您目前没读了特别Item,你用在延续这Item前复习一下她。

丢掉重载

Item
26面临之率先个例子(logAndAdd)就是一个卓越的例证,很多如此的函数假设想要避对universal引用举行重载,这要简单地指向将要重载的函数进行不同的命名即可。举个例子,三只logAndAdd重载能叫划分成logAndAddName和logAndAddNameIdx。可惜的凡,那一个法无可以以其次个例(Person构造函数)中行事,因为构造函数的名是出于语言固定的。再说了,何人而想放任重载呢?

通过const T&传参数

外一个挑选是回C++98,并且将pass-by-universal-reference(通过universal引用传参数)替换成pass-by-lvalue-reference-to-const(通过const左值引用传参数)。事实上,这是Item
26考虑的首先个格局(显示在175页)。这么些法子之短处是她的效能不可以上最好地道。要明了,对于我们明天所了解之universal引用和重载来说,牺牲局部频率来保持业务的简单性可能是一个异常有吸重力的方案。

传值

一个通常能被你升官效用又不搭复杂性的道是管染引用的参数替换成传值的参数。即便就可怜不直观,但那多少个计划遵守了Item
41的指出(当知道您得拷贝一个目的时,直接通过传值来传递它)。所以,对于其怎么工作同它发出多快之底细部分,我会推迟至Item
41还谈谈。在及时,我只是为您看一下者技能怎么用在Person例子中失去:

class Person {
public:
    explicit Person(std::string n)  // 替换T&&构造函数对于
    : name(std::move(n)) {}         // std::move的使用请看Item 41

    explicit Person(int idx)        // 和之前一样    
    : name(nameFromIdx(idx)) {}
    ...

private:
    std::string name;
};

因std::string的构造函数接受类型为整型的参数,所以具有污染为Person构造函数的int及类似int(比如,std::size_t,
short,
long)的参数讲调用int版本的重载。相似之,所有的std::string类型(以及这些可以为此来成立一个std::string的参数,比如字符串”鲁思(Ruth)”)会吃传染被以std::string为参数的构造函数。由此对调用者来说,这里没意外暴发。你会争持说“我道小人尚是碰头深感意外,他们使用0或NULL来表示null指针,所以就会丢于是int版本的重载”,可是这么些口应有归Item
8,然后再度念五遍,直到他们看使用0或NULL来表示null指针会于他俩当可怕。

动Tag dispatch(标签分发)

任是通过lvalue-reference-to-const传递依旧传值的章程来辅助周转发。即便运用universal引用的心劲是两全转发的话,大家并未此外的挑三拣四。我们尚是休思废弃重载。所以一旦我们不惦记遗弃重载,也非思丢弃universal引用的话,大家怎么才能够防止对universal引用进行重载呢?

实则远非如此困难。重载函数的调用是这般的:依次查看每个重载函数的参数(形参)以及调用点的参数(实参),然后择最为匹配的重载函数(匹配上存有的形参和实参)。一个universal引用参数平日提供一个挺的很是,使得不管传入的凡呀,都能配合配上,不过若universal引用只是参数列表的平有,这几个参数列表还富含其他不是universal引用的参数,那么,即便不考虑universal引用,非universal引用参数就够用我们造成不配合配了。这就是tag
dispatch方法背后的根底,一个事例会被从前的叙说更加好领悟。

俺们把tag
dispatch永以logAndAdd177页的事例上去。为了制止你麻烦去摸,那里吃起异常例子的代码:

std::multiset<std::string> names;                   // 全局数据结构

template<typename T>                                // 创建log的实体并把它加 
void logAndAdd(T&& name)                            // 到全局的数据结构中去
{
    auto now = std::chrono::system_clock::now();
    log(now, "logAndAdd");
    names.emplace(std::forward<T>(name));
}

假如仅拘留它自己,这多少个函数工作得可怜好,可是当大家投入以int(用来因而索引查找对象)类型也参数的重载函数时,我们尽管回了Item
26所遇的题材。这些Item的目标是避这题目。比起添加一个重载,大家更实现logAndAdd,让它们当作其他少单函数(一个为整型类型,一个以外连串)的代办。logAndAdd它自己拿同时接受整型和非整型类型的有着参数。

诚做事情的一定量独函数将于命名为logAndAddImpl,也就是我们将重载它们。其中一个以因universal引用为参数,所以我们用以拥有重载和universal引用。不过,每个函数也拿携第二独参数,一个据此来指示传入的参数是未是整型的参数。这第二只参数将防我们落入Item
26所讲述的牢笼被去,因为我们将受第二个参数成为决定什么人重载将被挑的要素。

然,我知,“废话少说,让自家看代码!”,没问题。这里让起改进后的logAndAdd,这是一个几是的版:

template<typename T>
void logAndAdd(T&& name)
{
    logAndAddImpl(std::forward<T>(name),
                  std::is_integral<T>());   // 不是很正确
}

是函数把其的参数转发让logAndAddImpl,不过它们也传了一个参数来指明第一单参数的品类(T)是未是一个整型。至少,那是我们要要到位的。对于是右值类型的整型参数,它为成就了该做的政工。可是,就像Item
28解释的这样,如若一个左值参数为染于name(universal引用),T的种将为演绎为左值引用。所以若int类型的左值被传出logAndAdd,T将于演绎为int&。它不是int类型,因为引用不是整型。那表示,对于此外左值类型,即使参数真的是一个整型,std::is_integral

认识问题之过程就是非凡给当化解问题了,因为好之C++标准库已经来type
trait(看Item
9)了,std::remove_reference既然做了她的名字假如召开的事务,也开了我们所要之事情:把一个种的援性被错过丢。因而logAndAdd的科学写法是:

template<typename T>
void logAndAdd(T&& name)
{
    logAndAddImpl(
        std::forward<T>(name),
        std::is_integral<typename std::remove_reference<T>::type()
    );
}

此间尚出个小技巧(在C++14挨,你可以通过动std::remove_reference_t<T>倘使丢失打几独字,详细内容雅观Item
9)

拍卖完这一个household,大家会拿咱的注意力转移至函数在为调用的时了,就是logAndAddImpl。这里暴发半点只重载,第一独重载只可以用在非整型变量上(也便是std::is_integral<typename std::remove_reference<T>::type>会返回false的类型):

template<typename T>
void logAndAddImpl(T&& name, std::false_type)       // 非整型参数:把它添加
{                                                   // 到全局的数据结构中去
    auto now = std::chrono::system_clock::now();
    log(now, "logAndAdd");
    names.emplace(std::forward<T>(name));
}

若您精晓了藏匿于std::false_type悄悄的法则,这样的代码就显示甚直白了。概念上讲logAndAdd传了一个布尔值给logAndAddImpl,用那布尔值来注脚传为logAndAdd的类是未是一个整型,可是true和false是运行期的价值,而我辈得负重载决议(这是一个编译期的场景)来摘取是的logAndAddImpl。这意味着大家用一个和true相一致的体系及此外一个同false相一致的品类。这样的要求充足大,由此标准库为咱提供了std::true_typestd::false_type。通过logAndAdd传被logAndAddImpl的参数是一个目的,即便T是整型的话,这些目标就是延续自std::true_type,否则是目的就是此起彼伏自std::false_type。最终大家赢得的结果就是,当调用logAndAdd时,只有当T不是整型时,我们兑现之斯logAndAddImpl重载才是重载决议的候选对象。

次只重载则盖了反倒的图景:当T是一个整型时。在这种状态下,logAndAddImpl简单地找到呼应下标下的name,然后将name传回给logAndAdd:
std::string nameFromIdx(int idx); // 和Item 26中一样
void logAndAddImpl(int idx, std::true_type) // 整型参数:查找name,
{ // 并且为此来调用logAndAdd
logAndAdd(nameFromIdx(idx));
}

透过给logAndAddImpl查找相应的name,并且以这传被logAndAdd(它以为std::forward给此外一个logAndAddImpl重载),大家避免了拿log的代码同时在五个logAndAddImpl重载着。

每当这种设计下,std::true_type类型和std::false_type系列为誉为标签,它的目标只是强制重载决议的结果变成我们惦念使的结果。注意,我们仍旧都无为那些参数命名。它们在运行期没有举办任何事情,并且实际大家要编译器会将标签参数视为无效参数,并且用它们由程序的执行画面中优化掉(有的编译器会如此做,至少有时分会这样做)。在logAndAdd里面,对受重载函数的调用中,通过创建适合的签对象来“分发”工作被正确的重载。因而这种规划之名称为“标签分发”。它是模板元编程的根本,并且,你看的当代C++库的代码越多,你不怕越可能遭遇她。

便我们的目标而言,标签分发是怎落实的连无是大重点,它而我们在未起Item
26所讲述的问题从前提下,将universal引用和重载结合起来了,这才是但是根本之。分发函数(longAndAdd)以一个不被限制的universal引用为参数,可是这么些函数不让重载。底层的贯彻函数(logAndAddImpl)会为重载,并且也盖universal引用为参数,不过它还带动一个即使签参数,并且标签值被设计改为不晤面生出超一个底重载会成为候选匹配。这样一来,它的签就是决定了哪个重载会叫调用。所以universal引用总是对其的参数有确切匹配的实况就未根本了。

于带universal引用参数的沙盘举办封锁

签分发的关键点是召开也客户API的单个(不重载的)函数。这一个函数把要形成的做事分发给落实函数。创造一个不重载的散发函数平日十分简单,不过虽然如Item
26丁考虑的亚独问题同样,对Person类(在178页)的构造函数举行到转发就是是一个不同。编译器可能会面生拷贝和move构造函数,所以,即使你偏偏写了一个构造函数,并针对性它们接纳标签转发,一些针对性构造函数的调用可能会面绕开标签分发系统,被编译器所爆发的函数处理。

实则,真正的问题无是编译器发生的函数有时候会绕开标签分发,而是她从不吃传染过去。你几总是想要拷贝构造的拍卖能一气呵成拷贝一客传来参数的左值,可是就使Item
26叙述的这样,提供一个缘universal引用为参数的构造函数会让,当拷贝一个非const左值时,universal引用版本的构造函数(而休是拷贝构造函数)会为调用。这些Item同时为解释了当一个基类讲明了一个周全转发构造函数时,假诺其的派生类为风俗形式(将参数传给基类)实现了祥和拷贝或move构造函数,即使对的所作所为应是调用基类的正片或move构造函数,最终的结果为或调用完美转发构造函数。

对这多少个情况,带universal引用参数的函数比你想象得进一步贪婪,但是如若做啊一个单分发函数也不够贪婪(译注:因为分发函数需要经受所有类型的参数,可是我们的函数不包拷贝构造函数和move构造函数),因而签分发不是你即使寻找的体制。你要一个不同之技能,那些技术能叫你分以下的状态:做也函数模板的平等局部,universal引用是否受允许用。我之爱人啊,你需要的凡std::enable_if

std::enable_if深受您能强制编译器表现得近乎有些异样的沙盘不在一样。那样的沙盘被称为无效的。日常意况下,所有的模板都是中的,可是下了std::enable_if继,只有满意std::enable_if克标准的模版才是实惠之。在我们的情形下,对于Person构造函数,我们唯有想叫吃流传的参数类型不是Person时展开宏观转发。虽然传入的类型是Person,大家怀念使让到转发构造函数失效(也就是深受编译器忽略她),因为就会使得,当我们回忆就此任何Person对象起头化一个Person对象时,类的正片和move构造函数能处理这个调用,

想使表明是想法不是非常困难,不过大家可非知底具体语法,尤其是您前边从未见了的语句,所以我会简单地奔而介绍一下。std::enable_if的格有还未是可怜醒目,所以大家会晤起它起。在大家受起底Person中出一个到家转发构造函数的宣示,和例子一样,std::enable_if因此起来特别简单。我光叫您显示了这些构造函数的扬言,因为std::enable_if当函数的实现着莫打算。实现依然跟Item
26丁的贯彻均等。

calss Person {
public:
    template<typename T,
             typename = typename std::enable_if<condition>::type>
    explicit Person(T&& n);
    ...
};

为知道(typename = typename std::enable_if

俺们惦记只要简明的格是T不是Person,也就是说,唯有当T是除了Person以外的路时,模板化的构造函数才是有效的。多亏了type
trait(std::is_same),我们能断定七只系列是否同样,看起,咱们想念使的极是!std::is_same<Person, T>::value。(注意,表达式最前方的”!”。我们怀恋假使之是Person和T是不同之)这与我们思量使的不行类似了,不过还稍语无伦次,因为,就比如Item
28讲的这样,用左值初步化时,对universal引用的序列推导总是一个左值引用。这代表像下这样的代码,

Person p("Nancy");

auto cloneOfP(p);       // 从左值初始化

于universal构造函数中,类型T将于演绎成Person&。类型Person和Person&不一致,因而std::is_same的结果以反馈以下的谜底:std::is_same<Person, Person&>::value是false

假使我们考虑地重新准一些,我们相会意识及,当我们当游说“Person的模板化构造函数只有在T不是Person时才有效”时,对于T,我们会面惦念要不经意:

  • 它们是否是一个引用。对于决定universal引用构造函数是否是可行之,Person,Person
    &,以及Person&&都应有跟Person相同。
  • 她是否是const或volatile的。就大家而言,一个const
    Person和一个volatile Person以及一个const volatile
    Person和一个Person都是如出一辙的。

立代表,在检查T和Person是否同样在此之前,我们要一个艺术来去除T的援,const,volatile属性。标准库再同赖用type
trait的模式被了我们我们所需要的物。这一次的trait是std::decay(decay是滞后的意思)。除了引用和CV限定符(也就算是const或volatile限定符)被移除以外,std::decay

!std::is_same<Person, typename std::decay<T>::type>::value

也即便是,在不经意引用或CV限定符意况下,Person和类型T不雷同。(就设Item
9解释的这样,std::decay前边的“typename”是得的,因为std::decay

用规范插入前边std::enable_if不醒目的组成部分,并且格式化一下,让结果的构造还清,于是对Person的完美转发构造函数就闹了这般的声明式:

class Person {
public:
    template<
        typename T,
        typename = typename std::enable_if<
                     !std::is_same<Person,
                                   typename std::decay<T>::type
                                  >::value
                    >::type
    >
    explicit Person(T&& n);

    ...

};

只要您向不曾看了点这样的代码,感谢主。我将这种计划保留到结尾是起一个原因的。当您能应用其余的编制来避免universal引用和重载的混合时(你几总是能这么做),你应有制止这样做。不过要你无独有偶了效能性的语法以及大量之尖括号,其实也未算是大不佳。其它,这被了你直接追求的作为。下边给出之讲明式,从此外一个Person(不管是左值依旧右值,const如故非const,volatile依旧非volatile)构造Person时,永远都不会合调用以universal引用为参数的构造函数。

遂了,对吧?我们就了!

哦,不。先不要急着庆祝。大家还没解决Item
26碰着最终领取的一点。我们要解决其。

如果一个类从Person继承,并因此传统的法门贯彻了拷贝和move构造函数:

class SpecialPerson: public Person {
public:
    SpecialPerson(const SpecialPerson& rhs)     // 拷贝构造函数,调用
    : Person(rhs)                               // 基类的转发构造函数
    { ... }

    SpecialPerson(SpecialPerson&& rhs)          // move构造函数,调用
    : Person(std::move(rhs))                    // 基类的转发构造函数
    { ... }

    ...
};

连注释,这段代码和Item
26(在206页)给闹之代码一模一样,它还需大家调整。当大家拷贝或move一个SpecialPerson对象时,我们想只要以基类的正片或move构造函数,来拷贝或move它的基类部分,然而于这么些函数中,我们传入了一个SpecialPerson对象为基类对象,并且为SpecialPerson和Person的花色不均等(就算以运std::decay之后呢不相同),所以基类中之universal引用构造函数是行之有效的,并且其充裕乐意实例化出一个克对SpecialPerson参数精确匹配的函数。这样的可靠匹配比由于派生类及基类的转移(要拿SpecialPerson对象绑定到Person的正片和move构造函数上之Person参数时,这种转移时务必的)更合适,所以于我们本具有的代码,move和拷贝SpecialPerson对象将调用Person的统筹兼顾转发构造函数来拷贝或move它们的基类部分!又平等次等回了Item
26备受的题材。

派生类在落实拷贝和move构造函数的早晚只是以了一般性的条条框框,所以若缓解是题材,必须把目光集中在基类中,尤其是判定Person的universal引用是否中之原则判断及。现在我们领悟,在模板化的构造函数中,大家无是思念叫除了Person类型以外的参数有效,而是想让除了Person以及由Person继承的花色以外的参数有效。讨人厌的延续!

今昔听到“标准type
traits中出一个traits能判断一个路是否从任何一个档继承”你当不相会感到惊奇了吧。它称作std::is_base_of。如果T2从T1那继承,那么std::is_base_of<T1, T2>::value啊true。类型自己于看是打友好继续的,所以std::is_base_of<T, T>::value呢true。这好有益于,因为大家惦念使修改大家的主宰规范,使得Person的两全转发构造函数满足以下标准:当T错过除其的援和CV限定符时,它即使Person,要么是于Person继承的。使用std::is_base_of来代替std::is_same即使是咱记念要之:

class Person {
public:
    template<
      typename T,
      typename = typename std::enable_if<
                   !std::is_base_of<Person,
                                    typename std::decay<T>::type>
                                    ::value
                  >::type
     >
     explicit Person(T&& n);

     ...
};

本大家算是形成了。我们提供的是C++11底代码。假使我们接纳C++14,代码同样可以干活,可是咱能使用别名template来防止讨厌的“typename”和“::type”,它们是std::enable_if_tstd::decay_t。由此会生如此越令人美观标代码:

class Person {                                      // C++14
public:
    template<typename T,
             typenmae = std::enable_if_t<           // 更少的代码
               !std::is_base_of<Person,
                                std::decay_t<T>     // 更少的代码
                                >::value
            >                                       // 更少的代码
    >
    explicit Person(T&& n);

    ...

};

好吧,我认可:我撒谎了。我们尚并未停止。不过大接近正确答案了。真的挺类似了!

咱既看罢怎么下std::enable_if来挑选给Person的universal引用构造函数的平部分的参数失效,使得这个参数能吃类似的正片和move构造函数调用,但是我们还尚无看罢怎么将该之所以当区分整型和非整型上。毕竟,这是大家最初的靶子;这么些构造函数的歧义问题不怕是我们一拖再拖的业务。

大家只要做的具备事情就是是:(1)添加一个Person构造函数的重载来处理整型参数,(2)进一步限制模板构造函数,使得其对整型参数失效。将我们研究了之事物尽数反倒入锅中,兼以缓慢火烘烤,然后便好痛快分享成功的芬芳了:

class Person {
public:
    template<
      typename T,
      typename = std::enable_if_t<
        !std::is_base_of<Person, std::decay_t<T>>::value
        &&
        !std::is_integral<std::remove_reference_t<T>>::value
      >
    >
    explicit Person(T&& n)      // std::string以及能被转换成
    : name(std::foward<T>(n))   // std::string的构造函数
    { ... }

    explicit Person(int idx)    // 整型的构造函数
    : name(nameFromIdx(idx))
    { ... }

    ...                         // 拷贝和move构造函数等

private:
    std::string name;
};

圈!多么美妙的事物!好吧,赏心悦目或者才是这一个模板元编程者宣称的,然则实际上,这么些办法不但做到了工作,它要拉动在独特的镇静来形成的。因为她以了健全转发,它提供了最高的频率,因为他操了universal引用和重载的咬合,而未是禁止它,那些技能可以叫用当重载是无法避免的事态下(比如构造函数)。

权衡

其一Item起首考虑的老二种技术(禁止重载,pass by const T&,pass by
value)明确了将调用的函数的每个参数类型。之后的片种技术(标签转发,限制模板的身份)使用了圆满转发,因而无引人注目参数的档次。这种从达的不同决策(是否明确项目)有着特别怪之震慑。

C++,召开也同样栽规则,完美转发逾快,因为其制止了单纯是以顺应表明式上之参数类型而创立临时对象,在Person构造函数的事例中,完美转发允许一个诸如”南希”一样的字符串被转接让std::string(Person中之name)的构造函数。而非使到转发的技能必须由字符串创制临时之std::string对象,这样才可以契合Person构造函数明确的参数类型。

然到转发有缺点。一个凡出头参数无法到转发,就算它会叫传染被以显明项目也参数的函数。Item
30会追究这个圆满转发失利的情形。

次独问题是当客户传入一个未合法参数时,错误指示的可读性。举个例子,插足一个客户创建Person对象的早晚,传入了一个由char16_t结的字符串(那些类型在C++11惨遭叫介绍,它好为此来表示16bit之字符)来替char(std::string是出于它们结合的):

Person p(u"Konrad Zuse")    // "Konrad Zuse"由const char16_t
                            // 的类型组成

出于本Item初步提及的老三单主意来促成时,编译器将相会视可用之构造函数只因为int或std::string为参数,然后她就会师发一个仍旧多或者掉那多少个直接的一无是处提醒,那一个错误指示会分演说无法从const char16_t[12]转换为int或std::string

可是用基于完美转发的不二法门来落实时,const char16_t数组会在并未指示的情下给绑定到构造函数的参数上。然后它会师给转接到Person的std::string数据成员的构造函数上去,只有以这些时刻,传入的调用者(一个const char16_t)与需要的参数(任何std::string构造函数能承受之参数)之间未配合配才汇合给发现。最终之荒谬提醒很可能是转的、“感人的”。我下的一个编译器报了超160举行的错误。

于这事例中,universal引用只叫转化了同赖(从Person的构造函数到std::string的构造函数),可是以还扑朔迷离的网面临,universal引用在到达最后决定参数类型是否可领时,很可能早就转化好几不善了。universal引用转发的次数更是多,当有错误时,错误提醒就会越令人困惑。很多开发者发现这题材虽会开为丰盛的理,让大家从来无错过用以universal引用为参数的接口,唯有当效用是率先注重点时才去用其。

以Person的例证中,我们了解转发函数的universal引用参数应该是一个std::string的起初化列表,所以我们会接纳static_assert来认同她是不是符合要求。std::is_constructible的type
trait能在编译期判断一个类此外目的是否因此外不同品类(或者项目集合)的一个靶(或者目标集合)构造出,所以断言很同意写:

class Person {
public:
    template<
      typename T,
      typename = std::enable_if_t<
        !std::is_base_of<Person, std::decay_t<T>>::value
        &&
        !std::is_integral<std::remove_reference_t<T>>::value
      >
    >
    explicit Person(T&& n)      
    : name(std::foward<T>(n))   
    { ... }

    explicit Person(int idx)    
    : name(nameFromIdx(idx))
    { 
        // 断言std::string能否被T对象创建
        static_assert(
            std::is_constructible<std::string, T>::value,
            "Parameter n can't be used to construct a std::string"
        );
        ...                         // 普通的构造函数在这
    }

    ...                             //  剩下的Person构造函数(和之前一样)

};

这般做下,假诺客户试着用一个不可知协会std::string的参数来创建Person,错误音讯会是确定的。不幸的凡,在这么些事例中,static_assert举凡构造函数的相同片段,不过转发代码,是成员开头化列表的同部分(也虽然是转发先于断言)。在自我下的编译器中,只有当不平时的一无是处提醒(超越160行)出现之后,我们的static_assert来的完美只是读的左提醒才会油可是生。

            你如若记住的事
  • 将universal引用和重载结合起来的替代品有:用不同之函数名字,pass by
    lvalue-reference-to-const, pass by value,使用标签转发。
  • 通过std::enable_if来限制模板可以吃universal引用和重载一起工作,不过只有当编译器能使universal引用重载的时光才会说了算原则。
  • universal引用参数平常会带来效能及的晋级,但是它们常以可用性上有毛病。