如何避免为类成员移动语义?

时间:2017-04-03 08:33:53

标签: c++ c++11 move-semantics

示例:
我有以下代码(简化为模型示例,使用的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))而不是移动的方式?有没有办法禁止析构函数中使用的类字段的移动语义?为什么移动临时对象成员首先成为默认选项?

1 个答案:

答案 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调用替换为放置构造,您的代码突然起作用。