c++11-17 模板核心知识(七)—— 模板参数 按值传递 vs 按引用传递

按值传递

大多数人不喜欢将参数设置为按值传递的缘由是怕参数拷贝的过程当中带来的性能问题,可是不是全部按值传递都会有参数拷贝,好比:ios

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

咱们逐一看一下上面的4个调用:c++

  • 第一个 : 咱们传递了一个lvalue,这会使用std::string的copy constructor
  • 第二和第三个 : 这里传递的是prvalue(随手建立的临时对象或者函数返回的临时对象),通常状况下编译器会进行参数传递的优化,不会致使copy constructor这个也是C++17的新特性:Mandatory Copy Elision or Passing Unmaterialized Objects
  • 第四个 : 传递的是xvalue(一个使用过std::move后的对象),这会调用move constructor

虽然上面4种状况只有第一种才会调用copy constructor,可是这种状况才是最多见的。git

Decay

以前的文章介绍过,当模板参数是值传递时,会形成参数decay:github

  • 丢失const和volatile属性。
  • 丢失引用类型。
  • 传递数组时,模板参数会decay成指针。
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 char const*

这种方式有优势也有缺点:数组

  • 优势:可以统一处理decay后的指针,而没必要区分是char const*仍是相似const char[13]
  • 缺点:没法区分传递的是一个数组仍是一个指向单一元素的指针,由于decay后的类型都是char const*

按引用传递

按引用传递不会拷贝参数,也不会有上面提到的decay。这看起来很美好,可是有时候也会有问题:缓存

传递const reference

template<typename T>
void printR (const T& 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 T&后,全部的调用都不会有拷贝。那么哪里会有问题呢?安全

你们都知道,传递引用时,实际传递的是一个地址,那么编译器在编译时不知道调用者会针对这个地址作什么操做。理论上,调用者能够随意改变这个地址指向的值(这里虽然声明为const,可是仍然有const_cast能够去除const)。所以,编译器会假设全部该地址的缓存(一般为寄存器)在该函数调用后都会失效,若是要使用该地址的值,会从新从内存中载入。app

引用不会Decay

以前文章介绍过,按引用传递不会decay。所以若是传递的数组,那么推断参数类型时不会decay成指针,而且const和volatile都会被保留。函数

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]

所以,在printR函数内经过T声明的变量没有const属性。性能

传递nonconst reference

若是想改变参数的值而且不但愿拷贝,那么会使用这种状况。可是这时咱们不能绑定prvalue和xvalue给一个nonconst reference(这是c++的一个规则

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

一样,这种状况不会发生decay:

int arr[4];
outR(arr);          // OK: T deduced as int[4], arg is int(&)[4]

传递universal reference

这个也是声明参数为引用的一个重要场景:

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)

可是这里须要额外注意一下,这是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

使用std::ref()和std::cref()

主要用来“喂”reference 给函数模板,后者本来以按值传递的方式接受参数,这每每容许函数模板得以操做reference而不须要另写特化版本:

template <typename T>
void foo (T val) ;

...
int x;
foo (std: :ref(x));
foo (std: :cref(x));

这个特性被C++标准库运用于各个地方,例如:

  • make_pair()用此特性因而可以建立一个 pair<> of references.
  • make_tuple()用此特性因而可以建立一个tuple<> of references.
  • Binder用此特性因而可以绑定(bind) reference.
  • Thread用此特性因而可以以by reference形式传递实参。

注意std::ref()不是真的将参数变为引用,只是建立了一个std::reference_wrapper<>对象,该对象引用了原始的变量,而后将std::reference_wrapper<>传给了参数。std::reference_wrapper<>支持的一个重要操做是:向原始类型的隐式转换:

#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"
}

区分指针和数组

前面说过,按值传递的一个缺点是,没法区分调用参数是数组仍是指针,由于数组会decay成指针。那若是有须要区分的需求,能够这么写:

template <typename T, typename = std::enable_if_t<std::is_array_v<T>>>
void foo(T &&arg1, T &&arg2) {
  ...
}

std::enable_if后面会介绍,它的意思是,假如不符合enable_if设置的条件,那么该模板会被禁用。

其实如今基本上也不用原始数组和字符串了,都用std::string、std::vector、std::array。可是假如写模板的话,这些因素仍是须要考虑进去。

处理返回值

通常在下面状况下,返回值会被声明为引用:

  • 返回容器或者字符串中的元素(eg. operator[]、front())
  • 修改类成员变量
  • 链式调用(operator<<、operator>>、operator=)

可是将返回值声明为引用须要格外当心:

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

确保返回值为值传递

若是你确实想将返回值声明为值传递,仅仅声明T是不够的:

  • forwarding reference的状况,这个上面讨论过
template<typename T>
T retR(T&& p) {
    return T{...};        // OOPS: returns by reference when called for lvalues
}
  • 显示的指定模板参数类型:
template<typename T>  // Note: T might become a reference
T retV(T p) {
  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<> :
template<typename T>
typename std::remove_reference<T>::type retV(T p) {
  return T{...};     // always returns by value
}
  • auto :
template<typename T> 
auto retV(T p)  {     // by-value return type deduced by compiler
  return T{...};      // always returns by value
}

以前文章讨论过auto推断类型的规则,会忽略引用。

模板参数声明的推荐

  • 按值传递
    • 数组和字符串会decay。
    • 性能问题(可使用std::ref和std::cref来避免,可是要当心这么作是有效的)。
  • 按引用传递
    • 性能更好。
    • 须要forwarding references,而且注意此时模板参数为隐式的引用类型。
    • 须要对参数是数组和字符串的状况额外关注。

通常性建议

对应模板参数,通常建议以下:

  • 默认状况下,使用按值传递。理由:
    • 简单,尤为是对于参数是数组和字符串的状况。
    • 对于小对象而言,性能也不错。调用者可使用std::ref和std::cref.
  • 有以下理由时,使用按引用传递:
    • 须要函数改变参数的值。
    • 须要perfect forwarding。
    • 拷贝参数的性能很差。
  • 若是你对本身的程序足够了解,固然能够不遵照上面的建议,可是不要仅凭直觉就对性能作评估。最好的方法是:测试。

不要将模板参数设计的太通用

好比你的模板函数只想接受vector,那么彻底能够定义成:

template<typename T>
void printVector (const std::vector<T>& v) {
  ...
}

这里就没有必要定义为const T& v.

std::make_pair()模板参数历史演进

std::make_pair()是一个很好演示模板参数机制的例子:

  • 在C++98中,make_pair<>()的参数被设计为按引用传递来避免没必要要的拷贝:
template<typename T1, typename T2>
pair<T1,T2> make_pair (T1 const& a, T2 const& b) {
  return pair<T1,T2>(a,b);
}

可是当使用存储不一样长度的字符串或者数组时,这样作会致使严重的问题。 这个问题记录在See C++ library issue 181 [LibIssue181]

  • 因而在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));
}

标准库中perfect forward和std::decay是常见的搭配。

(完)

朋友们能够关注下个人公众号,得到最及时的更新:

相关文章
相关标签/搜索