【问题标题】:How to avoid move semantics for class members?如何避免类成员的移动语义?
【发布时间】:2023-03-16 04:40:01
【问题描述】:

示例:
我有以下代码(简化为模型示例,使用 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&lt;Test&gt;().m; 似乎调用了value() 返回的临时对象的字段m 的移动赋值。之后,Test 析构函数被调用,并在试图访问非法地址时立即崩溃。
一般来说,我看到的问题是,虽然移动分配使对象本身处于一致状态,但包含移动实体的对象的状态“意外”发生了变化。

有几种方法可以避免这个特定示例中的问题,我能想到:将 Test 析构函数更改为预期 null m,编写模板 template&lt;typename T&gt; inline T no_move(T&amp;&amp; tmp) {return tmp;},在 Test 中显式创建临时对象 assign,为m 添加getter 并调用它来强制复制m(由Jarod42 建议); MS Visual Studio 允许写std::swap(m, v.value&lt;Test&gt;().m),但是这个代码是非法的。

问题:
是否有一种“正确的”(最佳实践?)方式来显式调用复制分配(或以某种方式正确调用swap)而不是移动?有没有办法禁止析构函数中使用的类字段的移动语义?为什么首先将移动临时对象成员设为默认选项?

【问题讨论】:

  • 将移动构造函数声明为已删除,即Test(Test&amp;&amp;) = delete;。但是修复你的班级以允许在没有 UB 的情况下移动会更好。
  • (a) 我不认为 Test 的 move 构造函数是这里的问题(实施它以强制复制没有任何改变,并且描述的解决方案(看似)工作而不会弄乱它)。 (b) 删除它会破坏Q_DECLARE_METATYPE(因为QVariant 按值返回对象)。
  • 你也可以使用 getter 来避免移动。
  • 猜测value&lt;Test&gt;() 会返回一个副本,但Test 类没有复制构造函数。那么那里会发生什么?
  • 检查析构函数中的共享指针。移动后它可以变成一个nullptr。如果确实如此,请不要取消引用它。 OTOH,如果您想要一个准确的计数器,您确实需要定义自己的复制 ctor 和复制分配,并在那里增加它。

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


【解决方案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 显然会使它为空。

通过明确地=defaulting 复制构造函数(和赋值),我们阻止了移动构造函数(和赋值)的自动合成。

通过在字段是右值时访问它们,而是首先复制它们,我们避免它们清除自己的状态,而周围的类希望它们存储状态。

这应该可以防止您的崩溃,但不能解决您的设计问题。


一般来说,以假定它不能为空的方式使用QSharedPointer 是不好的形式。是可空类型,可空类型可以为空。

我们可以通过停止这个假设来解决这个问题。或者我们可以写一个不可为空的共享指针。

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。相反,您向前构造它。如果你真的需要它,如果传入的指针为空,你应该让它抛出,防止构造发生。这也可能需要花哨的 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 调用替换为放置构造,您编写的代码突然可以工作了。

【讨论】:

  • "这应该可以防止你的崩溃,(...)" - 它没有(我已经检查过)而且我不明白为什么会这样。 m 的临时对象仍然被移动,留下空指针。另外,我认为您对我的前两个问题的回答是“不要设计您的课程,以便您首先需要防止移动”?
  • @Abstraction 啊,发现问题并修复它。请注意,第二个答案没有这个问题;因为我将不变量折叠到智能指针中,所以在包含类中的误用不能使其为空。第二个答案仍然是最好的:如果您必须有一个不可为空的指针,请使用类型系统强制它。如果失败,运行时使用断言和抛出强制它。只有在失败时才将执行与业务逻辑混合在一起。或者让业务逻辑不假定它是不可为空的。
猜你喜欢
  • 2016-07-23
  • 1970-01-01
  • 1970-01-01
  • 2012-01-05
  • 2013-10-08
  • 1970-01-01
  • 2017-02-22
  • 2018-05-18
  • 1970-01-01
相关资源
最近更新 更多