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

5
请确认我的理解是否正确,并告诉我是否有更好的解决方案:
我了解像 int const width; 这样具有常量成员的对象无法由编译器隐式创建的合成赋值运算符处理。但 QList(我想 std::list 也是如此)需要一个可用的赋值运算符。因此,当我想要使用具有常量成员和 QList 的对象时,我有三种选择:
1. 不使用常量成员。(不是一个解决方案) 2. 实现自己的赋值运算符。 3. 使用其他不需要赋值运算符的容器。
这些解决方案正确吗?还有其他优雅的解决方案吗?
我还想知道是否可以:
4. 强制编译器创建处理常量成员的赋值运算符!(我不明白为什么这是个大问题。为什么运算符不能聪明地在内部使用初始化列表?或者我漏掉了什么?) 5. 告诉 QList 我永远不会在列表中使用赋值操作。
编辑:我从未自己分配这个类的对象。它们只能通过复制构造函数或重载构造函数创建。因此,赋值运算符只由容器而不是我自己需要。
编辑2:这是我创建的赋值运算符。但我不确定它是否正确。Cell 有一个两个参数的构造函数。这些参数使用初始化列表设置了两个常量成员。但对象还包含其他变量(非 const)成员。
Cell& Cell::operator=(Cell const& other)
{
 if (this != &other) {
  Cell* newCell = new Cell(other.column(), other.row());
  return *newCell;
 }
 return *this;
}

编辑3: 我找到了一个几乎相同问题的线程:C++: STL troubles with const class members 所有答案综合起来回答了我的问题。


4
为什么(1)不是一个解决方案?它看起来就是最明显的解决方案。 - James McNellis
如果类通常是可分配的,那么成员就不会是常量吗?! - UncleBens
@James McNellis:将成员设置为常量是因为该属性应保持不变。我不想因为赋值运算符无法处理它而省略这个设计决策。 - problemofficer
@UncleBens:抱歉,我不明白。 - problemofficer
4
编辑后的版本不执行分配(assign)操作,它只会导致一些内存泄漏(memory leak)。 - UncleBens
显示剩余2条评论
4个回答

8

你可能是C++的新手,期望它像Python、Java或C#一样运行。

在Java中,将不可变的Java对象放入集合中是很常见的。这是因为在Java中,你实际上并没有把Java 对象放入集合中,而是仅仅将指向Java对象的Java 引用放入其中。更准确地说,集合内部由Java引用变量组成,对这些Java引用变量进行赋值根本不会影响引用的Java对象。它们甚至没有注意到。

我故意使用了"Java对象"、"Java引用"和"Java变量"这些词,因为在C++中,术语"对象"、"引用"和"变量"具有完全不同的含义。如果你想要可变的T变量,那么你就想要可变的T对象,因为在C++中,变量和对象基本上是相同的东西:

通过对象的声明引入变量。变量的名称表示该对象。

在C++中,变量不包含对象——它们对象。对变量进行赋值意味着改变对象(通过调用成员函数operator=)。没有任何绕过它的方法。如果你有一个不可变对象,那么赋值a = b 绝对不能正常工作,除非你明确地破坏类型系统,而如果你这样做,那么你就有效地欺骗了客户关于该对象是不可变的。做出承诺然后故意违背它是相当无意义的,不是吗?

当然,你可以简单地模拟Java的方式:使用指向不可变对象的指针集合。是否这是一种有效的解决方案取决于你的对象实际上代表什么。但仅仅因为这在Java中运行良好并不意味着它在C++中也能很好地运行。C++中没有不可变值对象模式。这在Java中是一个好主意,在C++中则是一个可怕的主意。

顺便说一下,你的赋值运算符完全不符合惯用法,并且会泄漏内存。如果你真的想学习C++,你应该阅读这些书籍之一。


3

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

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

const int i = 1;
i = 2;

(2) 是有问题的,因为你必须以某种方式克服这个问题。

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


如果你不想让你的类类型可赋值,那么你就不能在要求其值类型可赋值的容器中使用它。所有C++标准库容器都有此要求。


1
我不想分配任何东西。我只是想把这些对象存储在一个容器中。但是容器抱怨它需要赋值运算符。如果不是因为容器,我甚至都不知道我的对象没有赋值运算符,因为我不需要它,也不会使用它。 - problemofficer
@problemofficer:你不能使用那些容器,因为它们要求你的对象是可分配的。虽然在技术上实现一个不使用 operator= 的链表是可能的,但通常需要复制和赋值语义的容器会在内部使用它。 - André Caron
2
将指针或引用存储到对象中,是使用STL容器的一种解决方法吗? - Falmarri
@Falmarri:引用是不可行的,因为引用既不是对象也不可分配。指针的缺点是需要额外的间接访问对象。 - James McNellis
3
QList不是一个标准的容器。事实上,标准列表(std::list)可以处理不可分配类型,而需要可分配性的是Qt实现的列表。请注意不要改变原文的意思。 - David Rodríguez - dribeas
显示剩余7条评论

3
const并不意味着“只有在特殊情况下才能更改此值”。相反,const的意思是“您所允许使用它的任何操作都不会导致它以任何方式发生更改(您可以观察到)”。
如果您有一个带有const限定符的变量,则根据编译器的规定(以及您首先选择用const进行限定的选择),您不允许执行任何会导致其更改的操作。这就是const的作用。如果它是对非常量对象的const引用或任何其他原因的const引用,则可能会在您的操作下发生更改。如果作为程序员,您“知道”引用实际上并不是常量,则可以使用const_cast将其转换并进行更改。
但是,在您的情况下,作为常量成员变量,这是不可能的。该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的值上。
但是,对于常量限定符的修改不会导致可观察的变化。因此,GCC会产生一个相当长的链,将42放入%0中,然后将其加载到%1中,再将其存储到%retval中,然后将其加载到%retval2中。因此,G++将使此代码同时满足两个要求:const被强制转换,但i没有发生可观察的变化,main函数返回42。
如果需要一个可以更改的值,例如在标准容器的元素中,则不需要使用const。考虑使用具有公共getter和私有setter方法的private:成员。

抱歉,但是const与代码生成无关。存在许多情况,其中有mutable成员或const_cast,这些情况不允许编译器做出这样的假设(因为在任何时候指针可能或可能不指向被认为是const的成员,这是不可计算的)。编译器可以对常量成员做出一些假设,但必须能够证明对象实际上是const,而不是基于源程序中的提示。 - Billy ONeal
1
@Billy:但是当常量性被移除时,改变常量对象的值是未定义行为? - UncleBens
2
@Billy:我认为唯一可以保证不会产生未定义行为的情况是const指针/引用引用最初未被设为const的对象。 - lijie
3
@Billy:关于cv限定符的部分:“除了任何声明为mutable(dcl.stc)的类成员可以被修改外,任何在其生命周期(basic.life)内尝试修改const对象的行为都将导致未定义行为。” 至于你的例子,未定义行为并不意味着程序不会按预期运行。 - UncleBens
@UncleBens:我在第一个例子上的观点已经被纠正了。不过,我的第二个例子仍然能够展示这种行为。 - Billy ONeal
显示剩余12条评论

2
我会尝试简单地概括答案:
主要问题是 QList 要求存在赋值运算符,因为它们在内部使用赋值。因此,它们混合了实现和接口。因此,即使您不需要赋值运算符,QList 也无法正常工作。 source 3. 有 std::List,但它不提供对元素的常数时间访问,而 QList 提供。
2. 可以通过使用复制构造函数创建具有所需属性的新对象并返回它来实现。尽管您绕过了 const 属性,但仍比根本不使用 const 好,因为这样会允许容器在这里作弊,但仍然可以防止用户自己这样做,这是最初制作此成员常量的目的。
但请注意,创建重载的赋值运算符会增加代码的复杂性,并可能引入更多的错误,而成员的 const-ing 可以解决这些问题。
1. 最后,这似乎是最简单的解决方案。只要它是私有的,您只需要注意对象本身不会改变它。
4. 没有强制他的方法。他不知道怎么做,因为变量是常量,在某个点上他必须执行this->row = other.row,而int const row;已经被定义为常量。即使在这种情况下,const仍然表示常量。一个来源 5. QList没有这种选项。
额外的解决方案:
- 使用指向对象的指针而不是纯对象
*目前不确定。

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