关于stl:通过继承扩展C ++标准库?

Extending the C++ Standard Library by inheritance?

通常认为C++标准库一般不打算使用继承来扩展。当然,我(和其他人)批评了那些建议从诸如std::vector这样的阶级衍生出来的人。但是,这个问题:C++异常,可以(null)吗?使我认识到,标准库中至少有一个部分打算如此扩展——std::exception

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

  • 有没有其他的标准库类可以从中派生?

  • 如果一个源于标准库类(如std::exception类),那么它是否受ISO标准中描述的接口绑定?例如,一个使用what()成员函数的异常类的程序没有返回NTB(比如它返回了一个空指针)是否符合标准?


  • 很好的问题。我真的希望标准能更明确地说明预期的用途。也许应该有一个与语言标准并驾齐驱的C++基础文档。在任何情况下,下面是我使用的方法:

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

    • 如果它没有任何virtual方法,那么您不应该将它用作基础。这排除了std::vector等。
    • 如果它确实有virtual方法,那么它可以作为基类使用。
    • 如果有大量的friend语句在周围浮动,那么应该避开,因为可能存在封装问题。
    • 如果它是一个模板,那么在继承之前仔细观察,因为您可能可以用专门化来定制它。
    • 基于政策的机制(如std::char_traits的存在)是一个很好的线索,表明你不应该把它作为基础。

    不幸的是,我不知道一个好的综合或黑白名单。我通常凭直觉去做。

    (b)我会在这里申请LSP。如果有人对你的例外情况打电话给what(),那么它的可观察行为应该与std::exception的一致。我不认为这是一个真正的标准一致性问题,而是一个正确性问题。标准不要求子类可替换为基类。这真的只是一个"最佳实践"。


    a)流库被设置为继承:)


    关于B部分,从17.3.1.2"要求"第1段:

    The library can be extended by a C++ program. Each clause, as applicable, describes the requirements that such extensions must meet. Such extensions are generally one of the following:

    • Template arguments
    • Derived classes
    • Containers, iterators, and/or algorithms that meet an interface convention

    虽然17.3是信息性的而不是约束性的,但委员会对派生类行为的意图是明确的。

    对于其他非常相似的扩展点,有明确的要求:

    • 17.1.15"所需行为"包括替换(操作员新建等)和处理程序功能(终止处理程序等),并将所有不合规行为抛入UB土地。
    • "在某些情况下(替换函数、处理函数、用于实例化标准库模板组件的类型的操作),C++标准库依赖于C++程序提供的组件。如果这些组件不符合它们的要求,标准就不会对实现提出任何要求。"

    在最后一点上,我不清楚附加说明列表是详尽的,但是考虑到在下一段中如何具体处理每一个提到的案例,如果说当前的文本旨在涵盖派生类,那将是一种延伸。此外,2008年草案(17.6.4.8)中的17.4.3.6/1文本没有改变,我认为解决IT或派生类的虚拟方法没有问题。


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

    正如您所知,库的stl部分通常不打算派生自。它使用通用编程,并且通常避免OOP。

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

    std::exception是另一个例子。

    就像D.Shawley说的,我会把LSP应用到你的第二个问题上。用派生类替换基类应该总是合法的。如果我称之为exception::what(),它必须遵循exception类指定的契约,无论exception对象来自何处,或者它是否实际上是一个已被提升的派生类。在这种情况下,该合同是标准的承诺,即退回NTB。如果使派生类的行为不同,那么您将违反该标准,因为类型为std::exception的对象不再返回ntb。


    简约规则是"任何类都可以用作基类;在没有虚拟方法(包括虚拟析构函数)的情况下安全使用它的责任完全是派生作者的责任。"在std::exception的子级中添加非pod成员与在std::vector的派生类中添加非pod成员是相同的用户错误。容器不是"有意"作为基础课程的想法是文学教授所谓的权威意图谬误的工程实例。

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

    C++是多范式的。模板库使用继承,甚至是来自没有虚拟析构函数的类的继承,因此通过示例证明了这种构造是安全和有用的;它们是否是有意的是一个心理问题。


    functional中的一些东西,如greater<>less<>mem_fun_t是从unary_operator<>binary_operator<>衍生而来的。但是,IIRC,这只会给你一些typedef。


    回答问题2):

    我相信是的,它们将受到ISO标准的接口描述的约束。例如,该标准允许在全球重新定义operator newoperator delete。但是,标准保证operator delete是对空指针的no操作。

    不尊重这一点当然是不明确的行为(至少对斯科特·迈尔斯来说)。我认为我们可以通过对标准库的其他领域的类比来说明这一点。


    对于第二个问题,我认为答案是肯定的。标准规定std::exception的what成员必须返回非空值。不管我是否有一个堆栈、引用或指向std::exception的指针值。返回的what()由标准绑定。

    当然可以返回空值。但我认为这样的一个等级是不符合标准的。


    我知道这个问题由来已久,但我想在此添加我的评论。

    从现在开始,我使用了一个从std::string继承的class CfgValue,尽管文档(或者一些书或一些标准文档,我现在没有现成的源)说,用户不应该从std::string继承。这个类包含一个"todo:remove inheritance from std::string"注释。

    类cfgValue只是添加了一些构造函数、setter和getter,以便在字符串和数值和布尔值之间快速转换,以及从utf8(保留在std::string中)转换为ucs2(保留在std::wstring中)编码等。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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); }
        ...
    };

    W.R.T.问题2),根据C++标准,派生异常类必须指定一个不抛出即抛出()和返回非空。这意味着在许多情况下,派生的异常类不应使用std::string,因为std::string本身可能会根据实现抛出。