前言

随着C++11的到来,要考虑重载函数的参数类型增多了:右值引用以支持移动,通用引用以支持完美转发等。
我们为了更高效的支持拷贝或移动,需要考虑支持左右值(我这里不细分纯右值和亡值)。

对于确定的类型

通用引用 + 显式实例化

通过通用引用和完美转发可以支持左右值:

1
2
3
4
template<typename U>
void f(U &&value) {
auto s = std::forward<U>(value);
}

但这个比较麻烦的一点在于依赖于模板,也就是说U是个确定类型的话,你定义也得写在头文件中。
为了避免这一点,通过显式实例化可以分离实现到源文件中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// In a.h
struct A {
template<typename T>
void f(T &&v);
}

// In a.cpp
template<typename T>
void A::f(T &&v) {
auto val = std::forward<T>(v);
}

template void A::f<B>(B&&); // accept rvalue
template void A::f<B&>(B&); // accept lvalue

左右值重载

1
2
void f(T &&);
void f(T const&); // void f(T &);

避免通用引用的局限性,通过显式地重载两种值类别可以避免引入模板。
但比较冗余。

值参数

在《Effective Modern C++》 Item 41中提到了用值参数传递可拷贝对象,前提是该对象的移动操作足够廉价以及经常被拷贝。
这个前提条件很关键:

参数类型 左值 右值
T const& copy constructor + copy
T copy + move move + move
T&& copy move

显然,完美转发的开销最小。
T const&无法正确地处理右值,除非不打算支持移动或者该参数不可能移动,在C++11通常采用这种类型来接受左右值。
而值参数相比完美转发只是多了一次移动。因此如果移动开销足够小确实是可以接受的。
但是如果该参数很少被拷贝(即更偏向移动),那么对移动造成的多一次的移动开销反而是很多余的,所以会有两个前提条件。

对于泛型类型

那么只能使用通用引用和完美转发来实现。
典型例子就是STL容器的emplace()系方法。

接受左右值但不需要拷贝或赋值

另外对于接受左右值但是不需要拷贝或移动的函数而言,只需要转发给左值重载即可:

1
2
void f(T &&a) { f(a); /* a is lvalue here */ }
void f(T &a) { /* ... */ }

总结

对于移动开销小的,我或许会选择值参数,但根据场景往往会更倾向于通用引用+显式实例化