通过继承扩展 C++ 标准库?
Posted
技术标签:
【中文标题】通过继承扩展 C++ 标准库?【英文标题】:Extending the C++ Standard Library by inheritance? 【发布时间】:2009-07-02 11:58:37 【问题描述】:人们普遍认为 C++ 标准库通常不打算使用继承进行扩展。当然,我(和其他人)批评了建议从 std::vector
等课程派生的人。然而,这个问题:c++ exceptions, can what() be NULL? 让我意识到标准库中至少有一部分是打算如此扩展的 - std::exception
。
所以,我的问题分为两部分:
是否有任何其他标准库类打算派生自?
如果确实派生自诸如std::exception
之类的标准库类,那么是否受ISO 标准中描述的接口的约束?例如,使用 what()
成员函数不返回 NTBS(比如返回空指针)的异常类的程序是否符合标准?
【问题讨论】:
【参考方案1】:好问题。我真的希望标准能更明确地说明预期用途。也许应该有一个与语言标准并列的 C++ 基本原理文档。无论如何,这是我使用的方法:
(a) 我不知道存在任何此类列表。相反,我使用以下列表来确定标准库类型是否可能被设计为继承自:
如果它没有任何virtual
方法,那么您不应该将它用作基础。这排除了std::vector
等。
如果它确实有 virtual
方法,那么它是用作基类的候选对象。
如果有很多 friend
语句四处游荡,请避开,因为可能存在封装问题。
如果它是一个模板,那么在从它继承之前仔细观察,因为您可能可以使用特化来自定义它。
基于策略的机制(例如,std::char_traits
)的存在是一个很好的线索,您不应该将其用作基础。
不幸的是,我不知道一个很好的综合或黑白列表。我通常凭直觉。
(b) 我会在这里申请LSP。如果有人在您的异常上调用what()
,那么它的可观察行为应该与std::exception
的行为匹配。我不认为这真的是一个标准一致性问题,而是一个正确性问题。该标准不要求子类可以替代基类。这实际上只是一个“最佳实践”。
【讨论】:
您提到不从没有虚拟成员的类继承——但您也提到了策略类。那么(例如)从分配器(私下)继承有什么问题? 我要补充一点,有些东西是为了通过继承进行少量扩展,例如std::stack
和std::queue
,因为它们具有受保护,因为唯一的保护事物的原因是允许子类读取数据。显然,你需要非常小心地处理这个问题。【参考方案2】:
a) 流库被继承:)
【讨论】:
【参考方案3】:关于您的 b 部分,来自 17.3.1.2 “要求”,第 1 段:
该库可以通过 C++ 程序进行扩展。如果适用,每个条款都描述了此类扩展必须满足的要求。此类扩展通常是以下之一:
模板参数 派生类 满足接口约定的容器、迭代器和/或算法
虽然 17.3 提供信息而不是约束,但委员会对派生类行为的意图是明确的。
对于其他非常相似的扩展点,有明确的要求:
17.1.15 “必需的行为”涵盖替换(operator new 等)和处理程序函数(终止处理程序等),并将所有不合规行为抛入 UB-land。 17.4.3.6/1:“在某些情况下(替换函数、处理函数、对用于实例化标准库模板组件的类型的操作),C++ 标准库依赖于 C++ 程序提供的组件。如果这些组件不满足他们的要求,标准对实施没有任何要求。”最后一点,我不清楚括号中的列表是否详尽,但考虑到下一段中提到的每个案例的具体处理方式,如果说当前文本旨在涵盖派生类,那就有点牵强了.此外,17.4.3.6/1 文本在 2008 年草案中没有改变(它在 17.6.4.8 中),我没有看到 issues 解决它或派生类的虚拟方法。
【讨论】:
【参考方案4】:C++ 标准库不是一个单一的单元。它是组合和采用几个不同库的结果(一大块 C 标准库、iostreams 库和 STL 是三个主要构建块,并且每个都已独立指定)
如您所知,库的 STL 部分通常并不意味着派生自。它使用泛型编程,通常避免 OOP。
IOStreams 库是更传统的 OOP,并且在内部大量使用继承和动态多态 --- 并且希望用户使用相同的机制来扩展它。自定义流通常是通过派生自流类本身或其内部使用的streambuf
类来编写的。这两个都有可以在派生类中重写的虚拟方法。
std::exception
是另一个例子。
就像 D.Shawley 所说,我会将LSP 应用于您的第二个问题。将基类替换为派生类应该始终是合法的。如果我调用exception::what()
,它必须遵循exception
类指定的约定,无论exception
对象来自何处,或者它是否实际上是一个被向上转换的派生类。在这种情况下,该合同是标准对返回 NTBS 的承诺。如果您使派生类的行为有所不同,那么您将违反标准,因为 std::exception
类型的对象不再返回 NTBS。
【讨论】:
【参考方案5】:回答问题2):
我相信是的,它们会受到 ISO 标准的接口描述的约束。例如,该标准允许在全局范围内重新定义 operator new
和 operator delete
。但是,该标准保证operator delete
是对空指针的无操作。
不尊重这一点肯定是未定义的行为(至少对 Scott Myers 而言)。我想我们可以说标准库的其他领域也是如此。
【讨论】:
【参考方案6】:functional
中的一些内容,如greater<>
、less<>
和mem_fun_t
是从unary_operator<>
和binary_operator<>
派生的。但是,IIRC,这只会给你一些 typedef。
【讨论】:
【参考方案7】:简约规则是“任何类都可以用作基类;在没有虚方法(包括虚析构函数)的情况下安全使用它的责任完全由派生作者负责。”在 std::exception 的子类中添加非 POD 成员与在 std::vector 的派生类中的用户错误相同。容器不是“打算”成为基类的想法是文学教授所说的“作者意图谬误”的工程示例。
IS-A 原则占主导地位。不要从 B 派生 D,除非 D 可以在 B 的公共接口的各个方面替代 B,包括对 B 指针的删除操作。如果 B 有虚方法,这个限制就不那么繁琐了;但是,如果 B 只有非虚方法,那么对继承进行特化仍然是可能的,也是合法的。
C++ 是多范式的。模板库使用继承,甚至从没有虚拟析构函数的类继承,因此通过示例证明这种构造是安全且有用的;它们是否是故意的,这是一个心理问题。
【讨论】:
【参考方案8】:对于第二个问题,我相信答案是肯定的。标准规定 std::exception 的 what 成员必须返回非 NULL 值。如果我有一个指向 std::exception 的堆栈、引用或指针值,这无关紧要。 what() 的返回受标准约束。
当然可以返回 NULL。但我会认为这样的类不符合标准。
【讨论】:
【参考方案9】:我知道这个问题很老,但我想在这里添加我的评论。
从现在开始,我使用从 std::string 继承的class CfgValue
,尽管文档(或一些书或一些标准文档,我现在手头没有源)说,用户不应该继承来自 std::string。并且这个类包含一个 “TODO:从 std::string 中删除继承” 多年以来的注释。
Class CfgValue 只是添加了一些构造函数和setter 和getter 来在字符串和数字和布尔值之间快速转换,以及从utf8(保存在std::string)到ucs2(保存在std::wstring)编码等等.
我知道,有许多不同的方法可以做到这一点,但是对用户来说非常方便,而且效果很好(这意味着,它不打破任何标准库概念、异常处理等):
class CfgValue : public std::string
public:
...
CfgValue( const int i ) : std::string() SetInteger(i);
...
void SetInteger( int i );
...
int GetInteger() const;
...
operator std::wstring() return utf8_to_ucs16(*this);
operator std::wstring() const return utf8_to_ucs16(*this);
...
;
【讨论】:
【参考方案10】:w.r.t 问题 2),根据 C++ 标准,派生的异常类必须指定无抛出,即 throw() 规范以及返回非空值。这意味着在许多情况下,派生的异常类不应使用 std::string,因为 std::string 本身可能会根据实现抛出。
【讨论】:
以上是关于通过继承扩展 C++ 标准库?的主要内容,如果未能解决你的问题,请参考以下文章