万能引用 (Universal Reference)

C++ 中有左值引用和右值引用。左值引用可以绑定到左值,右值引用可以绑定到右值。那有没有一种引用,既可以绑定左值又可以绑定右值呢?有的,那就是我们这篇文章的主角,万能引用。万能引用在英文中称为 Universal Reference。它的形式为 T&&,并且这里的 T 是需要进行推导的类型。 如果读者感兴趣的话,可以看一下 Scott Mayers 的这篇博文。他讲得特别得详细,建议读者都看一下。本文基本上就是基于 Scott 的这篇博文进行了精简和整理。 万能引用在代码中大概是这样出现的: template <typename T> void foo(T &&x) {} auto &&x = ..; decltype(bar) &&x = ..; 万能引用中的型别推导 a) auto, template 在模板和 auto 型别推导形式下的万能引用,T 型别的左值被推导为 T&,而 T 型别的右值被推导为 T。 template <typename T> void foo(T &&x) {} int x = 1; foo(x); // T deduces to be int& foo(1); // T deduces to be int 上面的代码中使用了引用折叠的规则,对此尚不清楚的读者可以参考我的另一篇文章。 这条规则有一条隐藏的含义,那就是初始化值的引用类型在推导过程中被忽略了。不论初始化值是左值引用还是右值引用,或者是不是引用,都不影响推导的结果。非引用的左值,左值引用和右值引用都是左值,所以推导得到的型别 T 都是 T&;而只有右值会被推导为 T。 根据这条规则和引用折叠的规则,我们可以得到一条推论: 如果初始化值为左值,那么万能引用实例化为左值引用;如果初始化值为右值,那么万能引用实例化为右值引用。 int x = 1; auto &&y = x; // 因为 x 是左值,所以 y 的类型为 T& &&,折叠为 T& 即左值引用 auto &&p = y; // 同上,y 是左值 auto &&z = 1; 初始化值为右值,所以 z 的类型为 T &&,即右值引用 b) decltype 在 decltype 推导形式下的万能引用,初始化值的引用性得以保留。也就是说,如果初始化值为非引用左值,那么推导得到的型别为 T (而非 T&);如果初始化值为左值引用,那么推导得到的型别为 T&;如果初始化值为右值引用,那么推导得到的型别为 T&&。根据这条规则和引用折叠的规则,我们可以总结出这种形式下的万能引用的场景: ...

May 14, 2024

虚函数 (Virtual Function)

简介 虚函数是 C++ 中的一种成员函数,它的开头用关键字 “virtual” 进行修饰。虚函数是用于实现运行时多态的一种方式。 虚函数的简单例子 class Base { public: virtual void foo(); }; class Derived : public Base { public: void foo(); } 在以上例子中,Base 声明了一个虚函数 foo,它的派生类 Derived,重载了这个 foo 函数。在运行时,如果指针或者引用指向的将是 Base,那么调用的将是 Base::foo;如指针或者引用指向的是 Derived 对象,那么调用的将是 Derived::foo。 运行时多态 class Base { public: virtual void foo() { std::cout << "Base foo" << std::endl; }; }; class Derived : public Base { public: void foo() { std::cout << "Derived foo" << std::endl; }; }; int main() { Derived d; Base *b1 = &d; Base &b2 = d; b1->foo(); b2.foo(); return 0; } Output: Derived foo Derived foo 在上面的例子中,我们将 Base 类型的指针和引用指向 Derived 对象,在调用 foo 的时候,实际调用的是 Derived::foo,这就是运行时多态。当有函数的接口只接受基类对象的指针类型的时候,我们可以传入实际指向子类对象的指针,并且可以成功调用子类对象的成员函数。 ...

March 17, 2024

std::move

std::move 是移动语义中常见的一种标准库函数,它的作用是将一个左值转发为右值。如果读者了解完美转发(std::forward)的话,就会发现 std::move 其实是完美转发的一个子集。在一点在我之前的博客中也提到过。在 cppreference.com,对 std::move 下了如下的定义: std::move is used to indicate that an object t may be “moved from”, i.e. allowing the efficient transfer of resources from t to another object. In particular, std::move produces an xvalue expression that identifies its argument t. It is exactly equivalent to a static_cast to an rvalue reference type. std::move 的声明如下: template< class T > typename std::remove_reference<T>::type&& move( T&& t ) noexcept; std::move 的声明说明了它的输出的类型是 T&&,也就是 T 的右值引用。所以当 std::move 的结果被当作右值使用的时候,会激活右值引用形参的重载函数。我们看个例子: ...

March 13, 2024

完美转发 (std::forward)

完美转发(std::forward)是一个标准库函数。它的作用是什么呢?简而言之,它可以把左值当作左值或者右值转发出去,也可以把右值当作右值转发出去。这段像绕口令一样的描述不是我的原创,而是翻译自 cppreference.com: Forwards lvalues as either lvalues or as rvalues, depending on T. Forwards rvalues as rvalues and prohibits forwarding of rvalues as lvalues. 其目的,读者诸君应该也能大概猜到,凡是涉及到左值右值的,大多是为了实现移动语义。 逐条解释 我们先看一下标准库中对 std::forward 的声明。结合一下这段代码,我们可以尝试理解一下上面的那段绕口令。 template <class T> T&& forward(typename remove_reference<T>::type& arg) noexcept; template <class T> T&& forward(typename remove_reference<T>::type&& arg) noexcept; 1. 将左值转发为左值 #include <iostream> #include <utility> using namespace std; struct Foo { Foo() { std::cout << "Foo constructor." << std::endl; } }; struct Bar { Bar(Foo&) { std::cout << "Bar copy constructor." << std::endl; } Bar(Foo&&) { std::cout << "Bar move constructor." << std::endl; } }; int main() { Foo foo; auto b1 = Bar(std::forward<Foo&>(foo)); auto b2 = Bar(foo); } Output: Foo constructor. Bar copy constructor. Bar copy constructor. 注意我们调用 std::forward 的时候,实例化的模板参数是 Foo&,因为入参 foo 是左值,所以实例化的是 std::forward 的第一种声明。用 Foo& 替换 T 之后我们可以看到,std::forward 的返回类型为 Foo& &&,而根据引用折叠规则,这一类型实际为 Foo&,因此返回类型为 Foo&,也就是 Foo 的左值引用。所以,Bar 的构造函数看到的入参是一个 Foo& 类型的右值(我们直接把 std::forward 的结果传递给了 Bar 的构造函数),因此激活的是拥有 Foo& 形参的构造函数,也就是 “copy constructor”。可以看到这一调用的结果跟我们直接传递一个左值的 Foo 对象是一样的。因此,我们称在这种情况下,将左值转发为了左值。 ...

March 13, 2024

引用折叠 (Reference Collasping)

C++ 引用折叠可以说是 C++ 一系列高级模板编程技巧(例如万能引用,完美转发)的基础。C++ 中的左值和右值都可以有引用,分别为左值引用(T&)和右值引用(T&&)。左值引用和右值引用也都是类型,所以它们也可以有引用。对于如何解释引用的引用,C++ 有一套规定:除了右值引用的右值引用折叠为右值引用之外,其他对引用的引用均折叠为左值引用。 也就是说,总共四种情况,用下面的代码总结一下: // 左值引用的左值引用折叠为左值引用 T& & → T& // 左值引用的右值引用折叠为左值引用 T& && → T& // 右值引用的左值引用折叠为左值引用 T&& & → T& // 右值引用的右值引用折叠为右值引用 T&& && → T&& 一言以蔽之,只要沾了左值引用,就永远是左值引用。通过 Compiler Explorer 验证一下以上的论点。 一些反思 如果读者了解移动语义的话,那么引用折叠会使得我们回味一下移动语义并得到一些新的见解。参考以下的代码,我们重载了函数 foo,使得它可以接受右值的入参,这是很常见的一个场景。 void foo(int &x){ std::cout << "lvalue reference version called." << std::endl; } void foo(int &&x){ std::cout << "rvalue reference version called." << std::endl; } int main() { int x = 1; foo(x); // 调用了左值引用版本的构造函数 foo(1); // 调用了右值引用版本的构造函数 return 0; } 不过,如果我传入一个左值引用类型的右值呢,哪个重载版本会被调用呢? foo(static_cast<int&>(x)); // 传入的参数是左值引用类型的右值 如果你像我之前只根据左值或者右值进行判断的话,那就会认为右值引用的版本会被调用。但是实际上被调用的是左值引用的版本,也就是 foo(int &x) 这个版本。因为我们传入的参数的类型是 int&,而且是一个右值,那么实际上对应的形参应该是 foo(int& &&),而根据引用折叠的规则,这实际上就是 foo(int&) 这个函数。那么对于如下这种入参,调用的又会是哪个版本呢? ...

March 12, 2024