含const成员变量类的默认移动构造函数行为探究
编译环境:C17标准,g(开启
-Wall -Wextra编译选项)
测试代码
#include <iostream> #include <vector> struct myclass { const int num; const std::vector<int> vec; // Delete copy constructors myclass(const myclass&) = delete; myclass( myclass&) = delete; // Default move constructor myclass( myclass&&) = default; }; int main() { myclass A = {1, {2}}; std::cout << "Before A: " << &A.vec[0] << "\nBefore A[0]: " << A.vec[0] << "\nBefore A int: " << A.num; myclass otherA = std::move(A); std::cout << "\n\nAfter moving:\nOrig A: " << &A.vec[0] << "\nOrig A[0]: " << A.vec[0] << "\nOrig A int: " << A.num; std::cout << "\n\nOther A: " << &otherA.vec[0] << "\nOther A[0]: " << otherA.vec[0] << "\nOther A int: " << otherA.num << std::endl; return 0; }
运行结果
Before A: 0x55fd59295eb0 Before A[0]: 2 Before A int: 1 After moving: Orig A: 0x55fd59295eb0 Orig A[0]: 2 Orig A int: 1 Other A: 0x55fd592962e0 Other A[0]: 2 Other A int: 1
问题描述
当vec为非const成员时,执行std::move后原对象访问vec会触发段错误,新对象与原对象的vec地址相同(符合常规移动语义);但当vec为const成员时,原对象的vec地址不变,访问也正常,新对象的vec是新地址。请问这种情况为何看似正常?是否属于g++下碰巧可行的未定义行为?
原因分析
默认移动构造的行为限制
因为myclass包含const成员(num和const std::vector<int> vec),编译器生成的默认移动构造函数无法对这些const成员执行移动操作——移动语义的核心是转移资源所有权,通常会修改原对象的成员(比如把原vector的指针置空),但const成员不允许被修改。因此默认移动构造实际上退化成了逐成员的拷贝构造。原对象未被修改的本质
对const成员来说,默认移动构造只能复制它们的值:num直接拷贝,const std::vector也只能被拷贝(因为vector的移动构造会修改原对象内部状态,但const vector无法调用非const的移动构造函数)。这就导致原对象的所有成员都保持原样,新对象拥有一份独立的拷贝,所以原对象访问vec仍然正常,地址也不变,新对象的vec是新分配的内存(地址不同)。未定义行为的界定
这里原对象在移动后被访问的行为不属于未定义行为——因为默认移动构造实际上是拷贝,原对象并没有进入“可析构但不可使用”的移动后状态。但要注意:如果你的类设计初衷是使用移动语义,那么包含const成员会直接破坏移动语义,这种默认生成的“伪移动构造”可能和预期不符。非const成员的对比
当vec是非const时,编译器生成的默认移动构造会调用vector的移动构造函数,将原vector的资源转移给新对象,原vector会进入空状态(内部指针置空),此时访问原对象的vec[0]就会触发段错误,这是符合移动语义的正常结果。
内容的提问来源于stack exchange,提问作者TCD

