如何在c++中创建不可变结构体

How to create immutable structs in C++?

本文关键字:创建 不可变 结构体 c++      更新时间:2023-10-16

来自c#背景,我习惯于使结构不可变
因此,当我开始用c++编程时,我尝试对我通常通过值传递的类型做同样的事情。

我有一个简单的结构体,它表示一个自定义索引,并简单地包装了一个整数。因为在c++中const关键字有点类似于c#中的readonly,所以像这样实现我的结构似乎是合理的:

struct MyIndex
{
    MyIndex(int value) :
        Value(value)
    {
    }
    const int Value;
};

这种方法的问题是,这个结构体的行为与c#中的不可变结构体完全不同。不仅MyIndex变量的Value字段不能修改,而且任何现有的MyIndex变量(局部变量或字段变量)甚至不能被另一个MyIndex实例替换。

所以下面的代码示例不能编译:

a)使用index作为循环变量

MyIndex index(0);
while(someCondition)
{
    DoWork();
    index = MyIndex(index.Value + 1); // This does not compile!
}

b)修改类型为MyIndex的成员字段

class MyClass
{
    MyIndex Index;
    void SetMyIndex(MyIndex index)
    {
        Index = index; // This does not compile!
    }
};

这两个示例不能编译,构建错误是相同的:

错误C2582: 'operator ='函数在'MyIndex'中不可用

这是什么原因?为什么不能用另一个实例替换变量,尽管它们不是const ?构建错误究竟意味着什么?operator =函数是什么?

这样做的原因是,在c++中创建变量(无论是局部变量还是字段变量)之后,它不能被另一个实例"替换",只能改变它的状态。在这一行

index = MyIndex(1);

不是创建一个新的MyIndex实例(用它的构造函数),而是用它的复制赋值操作符修改现有的索引变量。缺少的operator =函数是MyIndex类型的复制赋值操作符,因为如果该类型具有const字段,则不会自动生成

不生成它的原因是它根本无法合理地实现。复制赋值操作符将像这样实现(这里不起作用):

MyIndex& operator=(const MyIndex& other)
{
    if(this != &other)
    {
        Value = other.Value; // Can't do this, because Value is const!
    }
    return *this;
}
因此,如果我们希望我们的结构体实际上是不可变的,但行为类似于c#中的不可变结构体,我们必须采取另一种方法,即使我们的结构体的每个字段都是私有的,并将我们类的每个函数标记为const
MyIndex的实现方法:
struct MyIndex
{
    MyIndex(int value) :
        value(value)
    {
    }
    // We have to make a getter to make it accessible.
    int Value() const
    {
        return value;
    }
private:
    int value;
};

严格地说,MyIndex结构体不是不可变的,但是它的状态不能被任何从外部访问的东西改变(除了它的自动生成的复制赋值操作符,但这正是我们想要实现的!)

现在,上面的代码示例可以正确编译,并且我们可以确定MyIndex类型的变量不会发生突变,除非通过给它们赋一个新值来完全替换它们。

我认为不可变对象的概念在很大程度上与按值传递相冲突。如果你想改变一个MyIndex,那么你真的不想要不可变对象,不是吗?然而,如果你确实想要不可变的对象,那么当你写index = MyIndex(index.Value + 1);时,你不想修改MyIndex index,你想用一个不同的MyIndex替换。这意味着完全不同的概念。即:

std::shared_ptr<MyIndex> index = make_shared<MyIndex>(0);
while(someCondition)
{
    DoWork();
    index = make_shared<MyIndex>(index.Value + 1);
}

在c++中看起来有点奇怪,但实际上,这就是Java的工作方式。通过这种机制,MyIndex 可以拥有所有const成员,并且不需要复制构造函数和复制赋值。这是对的,因为它是不可变的。此外,这允许多个对象引用相同的MyIndex,并且知道它将永远不会更改,这是我对不可变对象的大部分理解。

如果不使用类似的策略,我真的不知道如何保持http://www.javapractices.com/topic/TopicAction.do?Id=29中列出的所有好处。

class MyClass
{
    std::shared_ptr<MyIndex> Index;
    void SetMyIndex(const std::shared_ptr<MyIndex>& index)
    {
        Index = index; // This compiles just fine and does exactly what you want
    }
};

MyIndex被一个明确的位置/指针"拥有"时,将shared_ptr替换为unique_ptr可能是一个好主意。

在c++中,我认为更正常的事情是使可变结构,并在需要的地方简单地将它们转换为const:

std::shared_ptr<const std::string> one;
one = std::make_shared<const std::string>("HI");
//bam, immutable string "reference"

由于没有人喜欢开销,我们只使用const std::string&const MyIndex&,并且首先保持所有权清晰。

那么,您可以用一个全新的实例替换该实例。这是相当不寻常的,在大多数情况下,人们更喜欢使用赋值操作符。

但是如果你真的想让c++像c#一样工作,你应该这样做:

void SetMyIndex(MyIndex index)
{
    Index.~MyIndex();            // destroy the old instance (optional)
    new (&Index) MyIndex(index); // create a new one in the same location
}

但是没有意义。使c#结构不可变的整个建议无论如何都是非常值得怀疑的,并且有足够宽的异常来驱动推土机。在大多数情况下,c++比c#有优势的地方都属于这些例外。

关于结构的许多c#建议是处理。net值类的限制——它们是通过原始内存复制复制的,它们没有析构函数或终结器,并且默认构造函数不能可靠地调用。c++没有这些问题。