【问题标题】:Idiomatic Way to declare C++ Immutable Classes声明 C++ 不可变类的惯用方式
【发布时间】:2015-01-07 15:00:50
【问题描述】:

所以我有一些相当广泛的功能代码,其中主要数据类型是不可变的结构/类。我一直在声明不变性的方式是通过将成员变量和任何方法设为 const 来“实际上是不可变的”。

struct RockSolid {
   const float x;
   const float y;
   float MakeHarderConcrete() const { return x + y; }
}

这实际上是 C++ 中“我们应该这样做”的方式吗?还是有更好的办法?

【问题讨论】:

  • 这在很大程度上取决于您想要的不可变概念。考虑 Java 和 C# 字符串。它们是不可变但可分配的。
  • const 数据成员的优势在于,如果您忘记初始化此类基本类型的成员,则会出现错误。一个缺点是您不能分配给该类型的变量。
  • 一个主要的缺点是,如果它的成员是 const,您就不能将数据移出对象,这就是为什么我宁愿将成员设为私有并且只提供 const getter 和方法的原因正如 oikosdev 建议的那样

标签: c++ c++11 functional-programming immutability const-correctness


【解决方案1】:

我假设您的目标是真正的不变性——每个对象在构造时都不能被修改。您不能将一个对象分配给另一个对象。

您设计的最大缺点是它与移动语义不兼容,这会使返回此类对象的函数更加实用。

举个例子:

struct RockSolidLayers {
  const std::vector<RockSolid> layers;
};

我们可以创建其中之一,但如果我们有创建它的函数:

RockSolidLayers make_layers();

它必须(逻辑上)将其内容复制到返回值,或者使用return {} 语法直接构造它。在外面,你要么必须做:

RockSolidLayers&& layers = make_layers();

或再次(逻辑上)复制构造。无法移动构造会妨碍许多简单的方法来获得最佳代码。

现在,这两个复制构造都被省略了,但更一般的情况是——您不能将数据从一个命名对象移动到另一个,因为 C++ 没有“销毁和move" 操作,既可以将变量带出范围,也可以使用它来构造其他内容。

而在销毁之前 C++ 会隐式移动您的对象(例如 return local_variable;)的情况会被您的 const 数据成员阻止。

在围绕不可变数据设计的语言中,它会知道它可以“移动”您的数据,尽管其(逻辑)不可变。

解决此问题的一种方法是使用堆,并将数据存储在std::shared_ptr&lt;const Foo&gt; 中。现在constness 不在成员数据中,而是在变量中。您还可以只为返回上述shared_ptr&lt;const Foo&gt; 的每种类型公开工厂函数,从而阻止其他构造。

此类对象可以组合,Bar 存储 std::shared_ptr&lt;const Foo&gt; 成员。

返回std::shared_ptr&lt;const X&gt; 的函数可以有效地移动数据,而局部变量可以在完成后将其状态转移到另一个函数中,而不会弄乱“真实”数据。

对于一项相关技术,在约束较少的 C++ 中采用这种shared_ptr&lt;const X&gt; 并将它们存储在假装它们不是不可变的包装类型中是惯用的。当您执行变异操作时,shared_ptr&lt;const X&gt; 被克隆和修改,然后存储。优化“知道”shared_ptr&lt;const X&gt;“真的”是shared_ptr&lt;X&gt;(注意:确保工厂函数将shared_ptr&lt;X&gt; 转换为shared_ptr&lt;const X&gt;,否则这实际上不是真的),并且当use_count() is 1 而是丢弃const 并直接修改它。这是一种被称为“写时复制”的技术的实现。

现在随着 C++ 的发展,省略的机会越来越多。甚至 C++23 也会有更高级的省略。省略是指数据没有在逻辑上移动或复制,而只是有两个不同的名称,一个在函数内部,一个在函数外部。

依赖它仍然很尴尬。

【讨论】:

  • std::unique_ptr&lt;const X&gt;,可以通过std::move()按值返回。
【解决方案2】:

您提出的方式非常好,除非在您的代码中您需要对 RockSolid 变量进行赋值,如下所示:

RockSolid a(0,1);
RockSolid b(0,1);
a = b;

这不起作用,因为复制赋值运算符会被编译器删除。

因此,另一种选择是将结构重写为具有私有数据成员的类,并且只有公共 const 函数。

class RockSolid {
  private:
    float x;
    float y;

  public:
    RockSolid(float _x, float _y) : x(_x), y(_y) {
    }
    float MakeHarderConcrete() const { return x + y; }
    float getX() const { return x; }
    float getY() const { return y; }
 }

这样,您的 RockSolid 对象是(伪)不可变对象,但您仍然可以进行分配。

【讨论】:

  • 我特别想要的语义,至少对于这个项目而言,与 Haskell 等“900 磅函数式语言”中的语义相同,其中任何新状态都必须显式(类型)构造。
  • @MartinMoene :那个“成语”仍然构建不可复制分配和不可移动分配的值对象(或依赖于堆)。 oikosdev 的解决方案不会因为这个缺点而受到影响,并且不需要(配套)样板。它只是有效......为什么要把一切都复杂化?
  • 为什么建议使用class?即使structprivate 成员变量也可以!
  • structclass 是同一个东西,但一个默认为public:,一个默认为private:。建议使用class 没有区别。其他语言,如 C# 或 D,存在语义差异。
猜你喜欢
  • 2012-01-11
  • 2018-05-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-11-30
  • 2013-07-03
  • 2016-10-12
相关资源
最近更新 更多