7 传值和传引用

从C++11开始,函数传引用参数分为下面三种类型:

  • X const&,常量左值引用
  • X&,非常量左值引用
  • X&&,右值引用,该值可以被修改,或者移动

一般下列情况需要传引用:

  • 对象不能拷贝
  • 需要通过参数返回值
  • 模板只是用来转发
  • 性能方面的考虑

7.1 传值

传值是一定会发生对象构造的,但是拷贝构造可能会被编译器优化掉,还有可能进行移动构造:

template<typename T>
void printV (T arg) {
    // ...
}

std::string returnString();
std::string s = "hi";
printV(s);                  // copy constructor
printV(std::string("hi"));  // copying usually optimized away (if not, move constructor)
printV(returnString());     // copying usually optimized away (if not, move constructor)
printV(std::move(s));       // move constructor

根据类型推导规则,传值会丢弃constvolatile,数组会退化为指针。

7.2 传引用

7.2.1 传常量引用

传常量引用能保证不会发生拷贝:

template<typename T>
void printR (T const& arg) {
    // ...
}

std::string returnString();
std::string s = "hi";
printR(s);                  // no copy
printR(std::string("hi"));  // no copy
printR(returnString());     // no copy
printR(std::move(s));       // no copy

引用的内部机制仍然是传地址,但是不能保证函数不会修改该常量,因为const只是编译期间的检查。根据类型推导规则,传引用不会丢弃constvolatile,传数组时类型信息会包括数组长度。

7.2.2 传非常量引用

不能将临时量作为传非常量引用的参数:

template<typename T>
void outR (T& arg) {
    // ...
}

std::string returnString();
std::string s = "hi";
outR(s);                    // OK: T deduced as std::string, arg is std::string&
outR(std::string("hi"));    // ERROR: not allowed to pass a temporary (prvalue)
outR(returnString());       // ERROR: not allowed to pass a temporary (prvalue)
outR(std::move(s));         // ERROR: not allowed to pass an xvalue

常量可以传给非常量引用,此时推导结果中会包含const,例子如下:

std::string const c = "hi";
outR(c);                    // OK: T deduced as std::string const
outR(returnConstString());  // OK: same if returnConstString() returns const string
outR(std::move(c));         // OK: T deduced as std::string const
outR("hi");                 // OK: T deduced as char const[3]

有两种方法可以禁止将常量传递给以非常量引用作为参数的函数:

  • 使用静态断言
template<typename T>
void outR (T& arg) {
    static_assert(!std::is_const<T>::value, "out parameter of foo<T>(T&) is const");
    // ...
}
  • 使用std::enable_if
template<typename T, typename = std::enable_if_t<!std::is_const<T>::value>
void outR (T& arg) {
    // ...
}

7.2.3 传转发引用

template<typename T>
void passR (T&& arg) { // arg declared as forwarding reference
    // ...
}

std::string s = "hi";
passR(s);                   // OK: T deduced as std::string& (also the type of arg)
passR(std::string("hi"));   // OK: T deduced as std::string, arg is std::string&&
passR(returnString());      // OK: T deduced as std::string, arg is std::string&&
passR(std::move(s));        // OK: T deduced as std::string, arg is std::string&&
passR(arr);                 // OK: T deduced as int(&)[4] (also the type of arg)

std::string const c = "hi";
passR(c);                   // OK: T deduced as std::string const&
passR("hi");                // OK: T deduced as char const(&)[3] (also the type of arg)
int arr[4];
passR("hi");                // OK: T deduced as int (&)[4] (also the type of arg)

转发引用是唯一使得类型T可以被推导为引用的写法,所以下面的代码就会因为引用没有初始化而报错:

template<typename T>
void passR(T&& arg) {   // arg is a forwarding reference
    T x;                // for passed lvalues, x is a reference, which requires an initializer
    // ...
}

foo(42);    // OK: T deduced as int
int i;
foo(i);     // ERROR: T deduced as int&, which makes the declaration of x in passR() invalid

7.3 std::ref和std:cref

从C++11开始,即使模板的调用参数是传值的,也可以使用std::refstd::cref来传引用:

// basics/cref.cpp
#include <functional> // for std::cref()
#include <string>
#include <iostream>

void printString(std::string const& s)
{
    std::cout << s << '\n';
}

template<typename T>
void printT (T arg)
{
    printString(arg);       // might convert arg back to std::string
}

int main()
{
    std::string s = "hello";
    printT(s);              // print s passed by value
    printT(std::cref(s));   // print s passed “as if by reference”
}

std::ref的实现方式是创建了一个新的std::reference_wrapper对象来包装传入的参数,并将新的对象以值得方式传入模板,同时提供了隐式类型转换来将std::reference_wrapper转换为原始参数的类型。

7.4 C风格字符串和数组的处理

7.4.1 定义专用的模板函数

C风格的字符串和数组在模板中处理起来非常麻烦,这是由传值和传引用时模板的推导规则决定的,所以最好的办法就是单独重载:

  • 为数组单独重载,更详细的例子见第5章
template<typename T, std::size_t L1, std::size_t L2>
void foo(T (&arg1)[L1], T (&arg2)[L2])
{
    T* pa = arg1; // decay arg1
    T* pb = arg2; // decay arg2
    if (compareArrays(pa, L1, pb, L2)) {
        // ...
    }
}
  • 使用std::enable_if
template<typename T, typename = std::enable_if_t<std::is_array_v<T>>>
void foo (T&& arg1, T&& arg2)
{
    // ...
}

7.5 返回引用和返回值

返回引用会带来一些麻烦,因为引用的对象是从函数里返回的,在函数外不受控制,但是还是有返回引用的应用的:

  • 返回容器中的元素
  • 使得类成员可写
  • 链式调用,例如输入输出运算符和赋值运算符
  • 返回只读类成员的常量引用

下面的代码都是错的(感觉retV<int&>太扯了):

template<typename T>
T retR(T&& p)       // p is a forwarding reference
{
    return T{...};  // OOPS: returns by reference when called for lvalues
}

template<typename T>
T retV(T p)         // Note: T might become a reference
{
    return T{...};  // OOPS: returns a reference if T is a reference
}

int x;
retV<int&>(x);      // retT() instantiated for T as int&

为了避免上述错误,可以使用std::remove_referencestd::decay去除类型推导结果中的引用,或者使用auto保证永远返回值。

7.6 推荐的模板参数声明方式

  1. 一般将模板的调用参数设置为传值的方式,如果对象很大,尝试使用std::refstd::cref进行包装
  2. 或者在下面的情况下可以传引用:
    • 如果参数需要修改,同时可以考虑禁掉推到类型为常量引用
    • 如果模板需要传递参数,考虑使用转发引用
    • 如果性能瓶颈确实是性能瓶颈,考虑使用常量引用

书中给出了std::make_pair从C++98到C++11的发展历史:

// C++98
template<typename T1, typename T2>
pair<T1,T2> make_pair (T1 const& a, T2 const& b)
{
    return pair<T1,T2>(a,b);
}
// C++03
template<typename T1, typename T2>
pair<T1,T2> make_pair (T1 a, T2 b)
{
    return pair<T1,T2>(a,b);
}
// C++11
template<typename T1, typename T2>
constexpr pair<typename decay<T1>::type, typename decay<T2>::type>
make_pair (T1&& a, T2&& b)
{
    return pair<typename decay<T1>::type,typename decay<T2>::type>(
        forward<T1>(a), forward<T2>(b));
}

7.7 总结

  1. 可以通过不同长度的C风格字符串来测试模板的正确性
  2. 传值会发生类型退化(decay),而传引用不会
  3. std::decay可以将引用类型还原为引用的类型
  4. 通过std::refstd::cref可以实现向传值的模板传引用
  5. 传值最大的问题在于拷贝带来的性能开销
  6. 除非知道在干嘛,否则还是应该传值
  7. 函数最好是返回值而不是返回引用
  8. 关于性能的问题最好测一下,不要去猜

我本以为引用是一个很好的语法,但是看了这一章后我觉得引用会带来很多的问题,而且正是这些问题使得C++变得更加复杂,所以我开始怀疑:引用到底是不是一个好的语法?

results matching ""

    No results matching ""