示例:
我有以下代码(简化为模型示例,使用的Qt库,下面解释的Qt类行为):
struct Test_impl {
int x;
Test_impl() : x(0) {}
Test_impl(int val) : x(val) {}
};
class Test {
QSharedPointer<Test_impl> m;
public:
Test() : m(new Test_impl()) {}
Test(int val) : m(new Test_impl(val)) {}
void assign(const QVariant& v) {m = v.value<Test>().m; ++m->x;}
~Test(){--m->x;}
};
Q_DECLARE_METATYPE(Test)
QSharedPointer
是一个实现移动语义的智能指针(在文档中省略)。 QVariant
有点类似于std::any
,并且具有模板方法
template<typename T> inline T value() const;
宏Q_DECLARE_METATYPE
允许将Test
类型的值放在QVariant
内。
问题:
行m = v.value<Test>().m;
似乎调用m
返回的临时对象的字段value()
的移动分配。之后,调用Test
析构函数并立即崩溃尝试访问非法地址
通常,我认为问题在于,当移动赋值使对象本身处于一致状态时,包含移动实体的对象的状态出乎意料地&#34;变化。
在我能想到的这个特定示例中,有几种方法可以避免此问题:将Test
析构函数更改为期望null m
,编写模板template<typename T> inline T no_move(T&& tmp) {return tmp;}
,显式创建临时{{1在Test
中添加对象,为assign
添加getter并调用它以强制复制m
(由Jarod42建议); MS Visual Studio允许编写m
,但此代码是非法的。
问题(S):
是否有适当的&#34; (最佳实践?)显式调用复制赋值(或以某种方式正确调用std::swap(m, v.value<Test>().m)
)而不是移动的方式?有没有办法禁止析构函数中使用的类字段的移动语义?为什么移动临时对象成员首先成为默认选项?
答案 0 :(得分:1)
class Test {
QSharedPointer<Test_impl> m;
public:
Test() : m(new Test_impl()) {}
Test(int val) : m(new Test_impl(val)) {}
Test(Test const&)=default;
Test& operator=(Test const&)=default;
void assign(const QVariant& v) {
*this = v.value<Test>();
++m->x;
}
~Test(){--m->x;}
};
您的代码不支持移动构造,因为您希望永远不会为空,并且移动QSharedPointer显然会使其为空。
通过显式=default
复制构造函数(和赋值),我们阻止移动构造函数(和赋值)的自动合成。
当不访问字段时它们是rvalues,而是首先复制它们,我们避免它们清除自己的状态,而周围的类期望它们存储状态。
这可以防止您的崩溃,但不能解决您的设计问题。
一般来说,使用QSharedPointer
假设它不能为空的方式是不好的形式。它是可空类型,可空类型可以为null。
我们可以通过停止这个假设来解决这个问题。或者我们可以编写一个不可为空的共享指针。
template<class T>
struct never_null_shared_ptr:
private QSharedPointer<T>
{
using ptr=QSharedPointer<T>;
template<class...Args>
never_null_shared_ptr(Args&&...args):
ptr(new T(std::forward<Args>(args)...))
{}
never_null_shared_ptr():
ptr(new T())
{}
template<class...Ts>
void replace(Ts&&...ts) {
QSharedPointer<T> tmp(new T(std::forward<Ts>(ts)...));
// paranoid:
if (tmp) ((ptr&)*this) = std::move(tmp);
}
never_null_shared_ptr(never_null_shared_ptr const&)=default;
never_null_shared_ptr& operator=(never_null_shared_ptr const&)=default;
// not never_null_shared_ptr(never_null_shared_ptr&&)
~never_null_shared_ptr()=default;
using ptr::operator*;
using ptr::operator->;
// etc
};
只需using
导入您想支持的QSharedPointer的部分API,并且不允许您重置指针中的值。
现在类型never_null_shared_ptr
强制它的不变量不为空。
请注意,不建议从指针构造never_null_shared_ptr
。相反,你转发构建它。如果你真的需要它,如果传入的指针是null,你应该抛出它,防止构造发生。这也可能需要花哨的SFINAE。
实际上,这种运行时检查比静态检查更糟糕,所以我只是删除了from-pointer构造函数。
给我们:
class Test {
never_null_shared_ptr<Test_impl> m;
public:
Test() : m() {}
Test(int val) : m(val) {}
void assign(const QVariant& v) {m = v.value<Test>().m; ++m->x;}
~Test(){--m->x;}
};
本质上,通过使用不可为空的共享ptr替换可空共享ptr,并将new
调用替换为放置构造,您的代码突然起作用。