按值傳遞
大多數人不喜歡將參數設置為按值傳遞的原因是怕參數拷貝的過程中帶來的性能問題,但是不是所有按值傳遞都會有參數拷貝,比如:
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個調用:
- 第一個 : 我們傳遞了一個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
,但是這種情況才是最常見的。
Decay
之前的文章介紹過,當模板參數是值傳遞時,會造成參數decay:
- 丟失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)。因此,編譯器會假設所有該地址的緩存(通常為寄存器)在該函數調用后都會失效,如果要使用該地址的值,會重新從內存中載入。
引用不會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是常見的搭配。
(完)
朋友們可以關注下我的公眾號,獲得最及時的更新: