C++移动语义下临时对象与函数返回行为的技术咨询
近期复习C++并重新学习移动语义,编写了如下示例程序并得到对应输出,但对其中步骤3、4、5的行为存在疑惑。
示例程序
#include <iostream> #include <iomanip> int sepI = 0; void printSep(const char* msg) { std::cout << std::setw(40) << std::setfill('>') << "" << std::setfill(' ') << std::setw(2) << std::setiosflags(std::ios::right) << sepI++ << ": " << std::resetiosflags(std::ios::right) << std::setiosflags(std::ios::left) << msg << std::resetiosflags(std::ios::left) << std::endl; } void printHook(const char* msg, void* p) { std::cout << std::setw(25) << std::setiosflags(std::ios::left) << msg << std::resetiosflags(std::ios::left) << std::setw(15) << std::setiosflags(std::ios::right) << p << std::resetiosflags(std::ios::right) << std::endl; } class MyT { public: MyT() { printHook("constructor call", this); } MyT(MyT& t) { printHook("copy constructor call", this); } MyT(MyT&& t) { printHook("move constructor call", this); } ~MyT() { printHook("destructor call", this); v = 0; } MyT& operator=(MyT& t) { printHook("copy assigment call", this); return *this; } MyT& operator=(MyT&& t) { printHook("move assigment call", this); return *this; } int v = 1; }; MyT fn() { return MyT(); } int main() { printSep("a"); MyT a; printSep("MyT&& v1 = std::move(a)"); MyT&& v1 = std::move(a); std::cout << v1.v << " " << &v1 << std::endl; printSep("MyT&& v2 = MyT()"); MyT&& v2 = MyT(); std::cout << v2.v << " " << &v2 << std::endl; printSep("MyT&& v3 = std::move(MyT())"); MyT&& v3 = std::move(MyT()); std::cout << v3.v << " " << &v3 << std::endl; printSep("MyT&& v4 = fn()"); MyT&& v4 = fn(); std::cout << v4.v << " " << &v4 << std::endl; printSep("MyT&& v5 = std::move(fn())"); MyT&& v5 = std::move(fn()); std::cout << v5.v << " " << &v5 << std::endl; printSep("end"); return 0; }
程序输出
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 0: a OK. constructor call 0x7ffe656eeae0 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 1: MyT&& v1 = std::move(a) OK. Makes sense. 1 0x7ffe656eeae0 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 2: MyT&& v2 = MyT() OK. Makes sense. constructor call 0x7ffe656eeae8 1 0x7ffe656eeae8 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 3: MyT&& v3 = std::move(MyT()) NOT OK. constructor call 0x7ffe656eeaec destructor call 0x7ffe656eeaec 1 0x7ffe656eeaec >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 4: MyT&& v4 = fn() NOT OK. constructor call 0x7ffe656eeaf0 1 0x7ffe656eeaf0 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 5: MyT&& v5 = std::move(fn()) NOT OK. constructor call 0x7ffe656eeaf4 destructor call 0x7ffe656eeaf4 1 0x7ffe656eeaf4 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 6: end destructor call 0x7ffe656eeaf4 destructor call 0x7ffe656eeaf0 destructor call 0x7ffe656eeae8 destructor call 0x7ffe656eeae0
疑问与解答
步骤3相关解答
执行过程:
- 首先创建临时对象
MyT(),调用默认构造函数; - 调用
std::move()将该临时对象转换为右值引用(注意std::move仅做类型转换,不触发任何构造/移动操作); - 用转换后的右值引用初始化
v3,但此时编译器无法触发临时对象生命周期延长规则,临时对象在当前表达式结束后立即被销毁,调用析构函数; - 后续
v3实际绑定的是已销毁对象的内存地址,打印v3.v属于访问已销毁的栈内存。
- 首先创建临时对象
与步骤2的差异原因:
步骤2的MyT&& v2 = MyT()触发了C++的临时对象生命周期延长规则:当右值引用直接绑定到临时对象时,临时对象的生命周期会被延长至与引用变量v2一致。而步骤3中,std::move(MyT())返回的是一个右值引用,v3绑定的是这个引用的结果而非原始临时对象,因此生命周期延长规则不生效,临时对象会立即销毁。未触发段错误的原因:
访问已销毁对象的内存属于未定义行为,未定义行为不保证一定会触发段错误。此处地址属于栈内存,对象销毁后该内存未被立即覆盖或回收,因此读取v3.v时刚好能读到旧值,但这种行为完全不可靠,更换编译选项或运行环境就可能出现崩溃。
步骤4和5相关解答
fn()返回实例的内存位置:
由于编译时开启了-O3优化,编译器触发了返回值优化(RVO),fn()中创建的MyT()临时对象会直接在main()函数的栈帧中分配内存,即直接构造在v4绑定的栈地址上,完全避免了拷贝/移动操作。两次调用地址不同且无动态分配的原因:
两次调用fn()时,编译器会在main()的栈帧中为每次返回的对象分配独立的栈空间(栈内存按调用顺序依次分配),因此得到不同的地址。这些对象都分配在栈上,无需new/malloc等动态分配操作。而步骤5中std::move(fn())破坏了生命周期延长规则,导致fn()返回的临时对象在表达式结束后立即销毁,v5绑定的是已销毁的对象地址。
内容的提问来源于stack exchange,提问作者Artsiom Miksiuk

