大多數人不喜歡將參數設置爲按值傳遞的緣由是怕參數拷貝的過程當中帶來的性能問題,可是不是全部按值傳遞都會有參數拷貝,好比: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++
copy constructor
。copy constructor
(這個也是C++17的新特性:Mandatory Copy Elision or Passing Unmaterialized Objects)move constructor
。雖然上面4種狀況只有第一種纔會調用copy constructor
,可是這種狀況纔是最多見的。git
以前的文章介紹過,當模板參數是值傳遞時,會形成參數decay:github
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*
這種方式有優勢也有缺點:數組
char const*
仍是相似const char[13]
。char const*
按引用傳遞不會拷貝參數,也不會有上面提到的decay。這看起來很美好,可是有時候也會有問題:緩存
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成指針,而且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屬性。性能
若是想改變參數的值而且不但願拷貝,那麼會使用這種狀況。可是這時咱們不能綁定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]
這個也是聲明參數爲引用的一個重要場景:
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
主要用來「喂」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。可是假如寫模板的話,這些因素仍是須要考慮進去。
通常在下面狀況下,返回值會被聲明爲引用:
可是將返回值聲明爲引用須要格外當心:
auto s = std::make_shared<std::string>("whatever"); auto& c = (*s)[0]; s.reset(); std::cout << c; // run-time ERROR
若是你確實想將返回值聲明爲值傳遞,僅僅聲明T是不夠的:
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&
因此,有兩種方法是安全的:
template<typename T> typename std::remove_reference<T>::type retV(T p) { return T{...}; // always returns by value }
template<typename T> auto retV(T p) { // by-value return type deduced by compiler return T{...}; // always returns by value }
以前文章討論過auto推斷類型的規則,會忽略引用。
對應模板參數,通常建議以下:
好比你的模板函數只想接受vector,那麼徹底能夠定義成:
template<typename T> void printVector (const std::vector<T>& v) { ... }
這裏就沒有必要定義爲const T& v
.
std::make_pair()
是一個很好演示模板參數機制的例子:
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]
template<typename T1, typename T2> pair<T1,T2> make_pair (T1 a, T2 b) { return pair<T1,T2>(a,b); }
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是常見的搭配。
(完)
朋友們能夠關注下個人公衆號,得到最及時的更新: