【问题标题】:Best practice for implementing an interface-like pure abstract base class in C++?在 C++ 中实现类似接口的纯抽象基类的最佳实践?
【发布时间】:2016-04-02 08:08:59
【问题描述】:

我想声明一个带有虚拟析构函数的纯抽象基类。我知道三种方法可以做到这一点,但我不知道哪种方法最好,或者为什么。

我的目标是以最佳实践 C++11 风格实现抽象基类接口,并具有最佳运行时性能。特别是,我想提供无操作析构函数的内联/消除。我还想消除与重复 vtable 相关的警告,方法是选择不生成重复 vtable 的实现,或者做出明智的决定来抑制警告。

以下是我所知道的实现抽象基类的三种方式:

选项#1

/// A.h:

class A {
public:
    virtual ~A() {}
    virtual int f() = 0;
};

选项 #2

/// A.h:

class A {
public:
    virtual ~A();
    virtual int f() = 0;
};

/// A.cpp:

A::~A() {}

选项#3

/// A.h:

class A {
public:
    virtual ~A() = default;
    virtual int f() = 0;
};

这些是我唯一的选择吗?

#1、#2、#3 中的哪一个被认为是最佳实践?如果有权衡(例如运行时与编译时性能),请描述它们。

使用选项#1,内联析构函数是否会被内联?

我了解选项 #1 会将 vtable 放入每个翻译单元。选项 #1 在 clang 中生成 -Wweak-vtables 警告,并包含在 gcc[1] 中的“模糊链接”类别中。选项 #3 不会生成 clang 警告——这是否意味着选项 #3 不会生成 vtable?

选项 #3 与其他选项有何不同?

其他问题已经讨论了关于 clang 警告的类似问题,但我无法找到具体解决哪个选项被认为是最佳实践以及为什么的问题。

[1]https://gcc.gnu.org/onlinedocs/gcc/Vague-Linkage.html

【问题讨论】:

  • CppCoreGuidelines 有一个关于此类问题的好部分,可能值得您阅读。
  • 我做科学计算,在我的领域,这些变体都没有用于多态性。对于真正对性能至关重要的任务,请查看 static polymorphismBarton-Nackman-Trick
  • @Maikel 是的,我也使用这些技术。但是这个问题是关于普通的基于 vtable 的多态性技术。
  • 那对不起,我只是希望能帮上忙。好吧,核心指南建议使用变体 C,但我认为您所要求的详细程度并未对其进行解释。 :-(
  • 您实际上/技术上还有第四个选项,尽管它对您没有多大帮助。在 A.cpp 中的选项#2 中,您可以使用 A::~A() = default; 而不是 A::~A() {}

标签: c++ c++11


【解决方案1】:

最佳实践(至少在我负责时):

struct A {

    //
    // keep move semantics available - rule of 0, 3, or 5
    // in this case, 5 because we defined a destructor.
    //
    A(const A&) = default;
    A(A&&) = default;
    A& operator=(const A&) = default;
    A& operator=(A&&) = default;
    virtual ~A() = default;

    // non-polymorphic interface in terms of private polymorphic
    // implementation

    int f() 
    try
    {
        // possibly lock a mutex here?
        // possibly some setup code, logging, bookkeeping?
        return f_impl();
    }
    catch(const std::exception& e) {
        // log the nested exception hierarchy
        std::throw_with_nested(std::runtime_error(__func__));
    }   

private:

    virtual int f_impl() = 0;

};

为什么在您看来为 f() 设置一个 try-catch 块很重要? – einpoklum 16 分钟前

@einpoklum 很高兴你问我。因为如果你在每个方法和每个函数中都这样做,并抛出一个包含函数名(和任何相关参数)的嵌套异常,这意味着当你最终捕获异常时,你可以将所有嵌套异常解包到你的日志文件中或cerr 并且您会得到一个完美的堆栈跟踪,准确地指向问题。

解包嵌套异常的参考:

http://en.cppreference.com/w/cpp/error/throw_with_nested

这不会影响性能吗?

一点也不。

但是给每个函数都加一个函数try块很痛苦

不得不尝试重现一个您不知道它是如何发生或为什么发生的问题,并且对上下文没有任何线索,这是一个更大的痛苦。相信我……

【讨论】:

  • 为什么在您看来为f() 设置一个try-catch 块很重要?
  • @einpoklum 很高兴你问我。因为如果你在每个方法和每个函数中都这样做,并抛出一个包含函数名(和任何相关参数)的嵌套异常,这意味着当你最终捕获异常时,你可以将所有嵌套异常解包到你的日志文件中或cerr 并且您会得到一个完美的堆栈跟踪,准确地指向问题。
  • 您已经解释了首选嵌套异常而不是普通异常的原因。但为什么要例外呢?没有人说 f() 一定会抛出任何东西。您是否建议在基类/mixin 的所有虚拟方法中使用 try-catch 块?
  • @einpoklum 方法 f 未标记为 noexcept。因此我们必须假设它可以抛出异常。
  • 问题的一个重要部分涉及析构函数的定义。你能否澄清一下为什么你选择= default析构函数形式,以及为什么你准备放弃vtable锚定(假设你是)。
猜你喜欢
  • 2012-02-28
  • 2010-12-28
  • 1970-01-01
  • 2017-07-23
  • 1970-01-01
  • 2020-09-03
  • 2017-03-21
  • 1970-01-01
  • 2012-07-17
相关资源
最近更新 更多