【问题标题】:Extending the C++ Standard Library by inheritance?通过继承扩展 C++ 标准库?
【发布时间】:2010-11-07 14:53:14
【问题描述】:

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

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

  1. 是否有任何其他标准库类打算派生自?

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

【问题讨论】:

    标签: c++ stl


    【解决方案1】:

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

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

    • 如果它没有任何virtual 方法,那么您不应该将它用作基础。这排除了std::vector 等。
    • 如果它确实有 virtual 方法,那么它是用作基类的候选对象。
    • 如果有很多 friend 语句四处游荡,请避开,因为可能存在封装问题。
    • 如果它是一个模板,那么在继承它之前仔细观察,因为您可能可以使用专门化来自定义它。
    • 基于策略的机制(例如,std::char_traits)的存在是一个很好的线索,您不应该将其用作基础。

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

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

    【讨论】:

    • 你提到不从没有虚拟成员的类继承——但你也提到了策略类。那么(例如)从分配器(私下)继承有什么问题?
    • 我要补充一点,有些东西是为了通过继承进行少量扩展,例如std::stackstd::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 newoperator 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 本身可能会根据实现抛出。

                    【讨论】:

                      猜你喜欢
                      • 2015-02-27
                      • 2014-02-15
                      • 2017-11-23
                      • 1970-01-01
                      • 2012-12-17
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多