Idiomatic way to create an immutable and efficient class in C++

lac*_*chy 37 c++ const immutability const-cast

I am looking to do something like this (C#).

public final class ImmutableClass {
    public readonly int i;
    public readonly OtherImmutableClass o;
    public readonly ReadOnlyCollection<OtherImmutableClass> r;

    public ImmutableClass(int i, OtherImmutableClass o,
        ReadOnlyCollection<OtherImmutableClass> r) : i(i), o(o), r(r) {}
}
Run Code Online (Sandbox Code Playgroud)

The potential solutions and their associated problems I've encountered are:

1. Using const for the class members, but this means the default copy assignment operator is deleted.

Solution 1:

struct OtherImmutableObject {
    const int i1;
    const int i2;

    OtherImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
}
Run Code Online (Sandbox Code Playgroud)

Problem 1:

OtherImmutableObject o1(1,2);
OtherImmutableObject o2(2,3);
o1 = o2; // error: use of deleted function 'OtherImmutableObject& OtherImmutableObject::operator=(const OtherImmutableObject&)`
Run Code Online (Sandbox Code Playgroud)

EDIT: This is important as I would like to store immutable objects in a std::vector but receive error: use of deleted function 'OtherImmutableObject& OtherImmutableObject::operator=(OtherImmutableObject&&)

2. Using get methods and returning values, but this means that large objects would have to be copied which is an inefficiency I'd like to know how to avoid. This thread suggests the get solution, but it doesn't address how to handle passing non-primitive objects without copying the original object.

Solution 2:

class OtherImmutableObject {
    int i1;
    int i2;
public:
    OtherImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
    int GetI1() { return i1; }
    int GetI2() { return i2; }
}

class ImmutableObject {
    int i1;
    OtherImmutableObject o;
    std::vector<OtherImmutableObject> v;
public:
    ImmutableObject(int i1, OtherImmutableObject o,
        std::vector<OtherImmutableObject> v) : i1(i1), o(o), v(v) {}
    int GetI1() { return i1; }
    OtherImmutableObject GetO() { return o; } // Copies a value that should be immutable and therefore able to be safely used elsewhere.
    std::vector<OtherImmutableObject> GetV() { return v; } // Copies the vector.
}
Run Code Online (Sandbox Code Playgroud)

Problem 2: The unnecessary copies are inefficient.

3. Using get methods and returning const references or const pointers but this could leave hanging references or pointers. This thread talks about the dangers of references going out of scope from function returns.

Solution 3:

class OtherImmutableObject {
    int i1;
    int i2;
public:
    OtherImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
    int GetI1() { return i1; }
    int GetI2() { return i2; }
}

class ImmutableObject {
    int i1;
    OtherImmutableObject o;
    std::vector<OtherImmutableObject> v;
public:
    ImmutableObject(int i1, OtherImmutableObject o,
        std::vector<OtherImmutableObject> v) : i1(i1), o(o), v(v) {}
    int GetI1() { return i1; }
    const OtherImmutableObject& GetO() { return o; }
    const std::vector<OtherImmutableObject>& GetV() { return v; }
}
Run Code Online (Sandbox Code Playgroud)

Problem 3:

ImmutableObject immutable_object(1,o,v);
// elsewhere in code...
OtherImmutableObject& other_immutable_object = immutable_object.GetO();
// Somewhere else immutable_object goes out of scope, but not other_immutable_object
// ...and then...
other_immutable_object.GetI1();
// The previous line is undefined behaviour as immutable_object.o will have been deleted with immutable_object going out of scope
Run Code Online (Sandbox Code Playgroud)

Undefined behaviour can occur due to returning a reference from any of the Get methods.

lub*_*bgr 33

  1. 您确实想要某种类型加值语义的不可变对象(因为您关心运行时性能并希望避免堆)。只需struct使用所有数据成员定义一个即可public

    struct Immutable {
        const std::string str;
        const int i;
    };
    
    Run Code Online (Sandbox Code Playgroud)

    您可以实例化和复制它们,读取数据成员,仅此而已。从另一个副本的右值引用中移动构建实例。

    Immutable obj1{"...", 42};
    Immutable obj2 = obj1;
    Immutable obj3 = std::move(obj1); // Copies, too
    
    obj3 = obj2; // Error, cannot assign
    
    Run Code Online (Sandbox Code Playgroud)

    这样,您确实可以确保对类的每次使用都尊重不变性(假设没有人做坏事const_cast)。可以通过自由函数提供其他功能,没有必要将成员函数添加到数据成员的只读聚合中。

  2. 您想要1.仍然具有值语义,但是稍微放松一些(这样对象就不再是真正不变的了),并且您还担心为了运行时性能而需要移动构造。无法绕过private数据成员和getter成员函数:

    class Immutable {
       public:
          Immutable(std::string str, int i) : str{std::move(str)}, i{i} {}
    
          const std::string& getStr() const { return str; }
          int getI() const { return i; }
    
       private:
          std::string str;
          int i;
    };
    
    Run Code Online (Sandbox Code Playgroud)

    用法是相同的,但是move构造确实可以移动。

    Immutable obj1{"...", 42};
    Immutable obj2 = obj1;
    Immutable obj3 = std::move(obj1); // Ok, does move-construct members
    
    Run Code Online (Sandbox Code Playgroud)

    现在是否要允许分配是您的控制。刚= delete赋值运算符,如果你不想要它,与编译器生成的一个,否则去或实现自己的。

    obj3 = obj2; // Ok if not manually disabled
    
    Run Code Online (Sandbox Code Playgroud)
  3. 您不在乎值语义和/或原子引用计数增量在您的方案中是可以的。使用@NathanOliver的答案中描述的解决方案。

  • @NathanOliver您无法防御C ++中坚定的恶意同伴程序员。他们总是可以调用一些不良的魔法。该问题需要在键盘的另一侧解决。 (5认同)
  • 2的问题是,对从吸气剂返回的引用进行const_cast合法化,并对“ immutable”对象进行变异是合法的。 (2认同)
  • *“`Immutable obj3 = std :: move(obj1); //好的,可以移动构造成员吗?” *。问题是obj1已经被突变了。 (2认同)

Nat*_*ica 22

您基本上可以通过使用std::unique_ptr或获得所需的内容std::shared_ptr。如果您只想要这些对象之一,但允许其移动,则可以使用std::unique_ptr。如果要允许所有具有相同值的多个对象(“副本”),则可以使用std::shared_Ptr。使用别名来缩短名称并提供工厂功能,它将变得很轻松。那将使您的代码看起来像:

class ImmutableClassImpl {
public: 
    const int i;
    const OtherImmutableClass o;
    const ReadOnlyCollection<OtherImmutableClass> r;

    public ImmutableClassImpl(int i, OtherImmutableClass o, 
        ReadOnlyCollection<OtherImmutableClass> r) : i(i), o(o), r(r) {}
}

using Immutable = std::unique_ptr<ImmutableClassImpl>;

template<typename... Args>
Immutable make_immutable(Args&&... args)
{
    return std::make_unique<ImmutableClassImpl>(std::forward<Args>(args)...);
}

int main()
{
    auto first = make_immutable(...);
    // first points to a unique object now
    // can be accessed like
    std::cout << first->i;
    auto second = make_immutable(...);
    // now we have another object that is separate from first
    // we can't do
    // second = first;
    // but we can transfer like
    second = std::move(first);
    // which leaves first in an empty state where you can give it a new object to point to
}
Run Code Online (Sandbox Code Playgroud)

如果将代码更改为使用,shared_ptr则可以执行

second = first;
Run Code Online (Sandbox Code Playgroud)

然后两个对象都指向同一个对象,但是任何一个都不能对其进行修改。


ben*_*nrg 12

由于C ++具有通用的值语义,因此不能将C ++中的不变性直接与大多数其他流行语言中的不变性进行比较。您必须弄清楚“不变”的含义。

您希望能够为类型的变量分配新值OtherImmutableObject。这是有道理的,因为您可以使用ImmutableObjectC#中的类型变量来做到这一点。

在这种情况下,获得所需语义的最简单方法是

struct OtherImmutableObject {
    int i1;
    int i2;
};
Run Code Online (Sandbox Code Playgroud)

看起来这是可变的。毕竟,你可以写

OtherImmutableObject x{1, 2};
x.i1 = 3;
Run Code Online (Sandbox Code Playgroud)

但是第二行的效果(忽略并发...)与

x = OtherImmutableObject{3, x.i2};
Run Code Online (Sandbox Code Playgroud)

因此,如果要允许分配给类型变量,OtherImmutableObject则不允许直接分配给成员没有意义,因为它不提供任何其他语义保证;它所做的就是使同一抽象操作的代码变慢。(在这种情况下,大多数优化的编译器可能会为两个表达式生成相同的代码,但是如果其中一个成员是a,则std::string它们可能不够聪明来做到这一点。)

请注意,这是在C语言基本上每个标准类型的行为++,其中包括intstd::complexstd::string等,他们都是可变的,即可以分配新的值给他们,在这个意义上,你唯一可以做的所有不可变(抽象)更改它们是为它们分配新值,就像C#中的不变引用类型一样。

如果您不希望使用这种语义,那么唯一的选择就是禁止分配。我建议通过将变量声明为而const不是将所有类型的成员声明为来做到const这一点,因为它为您提供了更多使用类的选择。例如,您可以创建该类的初始可变实例,在其中创建一个值,然后通过仅使用对它的const引用来“冻结”它,就像将a转换StringBuilder为a一样string,但没有复制它的开销。

(声明所有成员为的一个可能原因const可能是在某些情况下允许进行更好的优化。例如,如果一个函数获得OtherImmutableObject const&,并且编译器看不到调用站点,则缓存该对象是不安全的跨其他未知代码调用的成员值,因为基础对象可能没有const限定符。但是,如果声明了实际成员const,则我认为缓存值是安全的。)


Lau*_*ZZA 5

要回答您的问题,您不会在C ++中创建不可变的数据结构,因为const对整个对象的引用都可以解决问题。const_casts 的存在使违反规则的行为可见。

如果我可以参考凯夫林·汉尼(Kevlin Henney)的“在同步象限之外思考”,则有两个关于数据的问题要问:

  • 结构是不可变的还是可变的?
  • 是共享还是不共享?

这些问题可以安排成一个漂亮的2x2表格,包含4个象限。在并发上下文中,只有一个象限需要同步:共享的可变数据。

确实,不可变数据不需要同步,因为您无法对其进行写入,并且并发读取很好。未共享的数据不需要同步,因为只有数据的所有者才能对其进行写入或读取。

因此,在非共享上下文中可变数据结构是很好的,并且不变性的好处仅在共享上下文中才会出现。

IMO,为您提供最大自由度的解决方案是为可变性和不可变性定义类,仅在有意义的地方使用constness(先初始化然后永不更改的数据):

/* const-correct */ class C {
   int f1_;
   int f2_;

   const int f3_; // Semantic constness : initialized and never changed.
};
Run Code Online (Sandbox Code Playgroud)

然后,您可以将类的实例C用作可变实例或不可变实例,这在两种情况下均可受益于constness-where-it-makes-sense。

如果现在要共享对象,则可以将其打包到一个智能指针中,该指针指向const

shared_ptr<const C> ptr = make_shared<const C>(f1, f2, f3);
Run Code Online (Sandbox Code Playgroud)

使用此策略,您的自由度将跨越整个3个未同步的象限,同时安全地脱离同步象限。(因此,限制了使结构不可变的需求)