19

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

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

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

    const int Value;
};

这种方法的问题在于,该结构的行为与 C# 中的不可变结构完全不同。不仅MyIndex不能修改变量的Value字段,而且任何现有的变量(局部变量或字段变量)甚至都不能用另一个-instanceMyIndex替换。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!
    }
};

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

错误 C2582:“操作员 =”功能在“MyIndex”中不可用

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

4

3 回答 3

18

这样做的原因是,在 C++ 中,在创建变量(局部变量或字段变量)后,它不能被另一个实例“替换”,只能更改其状态。所以在这一行

index = MyIndex(1);

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

它没有生成的原因是它根本无法明智地实现。复制赋值运算符将像这样实现(在这里不起作用):

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不会发生变异,除非它们被完全替换为它们分配一个新值。

于 2013-06-12T19:29:06.743 回答
3

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

当 a被一个明确的位置/指针“拥有”时,shared_ptr替换它可能是一个好主意。unique_ptrMyIndex

在 C++ 中,我认为更正常的事情是制作可变结构,并在需要的地方简单地将它们变成 const:

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

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

于 2013-06-12T19:58:03.333 回答
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++ 没有这些问题。

于 2013-06-12T19:33:42.200 回答