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
根据类型推导规则,传值会丢弃const
和volatile
,数组会退化为指针。
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
只是编译期间的检查。根据类型推导规则,传引用不会丢弃const
和volatile
,传数组时类型信息会包括数组长度。
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::ref
和std::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_reference
和std::decay
去除类型推导结果中的引用,或者使用auto
保证永远返回值。
7.6 推荐的模板参数声明方式
- 一般将模板的调用参数设置为传值的方式,如果对象很大,尝试使用
std::ref
和std::cref
进行包装 - 或者在下面的情况下可以传引用:
- 如果参数需要修改,同时可以考虑禁掉推到类型为常量引用
- 如果模板需要传递参数,考虑使用转发引用
- 如果性能瓶颈确实是性能瓶颈,考虑使用常量引用
书中给出了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 总结
- 可以通过不同长度的C风格字符串来测试模板的正确性
- 传值会发生类型退化(decay),而传引用不会
std::decay
可以将引用类型还原为引用的类型- 通过
std::ref
和std::cref
可以实现向传值的模板传引用 - 传值最大的问题在于拷贝带来的性能开销
- 除非知道在干嘛,否则还是应该传值
- 函数最好是返回值而不是返回引用
- 关于性能的问题最好测一下,不要去猜
我本以为引用是一个很好的语法,但是看了这一章后我觉得引用会带来很多的问题,而且正是这些问题使得C++变得更加复杂,所以我开始怀疑:引用到底是不是一个好的语法?