标签:成员函数 dscp 部分 gui operation 编写 abi 控制 按值传递
//c++11中std::move的简化版本 template<typename T> typename remove_reference<T>::type&& move(T&& param) //返回类型加上&&表明是一个右值引用 { using ReturnType = typename remove_reference<T>::type&&; // 当T本身是一个左值引用时,T&&就是一个左值引用, 为了确保&&不会被应用到一个引用类型上,需要使用remove_reference来做类型萃取 return static_cast<ReturnType>(param);//保证返回值是一个右值引用,该值本身就是右值 } //c++14中std::move的简化版本 template<typename T> decltype(auto) move(T&& param) //返回值类型推导 { using ReturnType = remove_reference_t<T>&&; //标准库别名模板 return static_cast<ReturnType>(param); }
std::move和std::forward不能用于const对象,因为把一个对象的值给移走,本质上就是对该对象进行修改,所以语言不允许对函数修改传递给他们的const对象,例如:
class Annotation { public: explicit Annotation(const std::string text) //通过拷贝将参数按值传递并设为const防止修改 :value(std::move(text))
// 使用移动操作避免重复拷贝,但是move不改变const属性,仅仅是将const std::string左值转变成const std::string右值,即传递给value构造函数的仍然是const std::string { ... // construction } private: std::string value; }; class string { public: ... string(const string& rhs); // const左值引用可以绑定到const 右值 string(string&& rhs); // 不接受const类型的参数 };
实现结果:
#include <iostream> using namespace std; class A { public: A(const string& a) { cout<<"copy construct"<<endl; cout<<"para:"<<a<<endl; name_ = a; cout<<"name:"<<name_<<endl; } A(string&& rhs):name_(std::move(rhs)) //此时传进来的rhs是左值变量,但引用的内容是右值,为了将内容传递给name_,需要将rhs的右值内容通过move来获取,最终传入string的移动构造函数中 { cout<<"move construct"<<endl; cout<<"para:"<<rhs<<endl; cout<<"name:"<<name_<<endl; } private: string name_; }; int main() { A a("blackwall"); string s = "whitewall"; //等号左边是左值变量,右边是右值,是内容 A b(std::move(s));// 为了实现移动构造,需要将左值变量的右值内容传给移动构造函数的右值引用 const string ss = "greenwall"; A c(std::move(ss)); cout << "Hello, World!" << endl; return 0; } //output ? ./a.out move construct para: name:blackwall move construct para: name:whitewall copy construct para:greenwall name:greenwall Hello, World!
std::forward的作用是当我们传入的参数是左值时,在内部将参数转发到其他函数时仍然是按照左值转发(也就是调用左值参数的函数),而当是右值时按照右值转发(调用右值参数的函数);仅当传入的参数被一个右值初始化过后,std::forward会把该参数转换成一个右值。
void process(const Widget& lvalArg); void process(Widget&& rvalArg); template<typename T> void logAndProcess(T&& param)//外部函数形参接收右值引用 { auto now = std::chrono::system_clock::now(); makeLogEntry("Calling ‘process‘", now); process(std::forward<T>(param));//转发时按照形参接收的类型进行转发 } Widget w; logAndProcess(w); // error, 左值不能绑定到右值上去 logAndProcess(std::move(w));
std::move完全可以使用std::forward来代替,而且std::forward完全可以使用static_cast来代替
但是使用std::forward来代替std::move时,需要额外接收一个模板类型参数,且该模板参数不能是引用类型,因为编码方式决定了传递的值必须是一个右值
使用static_cast来代替std::forward时需要在每个需要的地方手动编写转换过程,这种方式不够简洁且会出错。
void f(Widget&& param); // 右值引用 Widget&& var1 = Widget(); // 右值引用 auto&& var2 = var1; // 通用引用 template<typename T> void f(std::vector<T>&& param); // 右值引用 template<typename T> void f(T&& param); // 通用引用
template<typename T> void f(T&& param); Widget W; f(w); // 左值传递,左值引用 f(std::move(w)); // 右值传递,右值引用
template<typename T> void f(std::vector<T>&& param); // 右值引用,因为声明的形式不是T&&,而是std::vector<T>&&
template<typename T> void f(const T&& param);
template<class T, class Allocator = allocator<T>> class vector { public: void push_back(T&& x); //因为没有特定vector实例存在时,push_back也不会存在,而实例的类型完全决定了push_back的声明 ... }; //当出现如下情况时 std::vector<Widget> v; //使得 class vector<Widget, allocator<Widget>> { public: void push_back(Widget&& x); //右值引用,此处并没有使用类型推导 ... };
//相反,emplace_back成员函数却使用了类型推导
template<class T, class Allocator = allocator<T>>
class vector {
public:
template<class... Args>
void emplace_back(Args&&... args); //此处类型参数Args和vector的类型参数T无关,所以每次调用时都要做类型推导
...
};
class Widget { public: template<typename T> void setName(T&& newName) //通用引用 { name = std::move(newName); } //但是却使用std::move ... private: std::string name; std::shared_ptr<SomeDataStructure> p; }; std::string getWidgetName(); //工厂函数 Widget w; auto n = getWidgetName(); //n是一个局部变量 w.setName(n); //把n的值给移动走了,因为通用引用可以识别左值或右值引用
如果通过指定左值引用和右值引用函数来代替通用引用,那么这种做法会使得手写重载函数数量因为函数参数数量而呈指数增加
std::move和std::forward仅仅用在最后一次使用该引用的地方
template<typename T> void setSignText(T&& text) { sign.setText(text); // 使用text但不修改它 auto now = std::chrono::system_clock::now(); signHistory.add(now, std::forward<T>(text)); //有条件地将text转换成右值 }
返回右值引用或者通用引用的函数,可以通过std::move或std::forward将值直接移动到返回值内存中
Matrix operator+(Matrix&& lhs, const Matrix& rhs) { lhs+=rhs; return std::move(lhs); //移动lhs到返回值内存中,即便Matrix不支持移动,也只会简单的把右值拷贝到返回值内存中 } Matrix operator+(Matrix&& lhs, const Matrix& rhs) { lhs+=rhs; return lhs; //拷贝lhs到返回值内存中 } template<typename T> Fraction reduceAndCopy(T&& frac) { frac.reduce(); return std::forward<T>(frac);// 如果传入的是右值,就移动返回,如果是左值,就拷贝返回 }
对于返回局部变量的值,不能完全效仿上述规则
Widget makeWidget() { Widget w; ... return w; //“拷贝”返回 } Widget makeWidget() { Widget w; ... return std::move(w); // “移动”返回 }
编译器在处理返回值的函数时会采用一种优化:Return Value Optimization(RVO),它有时候会在返回值内存中直接构造这个结果。但是需要满足两个条件:
函数返回类型和局部对象类型一致
返回的值就是这个局部对象
因此,在上述拷贝返回值的函数中,满足了上述两个条件,编译器会使用RVO来避免拷贝。但是针对移动返回值的函数中,编译器不会执行RVO,因为这个函数不满足条件2,也就是返回值并不是局部对象本身,而是局部对象的引用,因此,编译器只能把w移动到返回值的位置。这样以来,那些想要通过对局部变量使用std::move来帮助编译器进行优化的程序员,实际上却限制了编译器的优化选择。
Widget makeWidget(Widget w) { ... return w; ---> return std::move(w); //被编译器认为是一个右值 }
4. Avoid overloading on universal references
std::multiset<std::string> names; //全局数据结构 std::string nameFromIdx(int idx); //根据索引返回姓名字符串 template<typename T> void logAndAdd(T&& name) { auto now = std::chrono::system_clock::now(); log(now, "logAndAdd"); names.emplace(std::forward<T>(name)); } void logAndAdd(int idx) { auto now = std::chrono::system_clock::now(); log(now, "logAndAdd"); names.emplace(nameFromIdx(idx)); } std::string petName("Darla"); logAndAdd(petName); //拷贝左值 logAndAdd(std::string("Persephone")); //移动右值 logAndAdd("Patty Dog"); //直接在multiset中创建string而不是拷贝一个临时字符串 short nameIdx = 22; logAndAdd(nameIdx); //错误,short参数将会匹配到通用引用参数的函数调用,在将short参数转发到names的string构造函数中时,会出错
带有通用引用参数的函数是C++中最贪婪的函数,它们几乎对所有类型的参数都会产生完美匹配的实例化。这样它就会产生许许多多的参数类型的重载实例函数。
在编译器为类自动生成移动和拷贝构造函数时,也不能使用重载过的通用引用参数构造函数,因为通用引用参数的构造函数在匹配顺序上会在其他重载函数之前。
class Person{ public: template<typename T> explicit Person(T&& n):name(std::forward<T>(n)) {} explicit Person(int idx):name(nameFromIdx)) {} Person(const Person& rhs); //编译器自动产生 Person(Person&& rhs); //编译器自动产生 ... private: std::string name; }; Person p("Nancy"); auto cloneOfP(p); //出错!!!
在合适的条件下,即便存在模板构造函数可以通过实例化来产生拷贝或者移动构造函数,编译器也会自动产生拷贝或者移动构造函数。
上述auto cloneOfP(p)语句似乎应该是调用拷贝构造函数,但是实际上会调用完美转发构造函数,然后会用Person对象去实例化Person的string成员,然而并没有这种匹配规则,马上报错!
如果对传入的对象p加上const修饰,那么虽然模板函数虽然会被实例化成为一个接收const类型Person对象的函数,但是具有在const类型参数的所有重载函数中,C++中的重载解析规则是:当模板实例函数和非模板函数同样都能匹配一个函数调用,那么非模板函数的调用顺序优先模板函数。
class Person{ public: explicit Person(std::string n):name(std::move(n)) {} explicit Person(int idx):name(nameFromIdx)) {} ... private: std::string name; };
这样以来,构造函数不仅能正确匹配,而且可以使用移动语义将拷贝传递的参数直接移动给成员变量。
一种高级做法,使用标签分发方式(Tag dispatch)
传递const左值引用和传值方式都不支持完美转发,如果使用通用引用是为了完美转发,那就不得不使用通用引用,同时如果不想放弃重载,就需要在特定条件下强制模板函数匹配无效。
在调用点解析重载函数具体是通过匹配调用点的所有参数与所有重载函数的参数进行匹配实现的。通用引用参数一般会对任何传入的参数产生匹配,但是如果通用引用是包含其他非通用引用参数的参数列表中的一部分,那么在非通用引用参数上的不匹配会使得已经匹配的通用引用参数无效。这就是标签分发的基础。
template<typename T> void logAndAdd(T&& name) //标签分发函数,通过使用对参数类型的判断,使得通用引用参数获得的匹配无效,将控制流分发到两个不同的处理函数中 { logAndAddImpl(std::forward<T>(name), std::is_integral<typename std::remove_reference<T>::type>());// 此处必须去掉引用,因为std::is_integral会把int& 判断为非int类型,也就是std::false_type } template<typename T> void logAndAddImpl(T&& name, std::false_type) //处理非整型参数 { auto now = std::chrono::system_clock::now(); log(now, "logAndAdd"); names.emplace(std::forward<T>(name)); } std::string nameFromIdx(int idx); void logAndAddImpl(int idx, std::true_type) //处理整型参数类型 { logAndAdd(nameFromIdx(idx)); //将整型转换成字符串,再重新转发到标签分发函数中,再次分发 }
上面的std::true_type和std::false_type就是标签,我们可以利用它们来强制选择我们希望调用的重载函数,这在模板元编程中非常常见。这种做法的核心是存在一个未重载过的函数作为客户端的API,然后将任务分发到其他实现函数中。但是,这种做法针对类的构造函数不可行,因为即便将构造函数写成标签分发函数,在其他函数中完成具体的任务,但是有些构造调用也会绕过标签分发函数而转向编译器自动生成的拷贝和移动构造函数。问题在于传入的参数并不总是会匹配到通用引用参数的函数,尽管大多数情况下确实会匹配。
另一种高级做法,限制(constraining)采用通用应用的模板
为了在特定的条件下,让函数调用发生在应该发生的位置上,我们需要根据条件来启用/禁用模板匹配,方式是std::enable_if,如果内部判断条件为true,那么就会启用模板,否则会禁用模板
class Person{ public: template<typename T, typename = typename std::enable_if<condition>::type> //在condition中指定满足什么条件 explicit Person(T&& n); ... };
在这种例子下,我们想要的结果是:当传入的参数类型是Person时,应该调用拷贝构造函数,也就是要禁用模板;否则应该启用模板,将函数调用匹配到通用引用构造函数中。判断类型的方式是std::is_same
class Person{ public: template<typename T, typename = typename std::enable_if<!std::is_same<Person, T>::value>::type> explicit Person(T&& n); ... };
但是这中间有一个问题,就是std::is_same会把Person和Person&判断为不同类型,因此我们希望会略掉对这个Person类型的一切修饰符,拿到最原始的类型,这需要用到std::decay<T>::type
//无论是否是引用: Person , Person& , Person&&应该和Person一样 //无论是否是const或volatile: const Person , volatile Person , const volatile Person应该和Person一样 class Person{ public: template<typename T, typename = typename std::enable_if<!std::is_same<Person, typename std::decay<T>::type>::value>::type> explicit Person(T&& n); ... };
但是上面的做法在有派生类存在的情况下会出现问题
class SpecialPerson: public Person{ public: SpecialPerson(const SpecialPerson& rhs): Person(rhs) {...} //子类构造函数应该先调用父类构造函数,但是传入的参数类型是SpecialPerson,根据上面的类型判断,Person与SpecialPerson不是同一个类型,因此会禁用模板函数,转而调用拷贝构造函数 SpecialPerson(SpecialPerson&& rhs): Person(std::move(rhs)) {...} ... };
因此,我们需要一个能够判断一个类型是继承自另一个类型的方法,这就是std::is_base_of<T1,T2>::value,这种方法在T2是T1的子类时返回true。对于用户自定义的类型而言,他们是继承自自身的,也就是说std::is_base_of<T,T>会返回为true,但是当T是内建类型时,就会返回为false。
class Person{ public: template<typename T, typename = typename std::enable_if<!std::is_base_of<Person, typename std::decay<T>::type>::value>::type> explicit Person(T&& n); ... };
class Person{ public: template<typename T, typename = typename std::enable_if< !std::is_base_of<Person, typename std::decay<T>::type>::value && !std::is_integral<std::remove_reference<T>::type>()> ::type> explicit Person(T&& n): name(std::forward<T>(n)) {...} explicit Person(int idx): name(nameFromIdx(idx)) {...} ... private: std::string name; };
class Person{ public: template<typename T, typename = typename std::enable_if< !std::is_base_of<Person, typename std::decay<T>::type>::value && !std::is_integral<std::remove_reference<T>::type>()> ::type> explicit Person(T&& n): name(std::forward<T>(n)) { static_assert(std::is_constructible<std::string, T>::value, "Parameter n can‘t be used to construct a std::string"); //因为该函数在转发之后执行,因此这条错误信息将会在左右错误信息输出之后出现 } ... private: std::string name; };
template<typename T> void func(T&& param); Widget widgetFactory(); Widget w; func(w); // T被推导为Widget& func(widgetFactory()); //T被推导为Widget
C++不允许用户使用指向引用的引用,但是编译器编译出的结果中如果出现了多重引用,就会应用引用折叠
int x; ... auto& & rx = x; // 错误 template<typename T> void func(T&& param); Widget w; func(w); // T被推导为Widget&
如果拿推导出的类型Widget&再去引用到func模板上,就会出现void func(Widget& &¶m); 我们知道当通用引用参数被一个左值初始化,那么这个参数的类型就应该是左值引用。由此可知,这里发生了引用折叠。
template<typename T> T&& forward(typename remove_reference<T>::type& param) { return static_cast<T&&>(param); }
那么把各种情况应用到std::forward函数上时,就有如下结果(引用折叠出现的第一种环境:模板实例化)
template<typename T> void f(T&& fParam) { ... someFunc(std::forward<T>(fParam); } 1.传入左值 Widget w; f(w); -------> void f(Widget& fParam) { ... someFunc(std::forward<Widget&>(fParam); } Widget& && forward(typename remove_reference<Widget&>::type& param) { return static_cast<Widget& &&>(param); } ---> Widget& && forward(Widget& param) { return static_cast<Widget& &&>(param); } -> Widget& forward(Widget& param) { return static_cast<Widget&>(param); } 2.传入右值 Widget w; f(std::move(w)); -------> void f(Widget fParam) { ... someFunc(std::forward<Widget>(fParam); } Widget && forward(typename remove_reference<Widget>::type& param) { return static_cast<Widget&&>(param); } ---> Widget&& forward(Widget& param) { return static_cast<Widget&&>(param); }
引用折叠出现的第二种环境是auto变量的类型推导
Widget widgetFactory(); Widget w; auto&& w1 = w; --> Widget& && w1 = w; --> Widget& w1 = w; //发生了引用折叠 auto&& w2 = widgetFactory(); -->Widget&& w2 = widgetFactory(); //没有发生引用折叠
可以看出,通用引用并不是一种新的引用,实际上是一种满足两种条件的右值引用:1.类型推导区分左值引用和右值引用 2.发生引用折叠
其他两种会出现引用折叠的环境是:
使用typedef和alias声明
template<typename T> class Widget{ public: typedef T&& RvalueRefToT; ... }; Widget<int&> w; --> typedef int& && RvalueRefToT; --> typedef int& RvalueRefToT;
使用decltype的地方
template<typename T> void fwd(T&& param) { f(std::forward<T>(param)); } template<typename... Ts> void fwd(Ts&&... params) { f(std::forward<Ts>(params)...); }
如果调用fwd和直接调用f函数所造成的结果不同,那么就是说完美转发出现了问题
void f(const std::vector<int>& v); f({1,2,3}); //会被隐式转换成std::vector<int> fwd({1,2,3}); //无法编译
原因是:
这种情况下出错的类型有:
编译器无法推导出一个类型:只要参数中有一个及以上无法推导出类型,就无法编译
编译器推到出错误的类型:要么是推导出来的类型使得无法编译,要么是推到出来的类型在重载函数情况下匹配到错误的函数调用。
auto il = {1,2,3}; fwd(il);
因为,花括号初始化对于auto变量的类型推导是可以被推导成std::initializer_list对象的,而有了具体类型之后,在模板函数中就可以进行匹配。
class Widget { public: static constexpr std::size_t MinVals = 28; ... }; ... std::vector<int> widgetData; widgetData.reserve(Widget::MinVals); //没问题 void f(std::size_t val); f(Widget::MinVals); --> f(28); //没问题 fwd(Widget::MinVals); //出错
因为对于缺乏定义的MinVals,编译器会把用到此值的地方替换成28,而不用分配内存,但是如果要取地址的话,编译器就会分配一块内存来存储这个值,并返回内存的地址,不提供定义这种做法只能在编译期通过,在链接的过程就会报错。同样,在将MinVals传递到模板函数fwd中时,这个模板参数是一个引用,它本质上和指针是一样,只不过是一个会自动解引用的指针,那么在编译该函数时就需要对MinVals进行取地址,而MinVals此时并没有定义,也就没有内存空间。
但是上述行为实际上是依赖于编译器的,安全的做法是在cpp文件中定义一次MinVals
constexpr std::size_t Widget::MinVals;
重载函数名和模板名的自动推导
一个模板函数接收重载函数作为参数时,模板函数无法自动推导出用户想要调用的重载函数
template<typename T> void fwd(T&& param) { f(std::forward<T>(param)); } void f(int (*pf)(int)); / void f(int pf(int)); int processVal(int value); int processVal(int value, int priority); f(processVal); //虽然processVal不是一个类型,但编译器可以正确匹配到第一个重载函数 fwd(processVal); //错误,proecssVal不是一个类型,自动推导的fwd不知道该匹配哪一个重载函数
template<typename T> T workOnVal(T param) {...} fwd(workOnVal); //出错,不知道匹配哪一个模板函数实例
using ProcessFuncType = int (*)(int); ProcessFuncType processValPtr = processVal; //指定函数 fwd(processValPtr); //可以正确转发,因为类型已经指定 fwd(static_cast<ProcessFuncType>(workOnVal); //可以正确转发,因为已经实例化
struct IPv4Header { std::uint32_t version:4, IHL:4, DSCP:6, ECN:2, totalLength:16; ... }; void f(std::size_t sz); IPv4Header h; f(h.totalLength); //没问题,bit会自动转换成size_t fwd(h.totalLength);//出错,因为fwd的参数是一个通用引用,而C++标准规定:非const类型的引用不能绑定到bit域上,因为没有办法寻址。 //bit域参数传递的可行方式只有:按值传递,或者加上const修饰的引用。 //按值传递时,函数会接收到bit域里面的值 //按const引用传递时,会首先将bit域的值拷贝到一个整型类型中,然后再绑定到该类型上 auto length = static_cast<std::uint16_t>(h.totalLength); fwd(length);
[Effective Modern C++(11&14)]Chapter 5: Rvalue References, Move Semantics, and Perfect Forwarding
标签:成员函数 dscp 部分 gui operation 编写 abi 控制 按值传递
原文地址:https://www.cnblogs.com/burningTheStar/p/8733475.html