【问题标题】:How to create immutable structs in C++?如何在 C++ 中创建不可变结构?
【发布时间】:2013-06-12 19:29:06
【问题描述】:

来自 C# 背景,我习惯于使 结构不可变
因此,当我开始使用 C++ 编程时,我尝试对通常按值传递的类型进行相同的处理。

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

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

    const int Value;
};

这种方法的问题在于,该结构的行为与 C# 中的不可变结构完全不同。不仅MyIndex变量的Value字段不能修改,任何现有的MyIndex变量(局部变量或字段变量)甚至都不能用另一个MyIndex-instance替换。

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

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

这两个示例不编译,编译错误是一样的:

错误 C2582:“MyIndex”中的“operator =”函数不可用

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

【问题讨论】:

  • C# 中的结构和 C++ 中的结构非常不同。我认为这个问题不适用于 C++。
  • 在 C++ 中,struct 和 class 是一样的,它们都更像 C# struct 而不是 C# classes。
  • @SLaks:整个问题是 C++,问题是“如何在 C++ 中制作类似 java 的不可变结构”
  • 使用复制构造函数似乎完全违反了不可变对象 IMO。这样做的正确方法不是涉及std::shared_ptr吗?事实上javapractices.com/topic/TopicAction.do?Id=29 提到“不需要复制构造函数”
  • 我对 C++ 不是很熟悉,但我认为 C# 中所谓的“不可变”结构等同于只有私有(但非 const)字段的 C++ 结构,并且只允许通过方法或只读属性读取这些字段的值[我不知道属性是 C++ 标准的一部分还是 MS 扩展]。请注意,在 .NET 中并没有真正的不可变结构类型之类的东西——只有使突变变得方便的结构和使突变变得尴尬的结构。

标签: c++ immutability


【解决方案1】:

这样做的原因是,在 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 类型的变量不会发生变异,除非它们被完全替换为它们分配一个新值。

【讨论】:

  • 虽然这是 100% 正确的,但我认为不可变数据只有在使用指向数据的指针时才真正有意义(这是 java 在幕后所做的),这意味着使复制构造函数工作不仅毫无意义,但实际上对不可变数据的想法有害。
【解决方案2】:

我认为不可变对象的概念在很大程度上与按值传递相冲突。如果您想更改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++ 中,我认为更正常的做法是制作 mutable 结构,然后在需要的地方简单地将它们变成 const:

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

由于没有人喜欢开销,我们只使用const std::string&amp;const MyIndex&amp;,并首先明确所有权。

【讨论】:

    【解决方案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++ 没有这些问题。

    【讨论】:

    • 我不确定这是否不是 UB,[basic.life]/7 表示该类不能有 const 数据成员用于要使用的对象的名称/引用在这个技巧之后。
    • 哇,这个语法我以前都没见过,很有趣,谢谢!但是,我认为使 C# 结构不可变是有益的,原因有很多(它们已经在 StackOverflow 上讨论过很多)。由于同样的原因,我发现相同的不变性在 C++ 中也是有利的——它可以帮助避免细微的错误。
    • @DyP:我认为你可能是对的。当编译器知道成员是const时,它可能会缓存成员的值。
    • @DyP:我想即使有const 成员,该类型仍然符合普通可复制 的条件,所以可以使用memcpy
    • 第一个index不应该是Index吗?
    猜你喜欢
    • 1970-01-01
    • 2020-07-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-12-09
    • 1970-01-01
    相关资源
    最近更新 更多