7.2 Passing by Reference
7.2 按引用传递
Now let’s discuss the different flavors of passing by reference. In all cases, no copy gets created (because the parameter just refers to the passed argument). Also, passing the argument never decays. However, sometimes passing is not possible, and if passing is possible, there are cases in which the resulting type of the parameter may cause problems.
现在,我们来讨论按引用传递的不同表现。在任何情况下,按引用传递都不会创建副本(因为形参引用了实参)。而且,传入的参数类型也不会退化(decay)。但是有时是不能使用按引用传参的,即使在可以使用地方,有时被推导出来的模板参数类型也会带来不少问题。
7.2.1 Passing by Constant Reference
7.2.1 按const引用传递
To avoid any (unnecessary) copying, when passing nontemporary objects, we can use constant references. For example:
为了避免(不必要的)拷贝,当传递非临时对象时,可以使用const引用进行传参。例如:
template<typename T>
void printR (T const& arg) {
…
}
With this declaration, passing an object never creates a copy (whether it’s cheap or not):
这样的声明,传递对象时永远都不会创建副本(不管拷贝成本高或低)
std::string returnString();
std::string s = "hi";
printR(s); // 没发生拷贝
printR(std::string("hi")); //没发生拷贝
printR(returnString()); //没发生拷贝
printR(std::move(s)); //没发生拷贝
Even an int is passed by reference, which is a bit counterproductive but shouldn’t matter that much. Thus:
甚至当按引用传递int类型的值时也不会创建副本。为int类型传递引用可能会适得其反(译注:不会提高性能,见下段的解释),但问题不会很严重。因此:
int i = 42;
printR(i); // 传递引用,而不是i的副本
results in printR() being instantiated as:
导致printR()被实例化为:
void printR(int const& arg) {
…
}
Under the hood, passing an argument by reference is implemented by passing the address of the argument. Addresses are encoded compactly, and therefore transferring an address from the caller to the callee is efficient in itself. However, passing an address can create uncertainties for the compiler when it compiles the caller’s code: What is the callee doing with that address? In theory, the callee can change all the values that are “reachable” through that address. That means, that the compiler has to assume that all the values it may have cached (usually, in machine registers) are invalid after the call. Reloading all those values can be quite expensive.
在底层,按引用传递参数其实是通过传递参数的地址来实现的。内存地址短小紧凑,因此,将地址本身从调用者(caller)传到被调用者(callee)效率是很高的。但是,当编译器在编译调用者(caller)的代码时,传递地址可能给编译器带来不确定性:被调用者(callee)该如何处理这个地址呢?理论上,被调用者(callee)可以任意修改通过该地址所指向的内容。这意味着,编译器必须假定当这次调用之后,它可能缓存的所有值(通常是在机器的寄存器中)都是无效的。而重新加载这些值的代价很高(译注:可能比拷贝对象的成本高很多)。
You may be thinking that we are passing by constant reference: Cannot the compiler deduce from that that no change can happen? Unfortunately, that is not the case because the caller may modify the referenced object through its own, non-const reference.
你或许会想我们传递的是const引用:编译器不能由此推断出任何修改都不会发生吗?不幸的是,确实不能,因为调用者可以通过自己的非const引用来修改这个被引用的对象。
This bad news is moderated by inlining: If the compiler can expand the call inline, it can reason about the caller and the callee together and in many cases “see” that the address is not used for anything but passing the underlying value. Function templates are often very short and therefore likely candidates for inline expansion. However, if a template encapsulates a more complex algorithm, inlining is less likely to happen.
对于内联函数,情况可能会好一些:如果编译器可以展开inline函数,它就可以推断出调用者和被调用者是放在一起的,并且在许多情况下可以“看到”该地址除了传递底层值以外,并没有作为其他用途。函数模板通常是很短小的,因此很可能被内联展开。然而,如果模板封装了一个很复杂的算法,那么内联就很可能不会发生。
Passing by Reference Does Not Decay
传引用不会导致类型退化
When passing arguments to parameters by reference, they do not decay. This means that raw arrays are not converted to pointers and that qualifiers such as const and volatile are not removed. However, because the call parameter is declared as T const&, the template parameter T itself is not deduced as const. For example:
按引用传递参数时,不会发生类型退化(decay)。这意味着原生数组不会被转换为指针类型,也不会移除const和volatile限定符。但是,由于形参被声明为T const&,所以模板参数T本身不会包含const。例如:
template<typename T>
void printR(T const& arg) {
...
}
std::string const c = "hi";
printR(c); // T deduced as std::string, arg is std::string const&
printR("hi"); // T deduced as char[3], arg is char const(&)[3]
int arr[4];
printR(arr); // T deduced as int[4], arg is int const(&)[4]
Thus, local objects declared with type T in printR() are not constant.
因此,在print()函数中用类型T来声明局部变量时,其类型也不会包含const。
7.2.2 Passing by Nonconstant Reference
7.2.2 按非const引用传递
When you want to return values through passed arguments (i.e., when you want to use out or inout parameters), you have to use nonconstant references (unless you prefer to pass them via pointers). Again, this means that when passing the arguments, no copy gets created. The parameters of the called function template just get direct access to the passed argument.
Consider the following:
当需要通过传入参数返回值时(也就是说需要修改变量的值),就需要使用非const引用(除非更喜欢使用指针)。同样,这意味着在传递参数时,也不会创建副本。被调用函数模板的参数可直接访问被传递的参数。考虑以下情况:
template<typename T>
void outR (T& arg) {
…
}
Note that calling outR() for a temporary (prvalue) or an existing object passed with std::move() (xvalue) usually is not allowed:
注意,outR()函数通常不允许传入临时变量(prvalue)或者通过std::move()传递的对象(xvalue)。
std::string returnString();
std::string s = "hi";
outR(s); //OK: T被推导为 std::string, 形参arg类型为std::string&
outR(std::string("hi")); //ERROR:不允许传入临时对象(prvalue)
outR(returnString()); // ERROR: 不允许传入临时对象(prvalue)
outR(std::move(s)); // ERROR: 不允许传入xvalue
You can pass raw arrays of nonconstant types, which again don’t decay:
可以传入非const类型的原生数组,其类型也不会退化(decay):
int arr[4];
outR(arr); // OK: T 被推导为int[4], arg的类型为int(&)[4]
Thus, you can modify elements and, for example, deal with the size of the array. For example:
因此,可以修改其中的元素(如处理数组的大小)。例如:
template<typename T>
void outR (T& arg) {
if (std::is_array<T>::value) {
std::cout << "got array of " << std::extent<T>::value << " elems\n";//数组的大小
}
…
}
However, templates are a bit tricky here. If you pass a const argument, the deduction might result in arg becoming a declaration of a constant reference, which means that passing an rvalue is suddenly allowed, where an lvalue is expected:
但是,这个模板有点棘手。如果传递的是const实参,会导致arg被推导为const引用。这意味着突然允许接受右值(rvalue)类型,而实际上模板期待的参数类型却是左值(lvalue)。
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]
Of course, any attempt to modify the passed argument inside the function template is an error in such cases. Passing a const object is possible in the call expression itself, but when the function is fully instantiated (which may happen later in the compilation process) any attempt to modify the value will trigger an error (which, however, might happen deep inside the called template; see Section 9.4 on page 143).
当然,在这种情况下,任何试图在函数模板内部修改这个被传入的参数都会报错。在调用表达式中也可以传递一个const对象,但是当函数被完全实例化后(可能发生在接下来的编译过程中),任何试图修改参数值的行为都会触发一个错误(但这可能发生在调用模板的深层代码中。可参阅第143页的9.4节)
If you want to disable passing constant objects to nonconstant references, you can do the following:
如果希望禁止将const对象传递给非const引用,可以这样做:
• Use a static assertion to trigger a compile-time error:
使用static_assert触发一个编译期错误:
template<typename T>
void outR (T& arg) {
static_assert(!std::is_const<T>::value, "out parameter of foo<T>(T&) is const");
…
}
• Disable the template for this case either by using std::enable_if<> (see Section 6.3 on page 98):
使用std::enable_if<>禁用这种情况下的模板(请参阅第98页的6.3节)
template<typename T, typename = std::enable_if_t<!std::is_const<T>::value>
void outR (T& arg) {
…
}
or concepts once they are supported (see Section 6.5 on page 103 and Appendix E):
或者在concept被支持后,通过concept来禁用该模板(见第103页6.5节以及附录E):
template<typename T>
requires !std::is_const_v<T>
void outR (T& arg) {
…
}
7.2.3 Passing by Forwarding Reference
7.2.3 按转发引用传递
One reason to use call-by-reference is to be able to perfect forward a parameter (see Section 6.1 on page 91). But remember that when a forwarding reference is used, which is defined as an rvalue reference of a template parameter, special rules apply.Consider the following:
使用按引用调用的一个原因是可以对参数进行完美转发(见第91页的6.1节)。但是请记住在使用完美转发时,被定义成模板参数的右值引用,有它自己特殊的规则。考虑如下代码:
template<typename T>
void passR (T&& arg) { // arg 被声明为完美转发引用
…
}
You can pass everything to a forwarding reference and, as usual when passing by reference, no copy gets created:
可以将任意类型传递给转发引用,而且与平常按引用传递一样,都不会创建参数的副本:
std::string s = "hi";
passR(s); // OK: T 和arg都被推导为std::string&
passR(std::string("hi")); // OK: T推导为std::string, arg为std::string&&
passR(returnString()); // OK: T推导为std::string, arg为std::string&&
passR(std::move(s)); // OK: T推导为std::string, arg为std::string&&
passR(arr); // OK: T和arg都被推导为int(&)[4]
However, the special rules for type deduction may result in some surprises:
但是,类型推导的特殊规则也可能会产生意想不到的收获:
std::string const c = "hi";
passR(c); //OK: T被推导为std::string const&
passR("hi"); //OK: T和arg都被推导为 char const(&)[3]
int arr[4];
passR(arr); //OK: T和arg都被推导为int (&)[4]
In each of these cases, inside passR() the parameter arg has a type that “knows” whether we passed an rvalue (to use move semantics) or a constant/nonconstant lvalue. This is the only way to pass an argument, such that it can be used to distinguish behavior for all of these three cases.
在以上各种情况中,都可以在passR()函数的内部通过参数arg的类型,得知被传递的是右值rvalue(使用移动语义),还是一个const或者非const的左值(lvalue)。这是唯一一种可以只传递一个参数,就可以区别三种情况的方法。
This gives the impression that declaring a parameter as a forwarding reference is almost perfect. But beware, there is no free lunch.
这给人的印象是,将参数声明为转发引用几乎非常完美。但是,没有免费的午餐。
For example, this is the only case where the template parameter T implicitly can become a reference type. As a consequence, it might become an error to use T to declare a local object without initialization:
例如,这是唯一一种可以将模板参数T隐式推导为引用类型的情况。因此,当使用T声明一个局部对象而不进行初始化时,可能会出现一个错误。
template<typename T>
void passR(T&& arg) { // arg is a forwarding reference
T x; // 传入左值时,T为T&,则x是个引用,需要被初始化(因为引用必须被初始化)
…
}
foo(42); // OK: T 被推导为int
int i;
foo(i); // ERROR: T被推导为int&, 这使得在passR中x的声明是无效的(引用未被初始化!)
See Section 15.6.2 on page 279 for further details about how you can deal with this situation.
关于如何处理这一问题的更多细节,请参阅第279页的15.6.2节。