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

时间:2016-09-12 22:37:22

标签: c++ const immutability mutable

许多语言都支持不可变类型 - 一旦构造了这种类型的值,就无法以任何方式对其进行修改。我不会偏离这些类型的好处,因为这已在其他地方广泛讨论过。 (参见例如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 vs varvar可以绑定一个新值,val不能
  • 不可变和可变对象,特别是集合

所以我可以写下以下内容:

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")

var可以反弹以指向其他值:

//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 ++中,const类型的数据成员使其成为不可变类型,但也无法创建该类型的“var”。没有后者,任何人都可以看到实现前者的合理轻量级方式吗? [请注意“轻量级”部分 - 特别是,我不想为struct的每个元素创建一个访问器函数,因为这会导致可读性大幅下降。]

3 个答案:

答案 0 :(得分:6)

请记住,大多数其他语言中的赋值行为类似于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

答案 1 :(得分:1)

如果您想模仿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

答案 2 :(得分:1)

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

https://stackoverflow.com/a/3372966/393816

同样的建议,即使你的实例const而不是你的成员,也在这个问题的评论主题中提出。我同意你的评论,因为你必须更改更多的代码它不太可靠,但也许你可以使用类似的typedef:

struct MutableCoordinates {
  double x;
  double y;
};

typedef const MutableCoordinates Coordinates;

那么Coordinates个对象的用户会自动获取不可变版本,除非他们明确地这样做了吗?这将抵消您对安全使用结构的详细程度的担忧。

当然,您也可以使用shared_ptr切换到引用语义,这对于const个对象非常有效,因为不同的引用不会相互混淆,但它附带了手动内存分配的开销对于两个double s的结构来说相当过分。