Why should one not derive from c++ std string class?
我想问一个具体的点,在有效的C++。
上面写着:
A destructor should be made virtual if a class needs to act like a polymorphic class. It further adds that since
std::string does not have a virtual destructor, one should never derive from it. Alsostd::string is not even designed to be a base class, forget polymorphic base class.
号
我不明白在一个类中有什么特别的要求才有资格成为一个基类(而不是多态类)?
我不应该从
此外,如果有一个纯粹为可重用性而定义的基类,并且有许多派生类型,那么有没有任何方法可以防止客户端执行
我认为这句话反映了这里的困惑(强调我的):
I do not understand what specifically is required in a class to be eligible for being a base clas (not a polymorphic one)?
在惯用的C++中,从类派生有两种用法:
- 私有继承,用于混合和使用模板的面向方面编程。
- 公共继承,仅用于多态情况。编辑:好吧,我想这也可以用在一些混合场景中——比如
boost::iterator_facade ,当crtp使用时就会出现。
如果你不想做一些多态的事情,完全没有理由在C++中公开一个类。该语言带有作为该语言标准特性的自由函数,您应该在这里使用自由函数。
这样想——您真的想强迫代码的客户机转换为使用一些专有的字符串类,仅仅是因为您想附加一些方法吗?因为与Java或C语言(或大多数类似的面向对象语言)不同,当您在C++中派生类时,基类的大多数用户需要知道这种类型的更改。在爪哇/C语言中,类通常是通过引用访问的,它们类似于C++的指针。因此,有一个间接的层次涉及到分离类的客户机,允许您在不知道其他客户机的情况下替换派生类。
但是,在C++中,类是值类型——不像大多数其他OO语言。最简单的方法就是所谓的切片问题。基本上,考虑:
1 2 3 4 5 6 7 8 9 10 | int StringToNumber(std::string copyMeByValue) { std::istringstream converter(copyMeByValue); int result; if (converter >> result) { return result; } throw std::logic_error("That is not a number."); } |
如果将自己的字符串传递给此方法,则将调用
长话短说——不要用继承来粘贴C++中的方法。这不是惯用语,会导致语言问题。尽可能使用非友元、非成员函数,然后是组合。不要使用继承,除非你是模板元编程或者想要多态行为。有关更多信息,请参见Scott Meyers的"有效C++项目23:首选非成员非友元函数"到"成员函数"。
编辑:下面是一个更完整的例子,展示了切片问题。你可以在codepad.org上看到它的输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | #include <ostream> #include <iomanip> struct Base { int aMemberForASize; Base() { std::cout <<"Constructing a base." << std::endl; } Base(const Base&) { std::cout <<"Copying a base." << std::endl; } ~Base() { std::cout <<"Destroying a base." << std::endl; } }; struct Derived : public Base { int aMemberThatMakesMeBiggerThanBase; Derived() { std::cout <<"Constructing a derived." << std::endl; } Derived(const Derived&) : Base() { std::cout <<"Copying a derived." << std::endl; } ~Derived() { std::cout <<"Destroying a derived." << std::endl; } }; int SomeThirdPartyMethod(Base /* SomeBase */) { return 42; } int main() { Derived derivedObject; { //Scope to show the copy behavior of copying a derived. Derived aCopy(derivedObject); } SomeThirdPartyMethod(derivedObject); } |
提供一般性建议的反面(当没有明显的详细性/生产力问题时,这是合理的)。好的。合理使用方案
至少有一种情况下,没有虚拟析构函数的基础的公共派生可能是一个好的决策:好的。
- 您需要专用用户定义类型(类)提供的一些类型安全性和代码可读性优势。
- 现有的基础非常适合存储数据,并且允许客户机代码也希望使用的低级操作。
- 您希望能够方便地重用支持该基类的函数
- 您理解,您的数据逻辑上需要的任何附加不变量只能在代码中强制执行,代码显式地将数据作为派生类型访问,并且取决于在您的设计中"自然"发生的程度,以及您可以信任客户端代码以理解和配合逻辑上理想的不变量的程度,您可以希望派生类的成员函数重新验证期望(和throw或其他)
- 派生类添加了一些在数据上操作的高度类型特定的方便函数,例如自定义搜索、数据过滤/修改、流式处理、统计分析,(可选)迭代器
- 客户机代码与基的耦合比与派生类的耦合更合适(因为基要么是稳定的,要么是对它的更改反映了对功能的改进,同时也是派生类的核心)
- 换一种方式:您希望派生类继续公开与基类相同的API,即使这意味着客户端代码被强制更改,而不是以某种方式将其隔离,从而使基API和派生API不同步。
- 在负责删除基对象和派生对象的部分代码中,您不会混合指向它们的指针。
这听起来可能有很大的限制,但在现实世界中,有很多程序与此场景匹配。好的。背景讨论:相对优势
编程就是妥协。在编写更具概念性的"正确"程序之前:好的。
- 考虑它是否需要增加复杂度和代码来模糊实际的程序逻辑,因此总体上更容易出错,尽管处理一个特定问题更为可靠,
- 权衡实际成本与问题的可能性和后果,以及
- 考虑一下"投资回报率"以及你可以利用你的时间做什么。
如果潜在的问题涉及到对象的使用,而您只是无法想象任何人试图根据您对对象在程序中的可访问性、范围和使用性质的了解来尝试使用这些对象,或者您可以生成编译时错误以供危险使用(例如,派生类大小与基大小匹配的断言,这将阻止添加新的数据内存)。否则任何其他的事情都可能是过早的工程。以简洁、直观、简洁的设计和代码轻松取胜。好的。型考虑派生sans虚拟析构函数的原因
假设您有一个从B公开派生的类D,那么不费吹灰之力,就可以在D上进行操作(除了构造,但是即使有很多构造函数,您也可以通过为每个不同数量的构造函数参数使用一个模板来提供有效的转发:例如
此外,如果b更改了,那么默认情况下d会暴露这些更改-保持同步-但可能需要有人检查d中引入的扩展功能,以查看它是否仍然有效,以及客户端的使用情况。好的。型
换言之:基类和派生类之间的显式耦合减少,但基类和客户端之间的耦合增加。好的。型
这往往不是你想要的,但有时它是理想的,而其他时候则是无问题的(见下一段)。对基础的更改会迫使更多的客户机代码在分布在整个代码基础中的位置进行更改,有时更改基础的人甚至可能无法访问客户机代码来相应地查看或更新它。但有时情况会更好:如果作为派生类提供者的"中间人",您希望基类的更改能够传递给客户机,并且您通常希望客户机能够(有时是被迫的)在基类更改时更新其代码,而无需您经常参与,那么公共派生可能是理想的。当类本身不是一个独立的实体,而是一个加在基上的细值时,这是很常见的。好的。型
其他时候,基类接口是如此稳定,以至于耦合可能被认为是无问题的。对于像标准容器这样的类尤其如此。好的。型
总之,公共派生是一种快速获取或近似派生类理想的、熟悉的基类接口的方法-以一种对维护者和客户机编码者都简明且自明无误的方式-具有作为成员函数可用的附加功能(imho-与sutter,Alexandrescu e明显不同tc-可以帮助可用性、可读性和提高生产力的工具,包括ides)好的。型C++编码标准-萨特和AlxEngRESCU-CONS检查
C++编码标准的第35项列出了从EDCOX1(1)派生的场景中的问题。随着场景的发展,它很好地说明了公开一个大型但有用的API的负担,但是好的和坏的都是非常稳定的,因为基础API是标准库的一部分。稳定的基础是一种常见的情况,但不比不稳定的基础更常见,良好的分析应该与这两种情况相关。在考虑这本书的问题清单的同时,我将具体对比一下这些问题对以下案例的适用性:好的。
a)
(希望我们能同意这种组合是可接受的实践,因为它提供了封装、类型安全以及比
所以,假设您正在编写一些新的代码,并从OO的角度开始考虑概念实体。也许在一个bug跟踪系统中(我想说的是jira),其中之一就是说一个问题ID。数据内容是文本的-由一个字母项目ID、一个连字符和一个递增的问题编号组成:例如"myapp-1234"。问题ID可以存储在一个
关于萨特和亚历山德里斯科的问题清单…好的。
Nonmember functions work well within existing code that already manipulates
string s. If instead you supply asuper_string , you force changes through your code base to change types and function signatures tosuper_string .Ok.
这个声明(以及下面的大多数声明)的根本错误是,它促进了仅使用少数类型的便利性,而忽略了类型安全的好处。它表达了对上述d)的偏好,而不是对c)或b)作为a)的替代品的洞察。编程艺术涉及到平衡不同类型的优缺点,以实现合理的重用、性能、便利性和安全性。下面的段落对此进行了详细说明。好的。
通过使用公共派生,现有代码可以隐式地将基类
因此,除非非成员支持函数被清理或扩展,而代价是将其与新代码紧密耦合,否则就不需要触及它。如果正在对它进行大修以支持问题ID(例如,使用对数据内容格式的洞察,只支持大写字母字符),那么通过创建一个重载al a
在任何地方使用
Interface functions that take a string now need to: a) stay away from
super_string 's added functionality (unuseful); b) copy their argument to a super_string (wasteful); or c) cast the string reference to a super_string reference (awkward and potentially illegal).Ok.
这似乎是在重新访问需要重构以使用新功能的第一点旧代码,尽管这次是客户机代码而不是支持代码。如果函数希望开始将其参数视为与新操作相关的实体,那么它应该开始将其参数视为该类型,并且客户机应该生成它们并使用该类型接受它们。构图也存在着完全相同的问题。否则,如果遵循下面列出的指导方针,
super_string's member functions don't have any more access to string's internals than nonmember functions because string probably doesn't have protected members (remember, it wasn't meant to be derived from in the first place)
Ok.
是的,但有时候这是件好事。许多基类没有受保护的数据。公共的
If
super_string hides some ofstring 's functions (and redefining a nonvirtual function in a derived class is not overriding, it's just hiding), that could cause widespread confusion in code that manipulatesstring s that started their life converted automatically fromsuper_string s.Ok.
对于组合也是如此——而且更可能发生,因为代码不会默认地传递信息,从而保持同步,在某些情况下,对于运行时多态层次结构也是如此。在最初看起来可互换的类中表现不同的samed命名函数-只是令人讨厌。这实际上是正确OO编程的常见注意事项,同样也不是放弃类型安全等好处的充分理由。好的。
What if
super_string wants to inherit fromstring to add more state [explanation of slicing]Ok.
同意——这不是一个好的情况,在某个地方,我个人倾向于划一条线,因为它经常通过指向基础的指针将删除问题从理论领域转移到非常实际的领域——析构函数不会被其他成员调用。尽管如此,切片通常可以做想要做的事情——假设派生
Admittedly, it's tedious to have to write passthrough functions for the member functions you want to keep, but such an implementation is vastly better and safer than using public or nonpublic inheritance.
Ok.
好吧,当然同意单调的生活……好的。成功派生sans虚拟析构函数的指南
- 理想情况下,避免在派生类中添加数据成员:切片的变体可能会意外地删除数据成员,损坏它们,无法初始化它们…
- 更重要的是,避免非pod数据成员:通过基类指针删除在技术上是未定义的行为,但如果非pod类型无法运行其析构函数,则更有可能出现资源泄漏、引用计数错误等非理论问题。
- 尊重Liskov替换原则/你不能坚定地保持新的不变量
- 例如,在从
std::string 派生时,不能截取一些函数并期望对象保持大写:任何通过std::string& 或...* 访问它们的代码都可以使用std::string 的原始函数实现来更改值) - 派生以在应用程序中为更高级别的实体建模,使用某些功能扩展继承的功能,这些功能使用但不与基冲突;不要期望或尝试更改基类型授予的基本操作以及对这些操作的访问
- 例如,在从
- 注意耦合:在不影响客户端代码的情况下不能删除基类,即使基类发展为具有不适当的功能,也就是说,派生类的可用性取决于基础的持续适当性。
- 有时,即使使用组合,由于性能、线程安全问题或缺少值语义,您也需要公开数据成员-因此,从公共派生中丢失封装并不会明显更糟。
- 使用潜在派生类的人越可能不知道它的实现妥协,就越不可能让它们变得危险。
- 因此,与程序员在应用程序级和/或"私有"实现/库中常规使用功能的本地使用相比,具有许多临时用户的低级别广泛部署的库应该更小心危险的派生。
总结
这种推导并非没有问题,因此除非最终结果证明了方法的合理性,否则不要考虑它。也就是说,我断然拒绝任何在特定情况下不能安全、适当地使用它的说法——这只是一个在哪里划界的问题。好的。个人经验
我确实有时会从
不仅析构函数不是虚拟的,std::string根本不包含虚拟函数,也不包含受保护的成员。这使得派生类很难修改其功能。
那你为什么要从中得出结论呢?
非多态性的另一个问题是,如果将派生类传递给需要字符串参数的函数,则额外的功能将被切掉,对象将再次被视为普通字符串。
如果您真的想从中派生(不讨论您为什么要这样做),我认为您可以通过使它成为
1 2 3 4 5 6 | class StringDerived : public std::string { //... private: static void* operator new(size_t size); static void operator delete(void *ptr); }; |
但是这样你就可以限制自己不受任何动态的
Why should one not derive from c++ std string class?
因为这是不必要的。如果您想使用
Is there any way to prevent client from doing
Base* p = new Derived()
对。确保在
1 2 3 4 | class Derived : protected Base { // 'protected' to avoid Base* p = new Derived const char* c_str () const { return Base::c_str(); } //... }; |
不从非多态类派生的原因有两个:
- 技术:它介绍切片错误(因为在C++中,除非另有说明,否则我们通过值)
- 功能性:如果它是非多态的,你可以通过组成和一些功能转发来达到相同的效果。
如果您希望向
如果您希望添加新的数据成员,那么通过将类访问(组合)嵌入到您自己设计的类中来正确包装类访问。
编辑:
@托尼正确地注意到,我引用的功能性原因对大多数人来说可能毫无意义。在好的设计中有一个简单的经验法则,那就是当你可以在几个方案中选择一个方案时,你应该考虑耦合较弱的方案。组合具有较弱的继承耦合,因此在可能的情况下应首选。
此外,构图还为您提供了很好地包装原始类方法的机会。如果您选择继承(public),并且方法不是虚拟的(这里就是这种情况),这是不可能的。
一旦您将任何成员(变量)添加到派生的std::string类中,如果您试图将std goodies与派生的std::string类的实例一起使用,您会系统地拧紧堆栈吗?因为stdc++函数/成员的堆栈指针[索引]固定[并调整]为(base std::string)实例大小的大小/边界。
对吗?
如果我错了,请纠正我。
C++标准声明,如果基类析构函数不是虚的,则删除基类的对象,该对象指向派生类的对象,然后导致未定义的行为。
C++标准节5.3.5/3:
如果操作数的静态类型与其动态类型不同,则静态类型应为操作数动态类型的基类,静态类型应具有虚拟析构函数或行为未定义。
明确虚拟析构函数的非多态类和需求使析构函数虚拟化的目的是通过删除表达式促进对象的多态删除。如果没有对象的多态删除,则不需要虚拟析构函数。
为什么不从字符串类派生?一般来说,应该避免从任何标准容器类派生,因为它们没有虚拟析构函数,这使得不可能以多态方式删除对象。至于字符串类,字符串类没有任何虚拟函数,因此没有任何可以重写的内容。你所能做的就是隐藏一些东西。
如果您想要有一个类似字符串的功能,那么您应该编写自己的类,而不是从std::string继承。