大多数人不喜欢将参数设置为按值传递的缘由是怕参数拷贝的过程当中带来的性能问题,可是不是全部按值传递都会有参数拷贝,好比: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是常见的搭配。
(完)
朋友们能够关注下个人公众号,得到最及时的更新: