对于重载函数的参数类型声明的思考
前言
随着C++11的到来,要考虑重载函数的参数类型增多了:右值引用以支持移动,通用引用以支持完美转发等。
我们为了更高效的支持拷贝或移动,需要考虑支持左右值(我这里不细分纯右值和亡值)。
对于确定的类型
通用引用 + 显式实例化
通过通用引用和完美转发可以支持左右值:1
2
3
4template<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 | 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
2void f(T &&a) { f(a); /* a is lvalue here */ }
void f(T &a) { /* ... */ }
总结
对于移动开销小的,我或许会选择值参数,但根据场景往往会更倾向于通用引用+显式实例化。