通过继承扩展C ++标准库?

时间:2009-07-02 11:58:38

标签: c++ stl

人们普遍认为,C ++标准库通常不打算使用继承进行扩展。当然,我(和其他人)批评那些建议来自std::vector等课程的人。但是,这个问题:c++ exceptions, can what() be NULL?让我意识到标准库中至少有一部分旨在如此扩展 - std::exception

所以,我的问题有两个部分:

  1. 是否有其他标准库类可以派生自?

  2. 如果一个派生自标准库类,例如std::exception,是否受ISO标准中描述的接口约束?例如,使用what()成员函数的异常类没有返回NTBS(比如它返回空指针)的程序是标准符合吗?

10 个答案:

答案 0 :(得分:38)

好问题。我真的希望标准对于预期用途更加明确。也许应该有一个与语言标准并列的C ++ Rationale文档。无论如何,这是我使用的方法:

(a)我不知道是否存在任何此类清单。相反,我使用以下列表来确定标准库类型是否可能被设计为继承自:

  • 如果它没有任何virtual方法,那么您不应该将它用作基础。这排除了std::vector之类的内容。
  • 如果它有virtual方法,则它可以作为基类使用。
  • 如果有很多friend语句浮出水面,那么就要明确,因为可能存在封装问题。
  • 如果它是一个模板,那么在继承它之前要仔细看看,因为你可以用专门化来定制它。
  • 基于策略的机制(例如std::char_traits)的存在是一个非常好的线索,您不应该将其用作基础。

不幸的是,我不知道一个很好的综合或黑白列表。我通常会感到直觉。

(b)我会在这里申请LSP。如果有人在您的异常上调用what(),则其可观察行为应与std::exception的行为相匹配。我认为这不是一个标准一致性问题,而是一个正确性问题。标准不要求子类可替代基类。它实际上只是一个“最佳实践”

答案 1 :(得分:17)

a)使流库继承:)

答案 2 :(得分:7)

关于你的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解决它或派生类的虚拟方法。

答案 3 :(得分:5)

C ++标准库不是一个单元。它是组合和采用几个不同库的结果(C标准库的一大块,iostreams库和STL是三个主要构建块,并且每个都是独立指定的)

如您所知,库的STL部分通常不是来自。它使用泛型编程,通常避免使用OOP。

IOStreams库是更传统的OOP,并且在内部使用继承和动态多态 - 并且期望用户使用相同的机制来扩展它。自定义流通常通过从流类本身或其内部使用的streambuf类派生来编写。这两个都有可以在派生类中重写的虚方法。

std::exception是另一个例子。

和D.Shawley说的一样,我会将LSP应用于你的第二个问题。将基类替换为派生类应始终是合法的。如果我调用exception::what(),它必须遵循exception类指定的合同,无论exception对象来自何处,或者它是否实际上是已被上升的派生类。在这种情况下,该合同是返回NTBS的标准承诺。如果您使派生类的行为不同,那么您违反了标准,因为std::exception类型的对象不再返回NTBS。

答案 4 :(得分:4)

回答问题2):

我相信是的,他们会受到ISO标准的界面描述的约束。例如,该标准允许全局重新定义operator newoperator delete。但是,该标准保证operator delete对空指针无操作。

不尊重这一点肯定是未定义的行为(至少对Scott Myers而言)。我想我们可以说,对标准库的其他领域进行类比也是如此。

答案 5 :(得分:4)

functional中的某些内容(如greater<>less<>mem_fun_t)来自unary_operator<>binary_operator<>。但是,IIRC,只给你一些typedef。

答案 6 :(得分:4)

简约规则是“任何类都可以用作基类;在没有虚方法的情况下安全地使用它,包括虚拟析构函数,完全是派生作者的。”在std :: exception的子节点中添加非POD成员与在std :: vector的派生类中的用户错误相同。容器不是“打算”作为基类的想法是文学教授称之为“权威意图谬误”的工程实例。

IS-A原则占主导地位。不要从B派生D,除非D在B的公共接口中可以在每个方面替换B,包括对B指针的删除操作。如果B有虚拟方法,这种限制就不那么繁重了;但是如果B只有非虚方法,那么继承专业化仍然是合法的。

C ++是多范式的。模板库使用继承,甚至从没有虚拟析构函数的类继承,因此通过示例演示这样的构造是安全且有用的;他们是否打算是一个心理问题。

答案 7 :(得分:3)

对于第二个问题,我认为答案是肯定的。标准说std :: exception的成员必须返回非NULL值。如果我有std :: exception的堆栈,引用或指针值,那应该没关系。 what()的返回受标准约束。

当然可以返回NULL。但我认为这样的课程符合非标准。

答案 8 :(得分:1)

w.r.t问题2),根据C ++标准,派生的异常类必须指定无抛出,即 throw()规范以及返回非null。这意味着在许多情况下派生的异常类不应该使用std :: string,因为std :: string本身可能会抛出,具体取决于实现。

答案 9 :(得分:1)

我知道这个问题已经过时了,但我想在这里添加我的评论。

几年以来,我使用了一个继承自std :: string的class CfgValue,虽然文档(或者一些书或一些标准文档,我现在没有源代码)说,用户应该不从std :: string继承。此类包含“TODO:从std :: string中删除继承”注释多年。

类CfgValue只是添加一些构造函数和setter和getter来快速转换字符串和数值和布尔值,并从utf8(保存在std :: string)转换为ucs2(保存在std :: wstring)编码等等

我知道,有许多不同的方法可以做到这一点,但对用户来说非常方便,并且它工作正常(这意味着,它没有打破任何stdlib概念,异常处理等):

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); }
    ...
};