C++ Template-7 按值传递还是按引用传递

本章讨论了在模板中声明传递参数的几种方式,推荐通常情况下按值传递,并对按引用传递的特殊情况展开讨论。同时,还讨论了处理字符串常量和其他原始数组中存在的问题。

不是本系列第一篇文章,推荐阅读完前面的文章再看本文,以下是本系列文章目录。

1. 函数模板

2. 类模板

3. 非类型模板参数

4. 可变参数模板

5. 基础技术

6. 移动语意与 enable_if<>

7.按值传递还是按引用传递(本文)

7. 按值传递还是按引用传递

C++ 语言初期就提供了按值传递和按引用传递,但是如何选择传递方式并不好判断:一般对于 nontrivial 类型按引用传递开销会更小但是会更加复杂。C++11 添加了移动语意,因此我们现在有三种不同的方式按引用传递:

  1. X const&(常量左值引用)

    改参数引用了被传递的对象,并且该参数不能被更改。

  2. X&(非常量左值引用)

    改参数引用了被传递的对象,并且该参数可以被更改。

  3. X&&(右值引用)

    改参数通过移动语意引用了被传递的对象,并且该参数可以被修改或者被”窃取“。

根据已有的类型决定如何声明传递参数已经很复杂了。在模板中,类型是不知道的,因此选择合适的传递机制会更加困难。

然而,在 1.6.1节我们推荐在函数模板中按值传递参数,除非遇到以下特殊情况:

  • 对象不允许被拷贝。
  • 参数被用于返回数据。
  • 模板是为了转发参数到另外地方,并且保持原参数的所有性质。
  • 可以获得显著的性能提升。

本章讨论了在模板中声明传递参数的几种方式,推荐在通常情况下按值传递,并对按引用传递的特殊情况展开讨论。同时,还讨论了处理字符串常量和其他原始数组中存在的问题。

在阅读本章时,最好能够熟悉附录B(未写)中的术语(lvalue, rvalue, prvalue, xvalue, etc.)。

7.1 按值传递

当值传递参数时,原则上来说,每个参数都必须被拷贝。因此,每个参数都是传递参数的一个拷贝。对于类来说,对象产生拷贝是由拷贝构造函数来初始化的。

调用拷贝函数可能会有很大开销。但是,有一些方法可以避免按值传递中拷贝产生的巨大开销:实际上,通过移动语意,编译器在拷贝对象时会优化拷贝行为,即使对复杂的对象进行拷贝,产生的开销也很小。

比如下面这个按值传递参数的函数模板:

1
2
3
4
template<typename T>
void printV(T arg){
...
}

当对int类型调用该函数模板时,实例化后的代码是:

1
2
3
void printV(int arg){
...
}

参数arg是任何传递对象的拷贝,无论该对象是字符,常量还是函数的返回值。

如果对std::string类型调用该函数模板:

1
2
std::string s = "hi";
printV(s);

模板参数T会被实例化为std::string,因此我们得到:

1
2
3
void printV(std::string arg){
...
}

同样,当传递字符串时,arg变成了字符串的拷贝。此时,该拷贝是由 string 类的拷贝构造函数生成的,这可能是一个开销很大的操作,因为原则上来说这个拷贝操作会进行深拷贝,它需要创建它独自的内存去存储这些值。

但是,不是所有情况会调用拷贝构造函数。考虑下面这种情况:

1
2
3
4
5
6
std::string returnString();
std::string s = "hi";
printV(s); // copy constructor
printV(std::string("hi")); // coping usually optimized away(if not, move constructor)
printV(returnString()); // coping usually optimized away(if not, move constructor)
printV(std::move(s)); // move constructor

在第一次调用时我们传递的是 lvalue*,会使用拷贝构造函数。但是,在第二次和第三次调用中,当直接调用对于 *prvalue 的函数模板,编译器通常会优化传递参数因此并不会有拷贝构造函数的调用。注意,从 C++17 开始,改优化是被要求的,在 C++17 之前,编译器不会完全优化拷贝,至少会尝试使用移动语意去使拷贝更加廉价。在最后的调用中,当传递 xvalue,我们通过一个我们不再需要使用s的信号,强制调用了移动构造函数。

综上所述,调用声明为按值传递的函数 printV() 时,只有在我们传递 lvalue(对象在使用之前被创建,而且我们并没有使用 std::move() 去传递它)会产生较大开销。不幸的是,该情况是非常常见的。因为我们通常是先创建一个对象,再去传递这个对象(通过一些操作后)给其他函数。

按值传递会导致类型退化

对于按值传递,我们还必须提到这一特性:当通过按值传递的方式传递参数时,该参数的类型会退化。这意味的原始数组(raw array)会转化为指针,而且参数的 constvolatile 修饰符也会被移除(就如同用值初始化一个auto的对象一样):

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
void printV(T arg){
...
}
std::string const c = "hi";
printV(c); // c decays so that arg has type std::string

printV("hi"); // decays to pointer so that arg has type char const*

int arr[4];
printV(arr); // decays to pointer so that arg has type int*

因此,当我们传递字符串常量 "hi" 时,他的类型 char const[3] 退化为 char const* ,此时模板被实例化为:

1
2
3
void printV(char const* arg){
...
}

这一行为继承自C语言,既有优点也有缺点。通常它简化了传递字符串常量的处理,但是其缺陷是内部的 printV()不能够对指针和原始数组进行区分。我们会在 7.4 节对如何处理字符串常量和其他原始数组进行讨论。

7.2 按引用传递

现在,我们来讨论一下按引用传递参数有什么不同。无论哪种情况,按引用传递都不会有对象的拷贝生成(因为该参数是传递参数的引用)。而且,传递的参数不会退化。但是,在有些情况下,是不能够按引用传递的,即使可以使用按引用传递,参数的类型上也会出现一些问题。

7.2.1 按常量引用传递

为了避免任何(不必要)的拷贝,当传递一个非临时对象是,我们可以使用常量引用。例如:

1
2
3
4
template<typename T>
void printR(T const& arg){
...
}

通过这个声明,传递的参数不会产生拷贝(无论拷贝的开销大不大):

1
2
3
4
5
6
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

甚至通过引用传递int类型,虽然不能够提升性能,但也不会产生拷贝。

1
2
int i = 42;
printR(i); // passes reference instead of just copying i

会使得printR()被实例化为:

1
2
3
void printR(int const& arg){
...
}

在底层上,按引用传递参数是通过传递该参数的地址实现的。地址被简洁编码,因此地址在调用者和被调用者之间的传递非常高效。但是,当编译器编译调用者代码时,传递地址会产生一些困惑:被调用者会如何处理这个地址?理论上被调用者可以改变所有可以通过改地址得到的值。这意味着,编译器必须假设在调用后,所有被缓存(通常,在机器的寄存器中)的值都会无效。而重新载入这些值会产生极大的开销。你可能认为我们传递常引用时,编译器会推断不会有改变产生。不幸的是,这并不存在,因为调用者可以通过自己的非常引用去修改引用的对象。

对于 inline 函数,这一情况会好一些:如果编译器可以展开 inline 的调用,它就可以通过调用者和被调用者的信息去推断被传递的地址中的值是否改变。函数模板通常非常简短,通常可以被 inline 展开。但是,如果函数模板中有一些复杂的算法,那么它就大概率不会被 inline

按引用传递不会退化

按引用传递的参数不会退化,这意味着原始数组不会被转化为指针,而且 constvolatile 修饰符也不会被移除。但是,因为调用参数是被声明为 T const&,模板参数 T 本身不会被推导为 const 变量。例如:

1
2
3
4
5
6
7
8
9
10
11
12
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 in const(&)[4]

注意,在 printR() 中的 T 对象的类型并不是 const 类型。

7.2.2 按非常量引用传递

当你想要用传递的参数来返回变量时(i.e. 当你想要使用 out 或是 inout 参数时),你必须使用非常量引用(除非你更期望通过指针来传递)。同样的,这样可以使得传递参数时并不会有对象的拷贝产生。调用函数模板的参数直接引用了传递参数。

考虑下面这个例子:

1
2
3
4
template<typename T>
void outR(T& arg){
...
}

注意对临时变量(prvalue)或者通过 std::move() 传递的现有对象(xvalue)来调用 outR() 通常不被允许:

1
2
3
4
5
6
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

你可以修改传递的对象,比如修改 arraysize

1
2
3
4
5
6
7
template<typename T>
void outR(T& arg){
if (std::is_array<T>::value){
std::cout << "got array of " << std:extent<T>::value << " elems\n";
}
...
}

如果你传递的是一个 const 参数,arg 可能会被推导为常量引用,此时传递的是 rvalue,但是期望的是 lvalue:

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

当然,在该情况下任何对传递参数进行修改的操作都会引发错误。在函数调用语句中,传递const对象是可以的,但是如果该函数模板被全实例化后(可能会发生在接下来的编译过程中),任何修改值的操作会引发错误(这可能会发生在模板调用的深层逻辑上;参见 9.3 节(未写))。

如果你想要禁止像非常引用传递常量对象,你可以这么做:

1
2
3
4
5
6
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<>(参见 6.3 节):

1
2
3
4
5
template<typename T,
typename = std::enable_if_t<!std::is_const<T>::value>
void outR(T& arg){
...
}

如果编译器支持的话,也可以使用 concept(参见 6.5 节):

1
2
3
4
5
template<typename T>
requires !std::is_const_v<T>
void outR(T& arg){
...
}

7.2.3 按转发传递

使用通过引用调用函数的一个原因是能够完美转发参数(参见 6.1 节)。但是注意使用转发引用的时候,如果模板参数被定义为一个 rvalue 引用,就有一些特殊的规则。考虑下面的这个例子:

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

你可以传递任何类型的参数给转发引用,而且因为是按引用传递,并不会有参数的拷贝:

1
2
3
4
5
6
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 std::string, arg is std::string&&

但是,有一些特殊的类型推导:

1
2
3
4
5
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(arr); // OK: T deduced as int const(&)[4](also the type of arg);

在上面的几种情况下,从 passR() 中的内部参数 arg 的类型可以知道我们传递的参数是一个 rvalue(使用移动语意)或者是一个(非)常量 lvalue。这是唯一可以通过只传递一个参数就可以区分以上三种情况的方法。

看上去将参数声明为转发引用总是完美的。但是,天下没有免费的午餐。

比如,由于转发引用是唯一可以将模板参数 T 推导为引用类型的方法。因此,当使用T去声明一个未初始化的局部对象时可能会产生错误:

1
2
3
4
5
6
7
8
9
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
...
}

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

参见 15.6.2 节(未写)进一步了解如何处理这种情况。

7.3 std::ref()std::cref的使用

从 C++11 开始,你可以让调用者决定函数模板参数是否要按值或者引用传递。当模板被声明为按值传递参数时,调用者可以使用std::cref()std::ref()(定义在头文件<functional>中)来按引用传递参数。例如:

1
2
3
4
5
6
7
8
template<typename T>
void printT(T arg){
...
}

std::string s = "hello";
printT(s); // pass by value
printT(std::cref(s)); // pass s "as if by reference"

但是,注意 std::cref() 并不会改变模板中处理参数的方式。但是,它使用了一个 trick:它让传递的参数s的行为像一个引用一样。实际上,它包装(wrap)了一个 std::reference_warpper<> 类型的对象,这个对象引用了原始对象,并且可以按值传递这个对象。这个包装器(wrapper)只支持:隐形地类型转换为原始类型,返回原始对象。因此,如果你对传递的对象操作合法,你可以使用引用包装器。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#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::reference_wrapper<string const> 参数传递给 arg,它后面会转换回它潜在的类型 std::string

注意编译器必须要知道隐式转换为原类型这一操作是必要的。由于这个原因,std::ref()std::cref() 只会在你通过泛型代码将对象传递给非泛型函数时才会有预期的表现。例如,直接尝试输出具有泛型类型T的传递对象会导致错误,因为没有针对std::reference_warpper<>类型的输出运算符:

1
2
3
4
5
6
7
8
template<typename T>
void printV(T arg){
std::cout << arg << '\n';
}
...
std::string s = "hello";
printV(s); // OK
printV(std::cref(s)); // ERROR: no operator<< for reference wrapper defined

同样,下面的代码也会出现错误,因为你不能对 char const*std::string 类型的 reference wrapper 进行比较:

1
2
3
4
5
6
7
8
9
template<typename T1, typename T2>
bool isless(T1 arg1, T2 arg2)
{
return arg1 < arg2;
}
...
std::string s = "hello";
if (isless(std::cref(s), "world")) ... //ERROR
if (isless(std::cref(s), std::string("world"))) ... //ERROR

即使传递给 arg1arg2 同样的类型 T 也不会编译成功:

1
2
3
4
5
template<typename T>
bool isless(T arg1, T arg2)
{
return arg1 < arg2;
}

因为这时编译器推导 arg1arg2 类型 T 时会得到两个冲突的类型。

std::reference_wrapper<> 类的作用是将引用作为第一类对象(first class object),使得你可以通过函数模板来拷贝和按值传递它。你也可以将它用于类对象,比如,存储一个容器类型的引用。但是,无论如何,该类型最后总需要转换回它潜在的原类型。

7.4 处理字符串字面量常量与原始数组

到目前为止,我们已经讨论了几种对字符串字面量常量与原始数组使用模板产生的效果:

  • 按值传递会导致类型退化,即变为指向元素类型的指针。
  • 任何按引用传递的方法都不会导致类型退化,因此引用的参数还是指向数组的。

这两种方法各有利弊。当将数组退化成指针时,你不能区分该参数是元素的指针还是数组。当按引用传递时,处理字符串字面量常量时,类型不退化会产生一些问题(因为不同长度的字符串类型不同)。例如:

1
2
3
4
5
6
template<typename T>
void foo(T const& arg1, T const& arg2){
...
}

foo("hi", "guy"); //ERROR

foo("hi", "guy") 编译失败,是因为 "hi" 的类型是 char const[3],而 "guy" 的类型是 char const[4],但是模板要求他们必须要有相同的类型 T。只有当这两个字符串常量有相同的长度时,这个代码才能被正确编译。也正因为此,强烈建议在测试用例中使用不同的字符串常量。

通过声明按值传递参数的函数模板 foo() 可以正确编译:

1
2
3
4
5
6
template<typename T>
void foo(T arg1, T arg2){
...
}

foo("hi", "guy") // compiles, but...

但是,能够正确编译不代表不会出现问题。更糟糕的是,编译期的问题可能会成为运行期间的问题。考虑下面的代码,当我们用 operator== 比较传递的参数时:

1
2
3
4
5
6
7
8
9
template<typename T>
void foo(T arg1, T arg2)
{
if (arg1 == arg2){ // OOPS: compares addresses of passed arrays
...
}
}

foo("hi", "guy"); // compilies, but...

作为代码编写者,你必须知道要传递的字符指针其实是字符串。但是,可能你处理的就是一个字符指针,因为函数模板同样要处理已经类型退化的字符串常量变量(这个变量是来自另一个按值传递的函数调用或者是该变量被分配给一个 auto 对象)。

然而,在许多情况下,类型退化是很有帮助的,特别是检查两个对象(都通过函数参数传递,或者一个通过函数参数传递,另一个是期望的参数)是否是或者转化为了同一类型。一个典型的用法是完美转发。但是如果你想要完美转发的话,你必须将参数声明为转发引用。在这些这些情况下,你可能需要显式地使用 std::decay<>() 将参数进行类型退化。参见 7.6 节 std::make_pair() 这一例子。

7.4.1 字符串字面常量和原始数组的特殊实现

你可能需要根据传递的是指针还是数组来决定你的实现。当然,这个传递的数组没有类型退化。

为了区分这些情况,你需要查明是否有数组被传递。这有两个基础的方法:

  • 你可以定义函数模板,使它只有在参数为数组时合法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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)){
    ...
    }
    }

    这里,arg1arg2 必须是存储同一类型 T 元素的原始数组,但是它们的大小不一定要相同。另外,为了支持多种类型的原始数组你可能需要多种实现方法(参见 5.4 节)。

  • 你也可以通过 type traits 去侦测是不是有数组(或者指针)传递过来:

    1
    2
    3
    4
    5
    6
    template<typename T,
    typename = std::enable_if_t<std::is_array_v<T>>>
    void foo(T&& arg1, T&& arg2)
    {
    ...
    }

由于这些特殊的处理,通常最好的对数组进行不同处理的方式只是简单地使用不同的函数名称罢了。当然,如果调用者能使用 std::vectorstd::array 那会更加好。但是,只要字符串常量是原始数组,在编写程序时,总需要将它们考虑进来。

7.5 处理返回值

对于返回值,你也可以决定时按值返回还是按引用返回。但是,按应用返回可能会有一些潜在的问题,如果你引用了一些不在你控制范围的对象。这里是一些在编写代码中需要返回引用的例子:

  • 返回容器或者字符串中的元素(通过 operator[] 或者 front()
  • 授予对类成员的写权限
  • 返回链式调用的对象(对于 stream 的 operator<<oeprator>> 还有对于一般对象的 operator=

另外,通常对类成员授予读权限我们使用的是返回一个常引用。

注意,如果你使用不正确的话,即使符合这些情况下也会有一些错误。例如:

1
2
3
4
std::string* s = new std::string("whatever");
auto& c = (*s)[0];
s.reset();
std::cout << c; // run-time ERROR

这里,我们获得了 string 类型的引用,但是当我们使用这个引用的时候,这个潜在的 string 类型就不存在了(我们创建了一个摇摆不定的引用),因此我们得到了一个 undefined behavior。这个例子可能有点做作(有经验的程序员可能会立刻注意到其中的问题),但是实际上可能问题没有那么明显,例如:

1
2
3
4
auto s = std::make_shared<std::string>("whatever");
auto& c = (*s)[0];
s.reset();
std::cout << c; // run-time ERROR

因此我们呢必须确保函数模板按值返回。但是,本章讨论过,使用模板参数 T 并不意味着它不是个引用,因为 T 有时候会隐式地推导成一个引用类型:

1
2
3
4
5
6
7
8
template<typename T>
T retR(T&& p) // p is a forwarding reference
{
return T{...}; // OOPS: returns a reference if T is a reference
}

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

为了安全起见,我们有两种选择:

  • 使用 type trait std::remove_reference<>参见附录 D.4(未写))将 T 转化为一个非引用类型:

    1
    2
    3
    4
    5
    template<typename T>
    typename std::remove_reference<T>::type retV(T p)
    {
    return T{...}; // always returns by value
    }
  • 让编译器推导返回值——将返回类型声明为 auto(参见 1.3.2 节),因为 auto 总会进行类型退化:

    1
    2
    3
    4
    5
    template<typename T>
    auto retV(T p) // by-value return type deduced by compiler
    {
    return T{...};
    }

7.6 模板参数声明推荐

在前面几节我们以及了解到有很多方式声明模板参数:

  • 按值传递参数:

    这种方式比较简单,他会将字符串常量和原始数组类型退化,但是它在一些庞大的对象上表现不佳。调用者可以通过 std::cref()std::ref() 来传递引用,但是必须清楚该行为是否合法。

  • 按引用传递参数:

    这种方式在一些庞大的对象上面有更好的表现,特别是传递

    • 现有的对象(lvalues) 传递给 lvalue reference,
    • 临时对象(prvalues)或者可移动对象(xvalue)传递给 rvalue reference
    • 前面两种对象传递给转发引用。

    因为在这些情况下,参数不会类型退化,你需要对字符串常量和其他原始数组进行特殊处理。对于转发引用,你必须意识到这种方法中函数模板参数可能会隐式地推导为一个引用类型。

一般性建议

考虑到声明模板参数的选择,对于函数模板,我们建议:

  1. 默认地将参数声明为按值传递。这种方法比较方便而且通常在字符串常量上也能正常工作。它在一些小对象和临时对象和可移动对象上面表现比较好。调用者有时候也可以使用 std::ref()std::cref() 来传递一些大型的对象(lvalues)来避免拷贝带来的巨大开销。
  2. 如果有充分的理由,使用其他方式:
    • 如果你需要 outinout 参数,需要返回一个新对象或者允许修改对象并返回给调用者,按非常引用来传递参数(除非你更希望用指针传递参数)。然而,你需要考虑禁止程序意外地接受了一个 const 对象,参见 7.2.2 节
    • 如果模板是用来转发参数的话,使用完美转发。也就是通过将参数定义成转发引用,并使用 std::forward<>()。考虑使用 std::decay<>std::common<>() 来处理字符串常量和原始数组。
    • 如果代码的开销是关键而且拷贝参数这一行为会有巨大开销,使用常引用。当然,不要使用它如果你需要本地副本的话。
  3. 如果你非常了解的话,不要遵守这些建议。但是,不要按照直觉来推测代码的行为,即使是专家按直觉也会出错。

不要过度泛型

注意,实际上,函数模板有一定的限制,一般不适用于任何类型的参数。例如,你可能知道要传递一些类型的 vector 对象。在这种情况下,最好不要将函数定义地太过泛型,因为可能会产生一些意想不到的错误。因此,最好使用以下的声明:

1
2
3
4
5
template<typename T>
void printVector(std::vector<T> const& v)
{
...
}

通过这个声明,我们可以确保 T 不会成为一个引用类型,因为 vector 元素的类型不能是引用。同样,按值传递 vector 很有可能成为一个开销巨大的操作,因为 std::vector<> 的拷贝构造会将所有元素拷贝。因此,将 vector 参数声明为按值传递并不合适。如果我们只是用模板参数 T 声明参数 v,这个函数是按值调用还是按引用调用会更加不明显。

std::make_pair() 实例分析

std::make_pair<>() 是一个展示参数传递机制的陷阱很好的例子。它是 C++ 标准库中一个方便的函数模板,可以通过类型推导创建一个 std::pair<> 对象。它的声明在不同的 C++ 版本中不一样:

  • 在第一个 C++ 标准 C++98 中,make_pair<>() 声明在 std 命名空间中,用引用调用来避免不必要的拷贝:

    1
    2
    3
    4
    5
    template<typename T1, typename T2>
    pair<T1, T2> make_pair(T1 const& a, T2 const& b)
    {
    return pair<T1, T2>(a, b);
    }

    当使用字符串常量或者原始数组类型的 pair 时,这个声明会产生一些严重的错误。

  • 因此,在 C++03时,函数被定义成了按值调用:

    1
    2
    3
    4
    5
    template<typename T1, typename T2>
    pair<T1, T2> make_pair(T1 a, T2 b)
    {
    return pair<T1, T2>(a, b);
    }

    编写者对这个更改的解释如下:”与其他两个建议相比,这是对标准库的一个非常小的更改。而且该解决方案带来优势足以抵消它性能方面的不足。“

  • 但是,通过 C++11,make_pair() 需要支持移动语义,因此参数必须是转发引用。因此,函数的定义差不多如下:

    1
    2
    3
    4
    5
    6
    7
    8
    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)));
    }

    真正完整的实现比这个更加复杂:为了支持 std::ref()std::cref(),这个函数也拆包了 std::reference_wrapper 实例,转化成了真正的引用。

C++ 标准库现在已经在很多地方通过同样的方法完美转发了传递的参数,这些实现通常都使用了 std::decay<>

7.7 小结

  • 当测试模板时,请用不同长度的字符串常量。
  • 按值传递的模板参数会类型退化,但按引用传递的不会。
  • type trait std::decay<> 允许你将模板中按引用传递的参数进行类型退化。
  • 在一些情况下 std::cref()std::ref() 允许你在按值传递参数的函数模板中按引用传递参数。
  • 按值传递模板参数比较简单,但是并不适用于所有情况。
  • 按值传递函数模板参数,除非你有足够的理由使用其他方式。
  • 确保返回的值是按值返回的(函数模板的类型不能直接用于返回类型)。
  • 经常测试重要代码的表现。不要依赖直觉;它可能是错误的。

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!