对象生存期不变性与移动语义


13

很久以前,当我学习C ++时,我就强烈强调C ++的部分意思是就像循环具有“循环不变式”一样,类也具有与对象生命周期相关的不变式-应该是正确的只要物体还活着 应该由构造函数建立并由方法保留的事物。封装/访问控制可帮助您强制执行不变式。RAII是您可以使用此想法做的一件事。

从C ++ 11开始,我们现在有了移动语义。对于支持移动的类,从某个对象移动不会正式终止其生命周期,因为该移动应该使它处于某种“有效”状态。

在设计一个类时,如果将其设计为仅保留该类的不变性直到将其移出之前,是不好的做法吗?或者是说没关系,如果它可以让你让它走得更快。

具体来说,假设我有一个不可复制但可移动的资源类型,如下所示:

class opaque {
  opaque(const opaque &) = delete;

public:
  opaque(opaque &&);

  ...

  void mysterious();
  void mysterious(int);
  void mysterious(std::vector<std::string>);
};

无论出于何种原因,我都需要为此对象创建一个可复制的包装,以便可以在某些现有的调度系统中使用它。

class copyable_opaque {
  std::shared_ptr<opaque> o_;

  copyable_opaque() = delete;
public:
  explicit copyable_opaque(opaque _o)
    : o_(std::make_shared<opaque>(std::move(_o)))
  {}

  void operator()() { o_->mysterious(); }
  void operator()(int i) { o_->mysterious(i); }
  void operator()(std::vector<std::string> v) { o_->mysterious(v); }
};

在此copyable_opaque对象中,构造时建立的类的不变性是成员o_始终指向有效对象,因为没有默认的ctor,并且唯一不是副本ctor的ctor保证了这些。所有operator()方法都假定此不变式成立,并随后将其保留。

但是,如果将对象从移出,o_则将指向无物。在那之后,调用任何方法operator()都将导致UB /崩溃。

如果从不移动对象,则将一直保持不变,直到dtor调用为止。

假设,我写了这个课,几个月后,我虚构的同事经历了UB,因为在某种复杂的功能中,由于某些原因,许多这些对象被拖曳了,他从其中之一移走了,后来称为其中之一。它的方法。显然,这是他一天结束时的错,但是这门课“设计得不好吗?”

想法:

  1. 在C ++中创建僵尸对象通常是不好的形式,如果您触摸它们就会爆炸。
    如果无法构造某些对象,则无法建立不变式,然后从ctor引发异常。如果您无法以某种方法保留不变式,则以某种方式发出错误信号并回滚。对于移出的对象这应该有所不同吗?

  2. 在标头中仅记录“从该对象移出之后,除销毁该对象之外,对其进行任何其他操作都是非法的”(UB)是否足够?

  3. 在每个方法调用中不断断言它是否有效更好?

像这样:

class copyable_opaque {
  std::shared_ptr<opaque> o_;

  copyable_opaque() = delete;
public:
  explicit copyable_opaque(opaque _o)
    : o_(std::make_shared<opaque>(std::move(_o)))
  {}

  void operator()() { assert(o_); o_->mysterious(); }
  void operator()(int i) { assert(o_); o_->mysterious(i); }
  void operator()(std::vector<std::string> v) { assert(o_); o_->mysterious(v); }
};

这些断言并不能从根本上改善行为,并且会导致速度变慢。如果您的项目确实使用“发布版本/调试版本”方案,而不是仅始终使用断言运行,我想这会更具吸引力,因为您无需为发布版本中的检查付费。如果您实际上没有调试版本,那么这似乎就没有吸引力。

  1. 使该类具有可复制性而不是可移动性更好吗?
    这似乎也很糟糕,并且会导致性能下降,但是它可以直接解决“不变”问题。

您认为这里的相关“最佳做法”是什么?



Answers:


20

在C ++中创建僵尸对象通常是不好的形式,如果您触摸它们就会爆炸。

但这不是你在做什么。您正在创建一个“僵尸对象”,如果错误触摸它将会爆炸。最终,这与任何其他基于状态的前提条件没有什么不同。

考虑以下功能:

void func(std::vector<int> &v)
{
  v[0] = 5;
}

这个功能安全吗?没有; 用户可以传递一个空的 vector。因此,该函数具有事实上的前提,v其中至少包含一个元素。如果不是,那么您在致电时会得到UB func

因此,此功能不是“安全的”。但这并不意味着它已经坏了。仅当使用它的代码违反前提条件时,它才会被破坏。也许func是一个静态函数,用作实现其他功能的助手。以这种方式进行本地化,没有人会以违反其先决条件的方式来调用它。

许多函数,无论是名称空间作用域还是类成员,都对它们所操作的值的状态有期望。如果不满足这些先决条件,则功能将失败,通常对于UB。

C ++标准库定义了一个“有效但未指定”的规则。这表示除非标准另有说明,否则从其移出的每个对象都是有效的(它是该类型的合法对象),但是未指定该对象的特定状态。搬迁者有多少个元素vector?没有说

这意味着您不能调用任何具有任何先决条件的函数。vector::operator[]前提是vector至少具有一个元素。由于您不知道的状态vector,因此无法调用它。这比func没有先验证vector不为空的调用更好。

但这也意味着没有先决条件的功能很好。这是完全合法的C ++ 11代码:

vector<int> v1 = {1, 2, 3, 4, 5};
vector<int> v2{std::move(v1)};
v1.assign({6, 7, 8, 9, 10});

vector::assign没有任何先决条件。它可以与任何有效vector对象一起使用,即使是已移走的对象也是如此。

因此,您不会创建损坏的对象。您正在创建状态未知的对象。

如果无法构造某些对象,则无法建立不变式,然后从ctor引发异常。如果您无法以某种方法保留不变式,则以某种方式发出错误信号并回滚。对于移出的对象这应该有所不同吗?

通常认为从move构造函数抛出异常是不礼貌的。如果移动拥有内存的对象,那么您将转移该内存的所有权。而且通常不涉及任何可能引发的问题。

可悲的是,由于各种原因我们不能强制执行此操作。我们必须接受掷球移动是可能的。

还应注意的是,您不必遵循“有效但尚未指定”的语言。这就是C ++标准库所说的默认情况下标准类型移动的工作方式。某些标准库类型具有更严格的保证。例如,对于unique_ptr移出unique_ptr实例的状态非常清楚:它等于nullptr

因此,您可以根据需要选择提供更强有力的保证。

请记住:运动是一种性能优化,其中之一是通常正在对那些对象做了关于被破坏。考虑以下代码:

vector<int> func()
{
  vector<int> v;
  //fill up `v`.
  return v;
}

这将移到v返回值中(假设编译器不会忽略它)。而且有没有办法来参考v,此举完成后。因此,您要做的任何使工作v变得有用的工作都是没有意义的。

在大多数代码中,使用从对象移出的实例的可能性很小。

在标头中仅记录“从该对象移出之后,除销毁该对象之外,对其进行任何其他操作都是非法的”(UB)是否足够?

在每个方法调用中不断断言它是否有效更好?

有前提条件的全部目的是不要检查这些东西。operator[]前提是vector拥有具有给定索引的元素。如果您尝试访问超出的大小,则会得到UB vectorvector::at 没有这样的前提;如果vector没有这样的值,它将明确地引发异常。

由于性能原因存在先决条件。它们是为了让您不必检查呼叫者本可以验证的内容。每次调用都v[0]不必检查是否v为空;只有第一个。

使该类具有可复制性而不是可移动性更好吗?

不能。事实上,一个类永远都不能“可复制但不能移动”。如果可以复制它,那么应该可以通过调用复制构造函数来移动它。如果您声明用户定义的副本构造函数但不声明move构造函数,则这是C ++ 11的标准行为。如果您不想实现特殊的移动语义,则应采用此行为。

存在移动语义来解决一个非常具体的问题:处理具有大量资源的对象,而复制成本过高或毫无意义(例如:文件句柄)。如果您的对象不符合条件,则复制和移动对您来说都是相同的。


5
真好 +1。我会指出:“有先决条件的全部目的是不要检查这类事情。” -我认为这不适合断言。断言是恕我直言,一种很好的且有效的工具,可以检查先决条件(至少在大多数情况下是这样)
Martin Ba

3
可以通过意识到移动ctor可以使源对象处于任何状态(包括与新对象相同的状态)来澄清复制/移动的混乱,这意味着可能的结果是复制ctor的结果的超集。
MSalters '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.