一、RVO优化和std::move、std::forward
以下是一个综合性的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
| #include <iostream> #include <memory> #include <ostream> using namespace std;
class Data {};
class Widget { std::string name; std::shared_ptr<Data> ptr;
public: Widget() {cout << "Widget() used for object@" << this << " addr_name: " << &(this->name) <<" string buffer addr: "<< static_cast<const void*>(this->name.data())<< endl;};
Widget(const Widget &w) : name(w.name), ptr(w.ptr) { cout << "Widget(const Widget& w) used for object@" << this << " addr_name: " << &(this->name) <<" string buffer addr: "<< static_cast<const void*>(this->name.data())<< endl; } Widget(Widget &&rhs) noexcept : name(std::move(rhs.name)), ptr(std::move(rhs.ptr)) { cout << "Widget(Widget&& rhs) used for object@" << this << " addr_name: " << &(this->name) <<" string buffer addr: "<<static_cast<const void*>(this->name.data())<< endl; }
template <typename T> void setName(T &&newName) { if (newName != name) { name = std::forward<T>( newName); } } ostream &print_addr_of_name(ostream &os) { cout << "addr of name: " << static_cast<const void*>(this->name.data()) << endl; return os; } };
class Complex { double x; double y;
public: Complex(double x = 0, double y = 0) : x(x), y(y) {} Complex &operator+=(const Complex &rhs) { x += rhs.x; y += rhs.y; return *this; } };
Complex operator+(Complex &&lhs, const Complex &rhs) { lhs += rhs; return std::move(lhs); }
template <typename T> auto test(T &&t) { return std::forward<T>( t); }
Widget makeWidget() { Widget w; return w; }
Widget makeWidget(Widget w) { return w; }
int main() { cout << "1. 针对右值引用实施std::move,针对万能引用实施std::forward" << endl; Widget w; w.setName("SantaClaus"); cout << "w_addr:" << &w << endl; w.print_addr_of_name(cout);
cout << "2. 按值返回时" << endl; auto t1 = test(w); auto t2 = test(std::move(w)); cout << "t1_addr:" << &t1 << endl; t1.print_addr_of_name(cout);
cout << "t2_addr:" << &t2 << endl; t2.print_addr_of_name(cout);
cout << "3. RVO优化" << endl;
Widget w1 = makeWidget(); cout << "w1_addr:" << &w1 << endl; w1.print_addr_of_name(cout);
cout << "w2:\n"; Widget w2 = makeWidget(w1);
cout << "w2_addr:" << &w2 << endl; w2.print_addr_of_name(cout);
return 0; }
|
打印结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ./main 1. 针对右值引用实施std::move,针对万能引用实施std::forward Widget() used for object@0x16dd46df0 addr_name: 0x16dd46df0 string buffer addr: 0x16dd46df0 w_addr:0x16dd46df0 addr of name: 0x16dd46df0 2. 按值返回时 Widget(const Widget& w) used for object@0x16dd46db8 addr_name: 0x16dd46db8 string buffer addr: 0x16dd46db8 Widget(Widget&& rhs) used for object@0x16dd46d90 addr_name: 0x16dd46d90 string buffer addr: 0x16dd46d90 t1_addr:0x16dd46db8 addr of name: 0x16dd46db8 t2_addr:0x16dd46d90 addr of name: 0x16dd46d90 3. RVO优化 Widget() used for object@0x16dd46d68 addr_name: 0x16dd46d68 string buffer addr: 0x16dd46d68 w1_addr:0x16dd46d68 addr of name: 0x16dd46d68 w2: Widget(const Widget& w) used for object@0x16dd46d18 addr_name: 0x16dd46d18 string buffer addr: 0x16dd46d18 Widget(Widget&& rhs) used for object@0x16dd46d40 addr_name: 0x16dd46d40 string buffer addr: 0x16dd46d40 w2_addr:0x16dd46d40 addr of name: 0x16dd46d40
|
需要注意的是,Widget{}
中的:
1 2 3 4
| Widget(Widget&& rhs) noexcept: name(std::move(rhs.name)), ptr(std::move(rhs.ptr)) { cout << "Widget(Widget&& rhs)" << endl; }
|
这个函数会利用一个右值对象来构造新的对象,其中name()
会调用string 的移动构造函数来创建新的 string 对象,这两个对象的地址是不同的,但是 string 指向的缓冲区也就是字符串存储地址是相同的,这点需要注意。
string 的移动构造函数:
1 2 3
| MyString(MyString&& other) noexcept : data(other.data) { other.data = nullptr; }
|
但是如果仔细观察,会发现 string.data()
并不会一样,尽管是通过移动构造的,这是因为SSO(小字符串优化)。SSO 的具体阈值取决于 std::string
的实现,通常在 15 到 24 个字符之间。如果字符串长度低于此阈值,字符串内容将存储在对象本身的内部缓冲区中;超过此阈值,则使用动态内存分配。因为事实是,移动构造在某些情况下,并不会比复制构造更高效。
当我们将上述示例的字符变长时:
1 2 3 4 5 6 7 8 9 10 11
| class Widget { std::string name = "SantaClausSantaClausSantaClausSantaClausSantaClausSantaClausSantaClausSantaClaus"; ... } int main() { ... ... Widget w; w.setName("SantaClausSantaClausSantaClausSantaClaus"); ... }
|
编译运行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ./main 1. 针对右值引用实施std::move,针对万能引用实施std::forward Widget() used for object@0x16f89adf0 addr_name: 0x16f89adf0 string buffer addr: 0x1286066c0 w_addr:0x16f89adf0 addr of name: 0x1286066c0 2. 按值返回时 Widget(const Widget& w) used for object@0x16f89adb8 addr_name: 0x16f89adb8 string buffer addr: 0x128606720 Widget(Widget&& rhs) used for object@0x16f89ad90 addr_name: 0x16f89ad90 string buffer addr: 0x1286066c0 t1_addr:0x16f89adb8 addr of name: 0x128606720 t2_addr:0x16f89ad90 addr of name: 0x1286066c0 3. RVO优化 Widget() used for object@0x16f89ad68 addr_name: 0x16f89ad68 string buffer addr: 0x128606750 w1_addr:0x16f89ad68 addr of name: 0x128606750 w2: Widget(const Widget& w) used for object@0x16f89ad18 addr_name: 0x16f89ad18 string buffer addr: 0x1286067b0 Widget(Widget&& rhs) used for object@0x16f89ad40 addr_name: 0x16f89ad40 string buffer addr: 0x1286067b0 w2_addr:0x16f89ad40 addr of name: 0x1286067b0
|
这里的运行结果,符合所有的预期。
二、完美转发失败的情形
(一)完美转发失败
1. 完美转发不仅转发对象,还转发其类型、左右值特征以及是否带有const或volation等修饰词。而完美转发的失败,主要源于模板类型推导失败或推导的结果是错误的类型。
2. 实例说明:假设转发的目标函数f
,而转发函数为fwd
(天然就应该是泛型)。函数如下:
1 2 3 4 5 6 7 8
| template<typename… Ts> void fwd(Ts&&… params) { f(std::forward<Ts>(params)…); }
f(expression); fwd(expression);
|
(二)五种完美转发失败的情形
1. 使用大括号初始化列表时
(1)失败原因分析:由于转发函数是个模板函数,而在模板类型推导中,大括号初始**不能自动被推导为std::initializer_list**。
(2)解决方案:先用auto声明一个局部变量,再将该局部变量传递给转发函数。
2. 0和NULL用作空指针时
(1)失败原因分析:0或NULL以空指针之名传递给模板时,类型推导的结果是整型,而不是所希望的指针类型。
(2)解决方案:传递nullptr,而非0或NULL。
3. 仅声明static const 整型成员变量,而无其定义时。
(1)失败原因分析:C++中常量一般是进入符号表的,只有对其取地址时才会实际分配内存。 调用 f
函数时,其实参是直接从符号表中取值,此时不会发生问题。但当调用 fwd
时由于其形参是万能引用,而引用本质上是一个可解引用的指针。 因此当传入 fwd
时会要求准备某块内存以供解引用出该变量出来。但因其未定义,也就没有实际的内存空间, 编译时可能失败(取决于编译器和链接器的实现)。
(2)解决方案:在类外定义该成员变量。注意这声变量在声明时一般会先给初始值。因此定义时无需也不能再重复指定初始值。
4. 使用重载函数名或模板函数名时
(1)失败原因分析:由于 fwd
是个模板函数,其形参没有任何关于类型的信息。当传入重载函数名或模板函数(代表许许多多的函数)时,就会导致 fwd
的形参不知绑定到哪个函数上。
(2)解决方案:在调用fwd调用时手动为形参指定类型信息。
5. 转发位域时
(1)失败原因分析:位域是由机器字的若干任意部分组成的(如32位int的第3至5个比特),但这样的实体是无法直接取地址的。而fwd的形参是个引用,本质上就是指针,所以也没有办法创建指向任意比特的指针。
(2)解决方案:制作位域值的副本,并以该副本来调用转发函数。
以下例子分别解释了这些情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| #include <iostream> #include <vector>
using namespace std;
void f(const std::vector<int> &v) { cout << "void f(const std::vector<int> & v)" << endl; }
void f(int x) { cout << "void f(int x)" << endl; }
class Widget { public: static const std::size_t MinVals; };
const std::size_t Widget::MinVals = 10;
int f(int (*pf)(int)) { cout << "int f(int(*pf)(int))" << endl; return 0; }
int processVal(int value) { return 0; } int processVal(int value, int priority) { return 0; }
struct IPv4Header { std::uint32_t version : 4, IHL : 4, DSCP : 6, ECN : 2, totalLength : 16; };
template <typename T> T workOnVal(T param) { return param; }
template <typename... Ts> void fwd(Ts &&...param) { f(std::forward<Ts>(param)...); }
int main() { cout << "-------------------1. 大括号初始化列表---------------------" << endl; f({1, 2, 3}); auto il = {1, 2, 3}; fwd(il);
cout << "-------------------2. 0或NULL用作空指针-------------------" << endl; fwd(NULL); f(nullptr); fwd(nullptr);
cout << "-------3. 仅声明static const的整型成员变量而无定义--------" << endl; f(Widget::MinVals); fwd(Widget:: MinVals);
cout << "-------------4. 使用重载函数名或模板函数名---------------" << endl; f(processVal); using ProcessFuncType = int (*)(int); ProcessFuncType processValPtr = processVal; fwd(processValPtr); fwd(static_cast<ProcessFuncType>(workOnVal));
cout << "----------------------5. 转发位域时---------------------" << endl; IPv4Header ip = {}; f(ip.totalLength); auto length = static_cast<std::uint16_t>(ip.totalLength); fwd(length);
return 0; }
|
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ./main -------------------1. 大括号初始化列表--------------------- void f(const std::vector<int> & v) void f(const std::vector<int> & v) -------------------2. 0或NULL用作空指针------------------- void f(int x) int f(int(*pf)(int)) int f(int(*pf)(int)) -------3. 仅声明static const的整型成员变量而无定义-------- void f(int x) void f(int x) -------------4. 使用重载函数名或模板函数名--------------- int f(int(*pf)(int)) int f(int(*pf)(int)) int f(int(*pf)(int)) ----------------------5. 转发位域时--------------------- void f(int x) void f(int x)
|
这样修改后,就可以实现完美转发了。
三、参考
这里。