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

20

我来自于C#背景,习惯于实现不可变的结构体(structs)
所以当我开始在C++中编程时,我尝试对通常通过按值传递的类型执行相同的操作。

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

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

    const int Value;
};

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

因此,以下代码示例无法编译:

a)将索引用作循环变量。

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!
    }
};

这两个样例无法编译,构建错误相同:

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

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


2
C#中的结构体和C++中的结构体非常不同。我认为这个问题在C++中并不适用。 - SLaks
在C++中,结构体和类是相同的,它们更像C#结构体而不是C#类。 - Billy ONeal
1
@SLaks:整个问题都是关于C++的,问题是“如何在C++中创建类似Java的不可变结构体”。 - Mooing Duck
使用复制构造函数似乎完全违反了不可变对象的原则。在我看来,正确的方法难道不应该涉及std::shared_ptr吗?实际上,http://www.javapractices.com/topic/TopicAction.do?Id=29提到“不需要复制构造函数”。 - Mooing Duck
我对C++不是特别熟悉,但我认为在C#中所谓的“不可变”结构体相当于一个只有私有(但非const)字段的C++结构体,并且只允许通过方法或只读属性读取这些字段的值[我不知道属性是否是C++标准的一部分还是MS的扩展]。请注意,在.NET中实际上并没有不可变的结构类型——只有使突变方便的结构和使其笨拙的结构。 - supercat
3个回答

18
这是因为在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的变量不会被改变,除非它们被赋予一个新值完全替换掉。


1
虽然这是100%正确的,但我认为只有在使用指向数据的指针时(这是Java在幕后执行的操作),不可变数据才真正有意义,这意味着使复制构造函数工作不仅毫无意义,而且实际上对于不可变数据的概念是有害的。 - Mooing Duck

4

我认为不可变对象的概念与按值传递存在很大冲突。如果您想要更改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
    }
};

当一个被单一的清晰地拥有时,使用替换可能是个好主意。

在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&,并在第一时间明确所有权。


3

好的,你可以用全新的实例来替换该实例。在大多数情况下,人们更喜欢使用赋值运算符,这是相当不寻常的。

但如果你真的想让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++没有这些问题。


我不确定这是否属于未定义行为,[basic.life]/7说了些关于类不能有常量数据成员的东西,以便在这个技巧之后使用对象的名称/引用。 - dyp
1
哇,我之前从没见过这种语法,它很有趣,谢谢!但是,我认为让 C# 结构体变成不可变的在很多方面都是有益的(已经在 StackOverflow 上得到了广泛的讨论)。我发现在 C++ 中实现同样的不可变性也有同样的优势——可以帮助避免微妙的错误。 - Mark Vincze
@DyP:我认为你可能是对的。当编译器知道成员变量是“const”时,它可能会缓存其值。 - Ben Voigt
@DyP:我想即使有const成员,该类型仍然符合可平凡复制的要求,因此可以使用memcpy吗? - Ben Voigt
第一个 index 不应该是一个 Index 吗? - dyp
它将具有一个微不足道的复制构造函数等,但它们将被删除。不确定那个。然而[dcl.type.cv]/4支持您的解释:“在其生命周期内尝试修改const对象的任何尝试都会导致未定义的行为。” - dyp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接