简体   繁体   English

在 C++ 中创建不可变且高效的类的惯用方法

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

I am looking to do something like this (C#).我正在寻找做这样的事情(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) {}
}

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. 1. 对类成员使用const ,但这意味着删除了默认的复制赋值运算符。

Solution 1:解决方案1:

struct OtherImmutableObject {
    const int i1;
    const int i2;

    OtherImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
}

Problem 1:问题1:

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

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&&)编辑:这很重要,因为我想在std::vector存储不可变对象但收到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. 2. 使用 get 方法和返回值,但这意味着必须复制大对象,这是我想知道如何避免的低效率。 This thread suggests the get solution, but it doesn't address how to handle passing non-primitive objects without copying the original object. 该线程建议了 get 解决方案,但它没有解决如何在不复制原始对象的情况下处理传递的非原始对象。

Solution 2:解决方案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.
}

Problem 2: The unnecessary copies are inefficient.问题2:不必要的副本效率低下。

3. Using get methods and returning const references or const pointers but this could leave hanging references or pointers. 3. 使用 get 方法并返回const引用或const指针,但这可能会留下挂起的引用或指针。 This thread talks about the dangers of references going out of scope from function returns. 该线程讨论了引用超出函数返回范围的危险。

Solution 3:解决方案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; }
}

Problem 3:问题 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

Undefined behaviour can occur due to returning a reference from any of the Get methods.由于从任何Get方法返回引用,可能会发生未定义的行为。

  1. You truly want immutable objects of some type plus value semantics (as you care about runtime performance and want to avoid the heap).您确实需要某种类型的不可变对象加上值语义(因为您关心运行时性能并希望避免堆)。 Just define a struct with all data members public .只需定义一个包含所有数据成员publicstruct

     struct Immutable { const std::string str; const int i; };

    You can instantiate and copy them, read data members, but that's about it.您可以实例化和复制它们,读取数据成员,仅此而已。 Move-constructing an instance from an rvalue reference of another one still copies.从另一个仍然复制的右值引用移动构造一个实例。

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

    This way, you really make sure every usage of your class respects the immutability (assuming no one does bad const_cast things).通过这种方式,您可以真正确保类的每次使用都尊重不变性(假设没有人做不好的const_cast事情)。 Additional functionality can be provided through free functions, there is no point in adding member functions to a read-only aggregation of data members.可以通过免费函数提供附加功能,将成员函数添加到数据成员的只读聚合中是没有意义的。

  2. You want 1., still with value semantics, but slightly relaxed (such that the objects aren't really immutable anymore) and you're also concerned that you need move-construction for the sake of runtime performance.您想要 1.,仍然具有值语义,但稍微放松(这样对象不再是真正不可变的),并且您还担心为了运行时性能而需要移动构造。 There is no way around private data members and getter member functions:没有办法绕过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; };

    Usage is the same, but the move construction really does move.用法是一样的,但移动结构确实移动。

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

    Whether you want assignment to be allowed or not is under your control now.现在是否允许分配由您控制。 Just = delete the assignment operators if you don't want it, otherwise go with the compiler-generated one or implement your own.如果您不想要,只需= delete赋值运算符,否则使用编译器生成的运算符或实现您自己的赋值运算符。

     obj3 = obj2; // Ok if not manually disabled
  3. You don't care about value semantics and/or atomic reference count increments are ok in your scenario.您不关心值语义和/或原子引用计数增量在您的场景中是可以的。 Use the solution depicted in @NathanOliver's answer .使用@NathanOliver's answer中描述的解决方案。

You can basically get what you want by leveraging a std::unique_ptr or std::shared_ptr .你基本上可以通过利用std::unique_ptrstd::shared_ptr来获得你想要的。 If you only want one of these objects, but allow for it to be moved around, then you can use a std::unique_ptr .如果您只想要这些对象之一,但允许它四处移动,那么您可以使用std::unique_ptr If you want to allow for multiple objects ("copies") that all have the same value, then you can use a std::shared_Ptr .如果您想允许多个对象(“副本”)都具有相同的值,那么您可以使用std::shared_Ptr Use an alias to shorten the name and provide a factory function and it becomes pretty painless.使用别名来缩短名称并提供工厂函数,它变得非常轻松。 That would make your code look like:这会让你的代码看起来像:

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
}

If the code is changes to use a shared_ptr instead then you could do如果代码更改为使用shared_ptr代替,那么您可以这样做

second = first;

and then both objects point to the same object, but neither can modify it.然后两个对象都指向同一个对象,但都不能修改它。

Immutability in C++ can't be directly compared to immutability in most other popular languages because of C++'s universal value semantics.由于 C++ 的通用值语义,C++ 中的不变性无法直接与大多数其他流行语言中的不变性进行比较。 You have to figure out what you want "immutable" to mean.你必须弄清楚你想要“不可变”是什么意思。

You want to be able to assign new values to variables of type OtherImmutableObject .您希望能够为OtherImmutableObject类型的变量分配新值。 That makes sense, since you can do that with variables of type ImmutableObject in C#.这是有道理的,因为您可以在 C# 中使用ImmutableObject类型的变量来做到这一点。

In that case, the simplest way to get the semantics you want is在这种情况下,获得所需语义的最简单方法是

struct OtherImmutableObject {
    int i1;
    int i2;
};

It may look like this is mutable.看起来这是可变的。 After all, you can write毕竟可以写

OtherImmutableObject x{1, 2};
x.i1 = 3;

But the effect of that second line is (ignoring concurrency...) exactly the same as the effect of但是第二行的效果(忽略并发...)与

x = OtherImmutableObject{3, x.i2};

so if you want to allow assignment to variables of type OtherImmutableObject then it makes no sense to disallow direct assignment to members, since it doesn't provide any additional semantic guarantee;因此,如果您想允许对OtherImmutableObject类型的变量赋值,那么禁止对成员直接赋值是没有意义的,因为它不提供任何额外的语义保证; all it does is make the code for the same abstract operation slower.它所做的只是使同一抽象操作的代码变慢。 (In this case, most optimizing compilers will probably generate the same code for both expressions, but if one of the members was a std::string they might not be smart enough to do that.) (在这种情况下,大多数优化编译器可能会为两个表达式生成相同的代码,但如果其中一个成员是std::string他们可能不够聪明,无法这样做。)

Note that this is the behavior of basically every standard type in C++, including int , std::complex , std::string , etc. They are all mutable in the sense that you can assign new values to them, and all immutable in the sense that the only thing you can do (abstractly) to change them is assign new values to them, much like immutable reference types in C#.请注意,这是 C++ 中基本上每个标准类型的行为,包括intstd::complexstd::string等。它们都是可变的,您可以为它们分配新值,并且在感觉您唯一可以(抽象地)更改它们的方法是为它们分配新值,就像 C# 中的不可变引用类型一样。

If you don't want that semantics, your only other option is to forbid assignment.如果您不想要这种语义,那么您唯一的其他选择就是禁止赋值。 I would advise doing that by declaring your variables to be const , not by declaring all the members of the type to be const , because it gives you more options for how you can use the class.我建议通过将变量声明为const来实现这一点,而不是将类型的所有成员声明为const ,因为它为您提供了更多关于如何使用该类的选项。 For example, you can create an initially mutable instance of the class, build a value in it, then "freeze" it by using only const references to it thereafter – like converting a StringBuilder to a string , but without the overhead of copying it.例如,您可以创建该类的初始可变实例,在其中构建一个值,然后通过仅使用对它的const引用来“冻结”它——就像将StringBuilder转换为string ,但没有复制它的开销。

(One possible reason to declare all members to be const might be that it allows for better optimization in some cases. For example, if a function gets an OtherImmutableObject const& , and the compiler can't see the call site, it isn't safe to cache the values of members across calls to other unknown code, since the underlying object may not have the const qualifier. But if the actual members are declared const , then I think it would be safe to cache the values.) (将所有成员声明为const一个可能原因可能是在某些情况下它允许更好的优化。例如,如果一个函数获得一个OtherImmutableObject const& ,并且编译器看不到调用站点,则它是不安全的在调用其他未知代码时缓存成员的值,因为底层对象可能没有const限定符。但如果实际成员被声明为const ,那么我认为缓存这些值是安全的。)

To answer your question, you don't create immutable data structures in C++ because const ing references to the whole object does the trick.为了回答您的问题,您不要在 C++ 中创建不可变的数据结构,因为对整个对象的const ing 引用可以解决问题。 Violations of the rule are made visible by the presence of const_cast s. const_cast的存在使违反规则的行为可见。

If I may refer to Kevlin Henney's "Thinking outside the synchronization quadrant", there are two questions to ask about data:如果我可以参考 Kevlin Henney 的《同步象限之外的思考》,关于数据有两个问题要问:

  • Is a structure immutable or mutable?结构是不可变的还是可变的?
  • Is it shared or unshared?它是共享的还是非共享的?

These questions can be arranged into a nice 2x2 table with 4 quadrants.这些问题可以排列成一个漂亮的 2x2 表格,有 4 个象限。 In a concurrent context, only one quadrant needs synchronization: shared mutable data.在并发上下文中,只有一个象限需要同步:共享可变数据。

Indeed, immutable data need not be synchronized because you cannot write to it, and concurrent reads are fine.确实,不可变数据不需要同步,因为您无法写入,并发读取也可以。 Unshared data needs not be synchronized, because only the owner of the data can write to it or read from it.非共享数据不需要同步,因为只有数据的所有者才能写入或读取数据。

So it is fine for a data structure to be mutable in an unshared context, and the benefits of immutability occur only in a shared context.因此,数据结构在非共享上下文中可变是可以的,而不变性的好处仅出现在共享上下文中。

IMO, the solution that gives you most freedom is to define your class for both mutability and immutability, using constness only where it makes sense (data that is initalized then never changed): IMO,给你最大自由的解决方案是定义你的类的可变性和不变性,只在有意义的地方使用常量(数据被初始化然后永远不会改变):

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

   const int f3_; // Semantic constness : initialized and never changed.
};

You can then use instances of your class C either as mutable or immutable, benefitting of constness-where-it-makes-sense in either case.然后,您可以将类C实例用作可变或不可变的,这在任何一种情况下都可以受益于 constness-where-it-makes-sense。

If now you want to share your object, you can pack it in a smart pointer to const :如果现在你想共享你的对象,你可以将它打包到一个指向const的智能指针中:

shared_ptr<const C> ptr = make_shared<const C>(f1, f2, f3);

Using this strategy, your freedom spans the whole 3 unsynchronized quandrants while staying safely out of the synchronization quadrant.使用此策略,您的自由可以跨越整个 3 个未同步象限,同时安全地保持在同步象限之外。 (therefore, limiting the need of making your structure immutable) (因此,限制了使您的结构不可变的需要)

I'd say the most idiomatic way would be that:我会说最惯用的方式是:

struct OtherImmutable {
    int i1;
    int i2;

    OtherImmutable(int i1, int i2) : i1(i1), i2(i2) {}
};

But... that not immutable??但是……那不是一成不变的??

Indeed but you can pass it around as a value:确实,但您可以将其作为值传递:

void frob1() {
    OtherImmutable oi;
    oi = frob2(oi);
}

auto frob2(OtherImmutable oi) -> OtherImmutable {
    // cannot affect frob1 oi, since it's a copy
}

Even better, places that don't need to mutate locally can define its local variables as const:更好的是,不需要在本地发生变异的地方可以将其局部变量定义为 const:

auto frob2(OtherImmutable const oi) -> OtherImmutable {
    return OtherImmutable{oi.i1 + 1, oi.i2};
}

C++ doesn't quite have the ability to predefine a class as immutable or const. C ++不相当有预定义类为不可改变或const的能力。

And at some point you'll probably come to the conclusion that you shouldn't use const for class members in C++.并且在某些时候您可能会得出结论,您不应该对 C++ 中的类成员使用const It's just not worth the annoyances, and honestly you can do without it.它只是不值得烦恼,老实说你可以没有它。

As a practical solution, I would try:作为一个实用的解决方案,我会尝试:

typedef class _some_SUPER_obtuse_CLASS_NAME_PLEASE_DONT_USE_THIS { } const Immutable;

to discourage anyone from using anything but Immutable in their code.阻止任何人在他们的代码中使用除了Immutable任何东西。

Immutable objects work much better with pointer semantics.不可变对象与指针语义一起工作得更好。 So write a smart immutable pointer:所以写一个智能的不可变指针:

struct immu_tag_t {};
template<class T>
struct immu:std::shared_ptr<T const>
{
  using base = std::shared_ptr<T const>;

  immu():base( std::make_shared<T const>() ) {}

  template<class A0, class...Args,
    std::enable_if_t< !std::is_base_of< immu_tag_t, std::decay_t<A0> >{}, bool > = true,
    std::enable_if_t< std::is_construtible< T const, A0&&, Args&&... >{}, bool > = true
  >
  immu(A0&& a0, Args&&...args):
    base(
      std::make_shared<T const>(
        std::forward<A0>(a0), std::forward<Args>(args)...
      )
    )
  {}
  template<class A0, class...Args,
    std::enable_if_t< std::is_construtible< T const, std::initializer_list<A0>, Args&&... >{}, bool > = true
  >
  immu(std::initializer_list<A0> a0, Args&&...args):
    base(
      std::make_shared<T const>(
        a0, std::forward<Args>(args)...
      )
    )
  {}

  immu( immu_tag_t, std::shared_ptr<T const> ptr ):base(std::move(ptr)) {}
  immu(immu&&)=default;
  immu(immu const&)=default;
  immu& operator=(immu&&)=default;
  immu& operator=(immu const&)=default;

  template<class F>
  immu modify( F&& f ) const {
    std::shared_ptr<T> ptr;
    if (!*this) {
      ptr = std::make_shared<T>();
    } else {
      ptr = std::make_shared<T>(**this);
    }
    std::forward<F>(f)(*ptr);
    return {immu_tag_t{}, std::move(ptr)};
  }
};

This leverages shared_ptr for most of its implementation;这在其大部分实现中利用了shared_ptr most of the disadvantages of shared_ptr are not a problem with immutable objects. shared_ptr大多数缺点都不是不可变对象的问题。

Unlike shared ptr, it permits you to create the object directly, and by default creates a non-null state.与共享 ptr 不同,它允许您直接创建对象,并且默认情况下创建非空状态。 It can still reach a null state by being moved-from.它仍然可以通过被移出而达到空状态。 You can create one in a null state by doing:您可以通过执行以下操作在空状态下创建一个:

immu<int> immu_null_int{ immu_tag_t{}, {} };

and a non-null int via:和一个非空的 int 通过:

immu<int> immu_int;

or或者

immu<int> immu_int = 7;

I added a useful utility method called modify .我添加了一个有用的实用方法,称为modify Modify gives you a mutable instance of the T to pass to a lambda to modify before it is returned packaged up in an immu<T> . Modify 为您提供T一个可变实例,以便在将其打包返回到immu<T>之前传递给 lambda 进行修改。

Concrete use looks like:具体使用如下:

struct data;
using immu_data = immu<data>;
struct data {
  int i;
  other_immutable_class o;
  std::vector<other_immutable_class> r;
  data( int i_in, other_immutable_class o_in, std::vector<other_immutable_class> r_in ):
    i(i_in), o(std::move(o_in)), r( std::move(r_in))
  {}
};

Then use immu_data .然后使用immu_data

Accessing members requires -> not .访问成员需要-> not . , and you should check for null immu_data s if you are passed them. ,如果你通过了它们,你应该检查 null immu_data s。

Here is how you use .modify :以下是您如何使用.modify

immu_data a( 7, other_immutable_class{}, {} );
immu_data b = a.modify([&](auto& b){ ++b.i; b.r.emplace_back() });

This creates a b whose value is equal to a , except i is incremented by 1, and there is an extra other_immutable_class in br (default constructed).这将创建一个b其值等于a ,除了i增加 1,并且br有一个额外的other_immutable_class (默认构造)。 Note that a is unmodified by creating b .请注意, a未通过创建b修改。

There are probably typos above, but I've used the design.上面可能有错别字,但我已经使用了这个设计。

If you want to get fancy, you can make immu support copy-on-write, or modify-in-place if unique.如果你想花哨,你可以让immu支持写时复制,或者如果唯一的话,就地修改。 It is harder than it sounds though.不过,这比听起来要难。

The issue at hand is a mistranslation from C# to C++.手头的问题是从 C# 到 C++ 的错误翻译。 In C++ there is simply no* need to do this:在 C++ 中,根本不需要*这样做:

class ImmutableObject {
    ImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
    const int i1;
    const int i2;
}
ImmutableObject o1(1,2):
ImmutableObject o2(2,3);
o1 = o2; // Doesn't compile, because immutable objects are by definition not mutable.

In your C# example you are using a class.在您的 C# 示例中,您使用的是一个类。 And a variable that holds an instance of a class in C# is really just a reference to a garbage collected object.在 C# 中保存类实例的变量实际上只是对垃圾收集对象的引用。 The closest equivalent in C++ is a reference counted smart pointer. C++ 中最接近的等价物是引用计数智能指针。 So your c# example is translated to C++ as:所以你的 c# 示例被转换为 C++ 为:

class ImmutableObject {
    ImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
    const int i1;
    const int i2;
}
std::shared_ptr<ImmutableObject> o1 = std::make_shared<ImmutableObject>(1,2);
std::shared_ptr<ImmutableObject> o2 = std::make_shared<ImmutableObject>(2,3);
o1 = o2; // Does compile because shared_ptr is mutable.

There are several options if you want a mutable reference to an immutable/const object, specifically you can use a pointer, a smart pointer , or a reference_wrapper .如果您想要对不可变/常量对象的可变引用,有多种选择,特别是您可以使用指针、智能指针reference_wrapper Unless you actually want to have a class whose content can be changed by anyone at any time, which is the opposite of an immutable class.除非你真的想要一个任何人都可以随时更改内容的类,这与不可变类相反。


*Of course, C++ is a language where "no" doesn't exist. *当然,C++ 是一种“不”不存在的语言。 In those precious few truly exceptional circumstances you can use const_cast .在那些宝贵的少数真正特殊的情况下,您可以使用const_cast

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM