我有一个带有const成员的类,该成员既需要move构造函数又需要赋值。
我通过以下方式实现它:
struct C
{
const int i;
C(int i) : i{i} {}
C(C && other) noexcept: i{other.i} {}
C & operator=(C && other) noexcept
{
//Do not move from ourselves or all hell will break loose
if (this == &other)
return *this;
//Call our own destructor to clean-up before moving
this->~C();
//Use our own move constructor to do the actual work
new(this) C {std::move(other)};
return *this;
}
//Other stuff here (including the destructor)....
}
这可以按预期进行编译和工作。
问题是这是实现这种移动分配的正常方法,还是没有那么人为的方法?
答案 0 :(得分:4)
不幸的是,这是未定义的行为。您不能像这样覆盖const对象,然后再使用相同的名称引用它。 [basic.life]/8
对此进行了介绍如果在对象的生存期结束之后且在重新使用或释放该对象所占用的存储之前,在原始对象所占用的存储位置上创建了一个指向原始对象的指针的新对象,引用原始对象的引用或原始对象的名称将自动引用新对象,并且一旦新对象的生存期开始,便可以用来操作新对象,如果:[... ]
- 原始对象的类型不是const限定的,并且,如果是类类型,则不包含任何类型为const限定的非静态数据成员或引用类型,[...]
简单的解决方法是将i
设为非常量和私有,而只需使用吸气剂即可阻止任何修改。
答案 1 :(得分:1)
经过研究,我得出以下结论。
有相关问题:
相关信息位于:
为我澄清的是https://en.cppreference.com/w/cpp/utility/launder
中的示例struct X {const int i; };
X * p = new X{0};
X * np = new (p) X{1};
这将导致未定义的行为:
const int i = p->i
但以下内容有效:
const int i = np->i;
对于我最初的问题,需要对移动分配进行修改:
struct C
{
const int i;
C() : i{} {}
C(C && other) noexcept: i{other.i} {}
C & operator=(C && other) noexcept
{
if (this == &other) return *this;
this->~C(); //Ok only if ~C is trivial
return *(new(this) C {std::move(other)});
}
}
C a;
C b;
b = std::move(a);
//Undefined behavior!
const int i = b.i;
这将按预期工作,但由于以下原因会导致未定义的行为。
调用析构函数时,对象的生存期结束。接下来,可以安全地调用move构造函数。但是,编译器可以随时假设b
的内容从不发生变化。因此,通过使用移动分配,我们存在导致不确定行为的矛盾。
另一方面,尽管从new位置返回的值与this
相同,但是当编译器通过返回的指针/引用执行访问时,它不得假定与该对象有关的任何信息。
鉴于C& C::operator=(C&&)
返回放置新结果的结果,以下应该是有效的(但并不真正有用)。
const int i = (b = std::move(a)).i;
感谢@NathanOliver,他的回答一直都是正确的答案,也感谢他和@SanderDeDycker与我一起打大脑乒乓球。