C++ 是否支持与 STL 兼容的不可变记录类型?

Does C++ support STL-compatible immutable record types?

许多语言都支持不可变类型——一旦构造了这种类型的值,就无法以任何方式对其进行修改。我不会偏离这些类型的好处,因为这已在别处广泛讨论。 (参见 http://codebetter.com/patricksmacchia/2008/01/13/immutable-types-understand-them-and-use-them/

我想用 C++ 创建一个轻量级的不可变记录类型。一个明显的方法是 const 所有成员。例如:

struct Coordinates {
    const double x;
    const double y;
};

不幸的是,以这种方式声明的类型不支持复制赋值,因此不能与 STL 容器一起使用,这是一个非常致命的缺点。我认为没有任何解决办法,但如果有人能看到,我将不胜感激。

就其价值而言,据我所知,C++ 将两件事混为一谈:a) 是否可以为变量分配新值,b) "values" (=objects) 是否可以构造完成后进行修改。例如,区别更加清晰。 Scala,其中一个有

所以我可以这样写:

val val_mutable = collection.mutable.Map(1 -> "One")
val val_immutable = collection.immutable.Map(1 -> "One")
var var_mutable = collection.mutable.Map(1 -> "One")
var var_immutable = collection.immutable.Map(1 -> "One")

vars 可以反弹指向其他值:

//FAILS: val_immutable = collection.immutable.Map(2 -> "Two")
//FAILS: val_mutable = collection.mutable.Map(2 -> "Two")
var_mutable = collection.mutable.Map(2 -> "Two")
var_immutable = collection.immutable.Map(2 -> "Two")

可以修改可变集合:

val_mutable(2) = "Two"    
//FAILS: val_immutable(2) = "Two"    
var_mutable(2) = "Two"    
//FAILS: var_immutable(2) = "Two"    

在 C++ 中,consting 类型的数据成员使其成为不可变类型,但也无法创建该类型的 "var"。没有后者,任何人都可以找到实现前者的合理轻量级方法吗? [请注意 "lightweight" 部分——特别是,我不想为 struct 的每个元素创建访问器函数,因为这会导致可读性大幅下降。]

请记住,大多数其他语言中的赋值行为类似于 C++ 中的 指针 重新赋值。

因此,C++ 中的 std::shared_ptr<const T> 与其他语言中的不可变类型具有相似的语义。

struct Coordinates {
    double x;
    double y;
};
std::shared_ptr<const Coordinates> p = std::make_shared<const Coordinates>(Coordinates{3.14, 42.0});
p->x *= 2;                                           // error; pointee is immutable
p = std::make_shared<const Coordinates>(Coordinates{6.28, 42.0}); // ok

如果您想模仿您的 var/val,您可以创建 2 个 类 类似于:

template <typename T> class val
{
public:
    val(const T& t) : t(t) {}

    val(const val&) = default;
    val& operator = (const val&) = delete;

    const T* operator->() const { return &t; }
    const T& get() const { return t; }

    T* operator->() { return &t; }
    T& get() { return t; }

private:
    T t;
};

template <typename T> class var
{
public:
    var(const T& t) : t(t) {}

    var(const var&) = default;
    var& operator = (const var&) = default;
    var& operator = (const T&) { this->t = t; return *this; };

    const T* operator->() const { return &t; }
    const T& get() const { return t; }

    T* operator->() { return &t; }
    T& get() { return t; }

private:
    std::remove_const_t<T> t;
};

然后

val<const Coordinate> immutable_val({42, 51});
val<Coordinate> mutable_val({42, 51});
var<const Coordinate> immutable_var({42, 51});
var<Coordinate> mutable_var({42, 51});

//immutable_val->x = 42; // error
mutable_val->x = 42;
//immutable_var->x = 42; // error
mutable_var->x = 42;

//immutable_val = {42, 42}; // error
//mutable_val = {42, 42}; // error
immutable_var = {42, 42};
mutable_var = {42, 42};

//mutable_val = mutable_val; // error
//immutable_val = immutable_val; // error
immutable_var = immutable_var; 
mutable_var = mutable_var;

Demo

您提到您不能将 Coordinates class 与 const 成员编写的 STL 容器一起使用。如何做到这一点的问题在这里得到了回答(不确定):

关于此问题的评论线程中也提出了相同的建议,即让您的实例 const 而不是您的成员。我同意你的评论,因为你必须更改更多代码,所以它不太可靠,但也许你可以像这样使用 typedef:

struct MutableCoordinates {
  double x;
  double y;
};

typedef const MutableCoordinates Coordinates;

以便 Coordinates 对象的用户自动获得不可变版本,除非他们明确要求它?这将抵消您对安全使用结构的冗长性的一些担忧。

当然,您也可以使用 shared_ptr 切换到引用语义,这对于 const 对象来说效果很好,因为不同的引用不会相互混淆,但它附带手动内存分配的开销对于两个 doubles.

的结构来说是相当大的