我试图了解在 C++ 中重载某些运算符的目的。
从概念上讲,赋值语句可以通过以下方式轻松实现:
- 销毁旧对象,然后复制构造新对象
- 复制新对象的构造,然后与旧对象交换,然后销毁旧对象
事实上,复制和交换的实现通常是实际代码中赋值的实现。
那么,为什么 C++ 允许程序员重载赋值运算符,而不仅仅是执行上述操作?
是否打算允许分配比破坏+构造更快的场景?
如果是这样,什么时候发生?如果不是,那么它打算支持什么用例?
我试图了解在 C++ 中重载某些运算符的目的。
从概念上讲,赋值语句可以通过以下方式轻松实现:
事实上,复制和交换的实现通常是实际代码中赋值的实现。
那么,为什么 C++ 允许程序员重载赋值运算符,而不仅仅是执行上述操作?
是否打算允许分配比破坏+构造更快的场景?
如果是这样,什么时候发生?如果不是,那么它打算支持什么用例?
1) 引用计数
假设您有一个被引用计数的资源并且它被包装在对象中。
void operator=(const MyObject& v) {
this->resource = refCount(v->resource);
}
// Here the internal resource will be copied and not the objects.
MyObject A = B;
2)或者您只想复制没有花哨语义的字段。
void operator=(const MyObject& v) {
this->someField = v->someField;
}
// So this statement should generate just one function call and not a fancy
// collection of temporaries that require construction destruction.
MyObject A = B;
在这两种情况下,代码都运行得更快。在第二种情况下,效果是相似的。
3)还有类型...使用运算符来处理将其他类型分配给您的类型。
void operator=(const MyObject& v) {
this->someField = v->someField;
}
void operator=(int x) {
this->value = x;
}
void operator=(float y) {
this->floatValue = y;
}
首先,请注意“破坏旧对象,然后复制构造新对象”不是异常安全的。
但是重新“复制新对象的构造,然后与旧对象交换,然后销毁旧对象”,这是实现赋值运算符的交换习惯,如果正确完成,它是异常安全的。
在某些情况下,自定义赋值运算符可能比交换习语更快。例如,POD 类型的直接数组不能真正交换,除非通过较低级别的分配。因此对于交换习语,您可以预期与数组大小成正比的开销。
然而,从历史上看,并没有太多关注交换和异常安全。
bjarne 最初想要例外(如果我没记错的话),但直到 1989 年左右它们才进入该语言。所以最初的 c++ 编程方式更侧重于分配。在某种程度上,失败的构造函数通过将 0 分配给this
……来表示其失败,我认为,在那些日子里,你的问题是没有意义的。这只是所有的任务。
类型方面,一些对象具有标识,而另一些对象具有价值。分配给值对象是有意义的,但是对于标识对象,通常希望限制可以修改对象的方式。虽然这不需要自定义复制分配的能力(只是使其不可用),但有了这种能力,就不需要任何其他语言支持。
我也认为出于任何其他可以想到的具体原因:可能没有这样的原因真的需要通用能力,但是通用能力足以涵盖所有内容,因此它降低了整体语言的复杂性。
获得比我的预感、回忆和直觉更明确的答案的一个很好的来源是 bjarne 的“c++ 的设计和演变”一书。
可能这个问题在那里有一个明确的答案。
破坏旧对象,然后复制构造新对象,通常是行不通的。除非类提供特殊的交换函数,否则交换习惯用法保证不会起作用std::swap
——在其非专门实现中使用赋值,并且直接在赋值运算符中使用它会导致无限递归。
当然,用户可能想要做一些特别的事情,例如将赋值运算符设为私有。
最后,几乎可以肯定的是,一个根本原因是:默认赋值运算符必须与 C 兼容。
实际上,在看到 juanchopanza 的答案(已被删除)之后,我想我最终自己弄清楚了。
复制赋值运算符允许类在可以重用资源(在本例中为内存)时 避免不必要地分配资源basic_string
。
因此,当您分配给 a 时basic_string
,重载的复制分配运算符将避免分配内存,而只会将字符串数据直接复制到现有缓冲区。
如果必须销毁并再次构造对象,则必须重新分配缓冲区,这对于小字符串来说可能成本更高。
(请注意,这vector
也可以从中受益,但前提是它知道元素的复制构造函数永远不会抛出异常。否则它需要维护其异常安全并实际执行复制和交换。)
它还允许您使用其他类型的分配。
你可以有一个Person类,它带有一个分配 ID 的赋值运算符。
但除此之外,您并不总是想照原样复制所有成员。
默认分配只做浅拷贝。
例如,如果类包含指针或锁,您并不总是希望从其他对象复制它们。
通常,当您有指针时,您想使用深拷贝,并且可能创建指针指向的对象的副本。
如果你有锁,你希望它们特定于对象,并且你不想从另一个对象复制它们的状态。
如果您的类将指针作为成员,则提供您自己的复制构造函数和赋值运算符实际上是一种常见的做法。
我经常将它用作转换构造函数,但已经存在对象。即分配成员变量类型等给一个对象。