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是常見的搭配。

(完)

朋友們能夠關注下個人公衆號,得到最及時的更新: