万能引用

  • 提到这个名词,在先前的描述中,我们可以知道的是,右值引用针对右值,左值引用适用于左值。但随着模版技术的引入,C++泛型能力的增强,我们如何接受一个尚未确定的引用类型呢
  • 其实,在一开始抛砖引玉,讲述右值引用前,我便给出了此种问题的一个答案,那便是采用常量的左值引用,由于const关键字可以延长将亡值的声明周期,故可以用来接收右值引用,但不可避免的是,右值引用由于const的修饰,无法按需更改。那么,万能引用应运而生
    1
    2
    3
    4
    5
    6
    7
    8
    void foo(int &&i) {} // i 为右值引用

    template <typename T>
    void bar(T &&t) {} // t 为万能引用

    int get_val() {return 5;}
    int &&x = get_val(); // 右值引用
    auto &&x = get_val(); // 万能引用
  • 聪明如你,可能已经注意到了万能引用和右值的引用的不同之处,尽管二者均采用了&&作为相应的符号,但当该类型能发生推导时,便成了万能引用。
  • 即推导过程如下,根据传入的引用类型,来返回不同的引用
  • 当然,能够实现这套流程,功不可没的一件事便是,采用了引用折叠
  • 规则如下
    • & + & -> &
    • & + && -> &
    • && + & -> &
    • && + && -> &&
  • 关键:凡是折叠中出现左值引用,优先将其折叠为左值引用

完美转发

  • 这是万能引用最为典型的一个用途

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <iostream>
    #include <string>

    template<class T>
    void show_type(T t)
    {
    std::cout << typeid(t).name() << std::endl;
    }

    template<class T>
    void normal_forwarding(T t)
    {
    show_type(t); // 传参时,会不可避免的发生一次临时值的复制
    }

    int main() {
    std::string s = "hello world";
    normal_forwarding(s);
    }
  • 你可能会自然而然的想到,通过左值引用来解决这次多余的复制。但有个糟糕的问题在于,当我们传入了一个右值时,编译就会报错。

  • 于是,完美转发便派上了用场

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    #include <iostream>
    #include <string>

    template<class T>
    void show_type(T t)
    {
    std::cout << typeid(t).name() << std::endl;
    }

    template<class T>
    void normal_forwarding(T t)
    {
    show_type(t); // 传参时,会不可避免的发生一次临时值的复制
    }

    template<class T>
    void perfect_forwarding(T&& t)
    {
    // show_type(static_cast<T &&>(t));
    show_type(std::forward<T>(t)); // 推荐
    }

    int main() {
    std::string s = "hello world";
    normal_forwarding(s);
    }
  • 辨析std::movestd::forward

    • std::move一定会转换为右值
    • std::forward视情况而定,且需要传入模版实参
  • 顺带一提,完美转发之所以存在,一个重要的意义在于,为了维持表达式左值与右值的各自的型别

    1
    2
    3
    4
    5
    6
    int tmp = 0
    int &a = &tmp;
    int &&b = &&tmp;

    void foo(int &a);
    void foo(int &&b); // 实际上,当传入参数b时,并不会触发这个函数的重载。其根本原因在于尽管存储的为右值,但对应的表达式仍为左值
  • 最后的最后,随着编译器技术的发展,我们在实际开发中可能会发现部分拷贝操作被隐式的替换为了移动操作

    • 一个最直观的答案便是,返回临时的局部变量