常量类成员、赋值运算符和 QList

Posted

技术标签:

【中文标题】常量类成员、赋值运算符和 QList【英文标题】:Constant class members, assignment operator and QList 【发布时间】:2011-05-16 08:22:57 【问题描述】:

如果我是正确的,请确认并告诉我是否有更好的解决方案:

我了解具有int const width; 等常量成员的对象无法由编译器隐式创建的合成赋值运算符处理。但是 QList (我想也是 std::list )需要一个有效的赋值运算符。因此,当我想使用具有常量成员和 QList 的对象时,我有三种可能性:

    不要使用常量成员。 (不是解决方案) 实现我自己的赋值运算符。 使用其他不需要分配的容器 运营商

正确吗?还有其他优雅的解决方案吗?

我也想知道我是否可以:

(4) 强制编译器创建处理常量成员的赋值运算符! (我不明白为什么这是一个这么大的问题。为什么操作员不够聪明,无法在内部使用初始化列表?还是我遗漏了什么?) (5) 告诉 QList 我永远不会在列表中使用赋值操作。

编辑:我自己从不分配此类的对象。它们仅由复制构造函数或重载构造函数创建。所以赋值运算符只有容器需要,我自己不需要。

EDIT2:这是我创建的赋值运算符。我不确定它是否正确。 Cell 有一个两个参数的构造函数。这些参数使用初始化列表设置两个常量成员。但该对象还包含其他变量(非 const)成员。

Cell& Cell::operator=(Cell const& other)

 if (this != &other) 
  Cell* newCell = new Cell(other.column(), other.row());
  return *newCell;
 
 return *this;

EDIT3:我发现这个帖子几乎有相同的问题:C++: STL troubles with const class members 所有答案结合在一起回答了我的问题。

【问题讨论】:

为什么 (1) 不是解决方案?这是显而易见的解决方案。 如果类通常是可赋值的,那么成员就不会是常量?! @James McNellis:因为当这个属性应该保持不变时,就完成了成员常量。我不想仅仅因为赋值运算符无法处理它而忽略这个设计决策。 @UncleBens:对不起,我不明白。 编辑后的版本不进行赋值。它只会让你泄漏一些内存。 【参考方案1】:

const 不代表“这个值只能在特殊情况下改变”。相反, const 的意思是“你不能用它做任何事情都会导致它以任何方式发生变化(你可以观察到)”

如果您有一个 const 限定变量,则编译器的命令(以及您自己选择首先用 const 限定它)不允许您做任何会导致它改变的事情。这就是const 所做的。尽管您采取了行动,但它可能会改变,如果它是对非 const 对象的 const 引用,或者出于任何其他原因。如果您作为程序员知道引用对象实际上不是常量,则可以使用const_cast 将其丢弃并更改它。

但在你的情况下,一个常量成员变量,这是不可能的。 const 限定变量不能是对非 const 的 const 引用,因为它根本不是引用。

编辑:关于这一切的激动人心的例子以及为什么你应该在 const 正确性方面表现自己,让我们看看真正的编译器实际上做了什么。考虑这个简短的程序:

int main() 
  const int i = 42; 
  const_cast<int&>(i) = 0; 
  return i;

这是 LLVM-G++ 发出的:

; ModuleID = '/tmp/webcompile/_2418_0.bc'
target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64"
target triple = "x86_64-linux-gnu"

define i32 @main() nounwind 
entry:
  %retval = alloca i32                            ; <i32*> [#uses=2]
  %0 = alloca i32                                 ; <i32*> [#uses=2]
  %i = alloca i32                                 ; <i32*> [#uses=2]
  %"alloca point" = bitcast i32 0 to i32          ; <i32> [#uses=0]
  store i32 42, i32* %i, align 4
  store i32 0, i32* %i, align 4
  store i32 42, i32* %0, align 4
  %1 = load i32* %0, align 4                      ; <i32> [#uses=1]
  store i32 %1, i32* %retval, align 4
  br label %return

return:                                           ; preds = %entry
  %retval2 = load i32* %retval                    ; <i32> [#uses=1]
  ret i32 %retval2

特别感兴趣的是store i32 0, i32* %i, align 4 这一行。这表明const_cast 是成功的,我们实际上为 i 初始化的值分配了一个零。

但是 对 const 限定的修改不会导致可观察到的变化。因此,GCC 产生了一个相当长的链,将 42 放入 %0,然后将 42 放入 %1,然后将其再次存储到 %retval,然后将其加载到 %retval2。因此,G++ 将让这段代码满足这两个要求,const 被丢弃,但 i 没有明显的变化,main 返回 42。


如果您需要一个可以更改的值,例如在标准容器的元素中,那么您不需要const

考虑使用带有公共 getter 和私有 setter 方法的 private: 成员。

【讨论】:

对不起,但是 -1: const 与代码生成无关。在存在mutable 成员或const_cast 的情况下存在很多情况,它们不允许编译器做出这样的假设(因为如果指针可能或不指向被视为@ 的成员,则它是不可计算的987654335@任何任何一个时间点)。编译器可以对常量成员做出一些假设,但它必须能够证明对象实际上是 const,而不是基于源程序中的提示。 它可以生成的代码与对 const 限定变量的操作可能导致的可观察更改有关。也就是说,可能观察不到任何变化。因此,const 变量上的操作可以安全地重新排序或省略。 @Billy:但是当 const 被抛弃时改变 const 对象是未定义的行为? @Billy:我认为唯一可以保证强制转换 const 不会产生未定义行为的实例是 const 指针/引用引用原始 unconst 对象时。 @Billy:关于 cv-qualifiers 的部分怎么样:“除了可以修改任何声明为可变的类成员 (dcl.stc) 之外,任何修改 const 的尝试对象在其生命周期 (basic.life) 会导致未定义的行为。” - 至于您的示例,未定义的行为并不意味着程序可能无法按预期运行。【参考方案2】:

我将尝试简单地捆绑答案:

主要问题是 QList 要求存在赋值运算符,因为它们在内部使用赋值。因此,它们将实现与接口混合在一起。因此,尽管您不需要赋值运算符 QList 没有它就无法工作。 source

@ 3. 有 std::List 但它不提供对元素的恒定时间访问,而 QList 提供。

@ 2. 可以通过使用复制构造函数和所需属性创建一个新对象并返回它*。虽然你绕过了 const 属性,但它仍然比不使用 const 更好,因为你会允许容器在这里作弊,但仍然阻止用户自己做这件事,这是让这个成员保持不变的初衷。

但要考虑到创建重载赋值运算符会增加代码的复杂性,并且可能会引入比成员的 const-ing 首先解决的错误更多的错误。

@ 1. 最后这似乎是最简单的解决方案。只要它是私有的,您只需要注意对象本身不会更改它。

@4. 没办法强迫他。他不知道怎么做,因为变量是恒定的,并且在某些时候他将不得不使用先前定义的int const row; 来执行this-&gt;row = other.row。即使在这种情况下, const 也意味着常量。 one source

@ 5 QList 没有此类选项。

其他解决方案:

使用指向对象的指针而不是纯对象

*目前不确定。

【讨论】:

【参考方案3】:

您可能是 C++ 的新手,并希望它的行为类似于 Python、Java 或 C#。

将不可变的 Java 对象放入集合中是很常见的。这是有效的,因为在 Java 中,您并没有真正将 Java objects 放入集合中,而只是将 Java references 放入到 Java 对象中。更准确地说,集合内部由 Java 引用变量组成,分配给这些 Java 引用变量根本不会影响被引用的 Java 对象。他们甚至没有注意到。

我特意说“Java 对象”、“Java 引用”和“Java 变量”,因为术语“对象”、“引用”和“变量”在 C++ 中具有完全不同的含义。如果你想要可变的T 变量,你想要可变的T 对象,因为变量和对象在C++ 中基本上是一回事:

一个变量是由一个对象的声明引入的。变量名表示对象。

在 C++ 中,变量不包含对象——它们对象。赋值给变量意味着改变对象(通过调用成员函数operator=)。没有其他办法了。如果你有一个不可变对象,那么赋值a = b无法在不明确破坏类型系统的情况下工作,如果你这样做,那么你实际上是在向你的客户撒谎说对象是不可变的。做出承诺然后故意违背它是没有意义的,不是吗?

当然,您可以简单地模拟 Java 方式:使用一组 指针 指向不可变对象。这是否是一个有效的解决方案取决于您的对象真正代表什么。但仅仅因为这在 Java 中运行良好并不意味着它在 C++ 中运行良好。 在 C++ 中不存在不可变值对象模式。这在 Java 中是个好主意,在 C++ 中是个糟糕的主意。

顺便说一句,您的赋值运算符是完全非惯用的并且会泄漏内存。如果你认真学习 C++,你应该阅读one of these books。

【讨论】:

【参考方案4】:

(4) 不是一个选项。隐式声明的复制赋值运算符将右侧对象的每个成员分配给左侧对象的相同成员。

编译器无法为具有 const 限定数据成员的类隐式生成复制赋值运算符,原因与此无效:

const int i = 1;
i = 2;

(2) 是有问题的,因为您必须以某种方式克服同样的问题。

(1) 是显而易见的解决方案;如果您的类类型具有 const 限定的数据成员,则它是不可分配的,并且分配没有多大意义。为什么说这不是解决方案?


如果您不希望您的类类型可分配,那么您不能在要求其值类型可分配的容器中使用它。所有的 C++ 标准库容器都有这个要求。

【讨论】:

请看我的编辑。那么如何处理带有 const 成员的对象呢?我想使用常量来防止不必要的更改,就像您将成员设为私有以防止错误一样。使用 std 容器时是否“禁用”了这样一个主要功能?我不敢相信。 @problemofficer:我不明白这个问题:如果你真的不想改变成员变量的值,那么这个类显然是不可赋值的。一方面,您希望数据成员是不可变的(因为您希望它是const),但您也希望它是可变的(因为您希望能够为其分配一个新值)。符合 const 条件的数据成员很少物有所值。 我不想分配任何东西。我只是想将这些对象存储在容器中。容器抱怨它需要赋值运算符。如果不是容器,我什至不知道我的对象没有赋值运算符,因为正如我所说,我不需要它也不会使用它。 将指针或对对象的引用存储为能够使用 stl 容器的解决方法吗? @problemofficer:QList 不是标准容器。事实上,标准列表 (std::list) 可以处理不可分配的类型,它是列表的 Qt 实现需要可分配性。

以上是关于常量类成员、赋值运算符和 QList的主要内容,如果未能解决你的问题,请参考以下文章

赋值运算符 - 自赋值

C++中的重载赋值运算符

当类成员是引用时无法生成默认赋值运算符?(在 C++ 中)

隐式移动构造函数和赋值运算符

c++中拷贝构造函数和赋值运算符重载本质上一样么

常量与转义符和运算符