Ross Bencina
Ross Bencina

Reputation: 4193

Best practice for implementing an interface-like pure abstract base class in C++?

I would like to declare a pure abstract base class with a virtual destructor. I know three ways to do this, but I do not know which is best, or why.

My goal is to implement abstract base class interfaces in best-practice C++11 style, with optimal runtime performance. In particular, I would like to afford inlining/elimination of no-op destructors. I'd also like to eliminate warnings related to duplicate vtables, either by choosing an implementation that doesn't generate the duplicat vtables, or by making an informed decision to suppress the warnings.

Here are the three ways to implement an abstract base class that I know:

Option #1

/// A.h:

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

Option #2

/// A.h:

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

/// A.cpp:

A::~A() {}

Option #3

/// A.h:

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

Are these my only options?

Which of #1, #2, #3, is considered best practice? If there are trade-offs (e.g. runtime vs. compile time performance) please describe them.

With option #1, will the inline destructor ever be inlined?

I understand that option #1 will put a vtable into every translation unit. Option #1 generates the -Wweak-vtables warning in clang, and is covered by the "vague linkage" category in gcc[1]. Option #3 does not generate the clang warning -- does this mean that option #3 does not generate a vtable?

How exactly does option #3 differ from the other options?

Other questions have discussed similar issues with respect to clang's warnings, but I was unable to find a question that specifically addressed which option is considered best practice and why.

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

Upvotes: 2

Views: 1639

Answers (1)

Richard Hodges
Richard Hodges

Reputation: 69912

best practice (at least when I'm in charge):

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;

};

Why is it important in your opinion to have a try-catch block for f()? – einpoklum 16 mins ago

@einpoklum I'm glad you asked. Because if you do this in every method and every function, and throw a nested exception containing the function name (and any relevant arguments), it means that when you finally catch the exception, you can unwrap all the nested exceptions into your log file or cerr and you get a perfect stack trace pointing exactly at the problem.

reference for unwrapping nested exceptions:

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

doesn't that hurt performance?

Not one little bit.

But it's a pain to add a function try block to every function

It's a much bigger pain to have to try to reproduce a problem that you have no idea how or why it occurred, and no clue as to the context. Trust me on this...

Upvotes: 1

Related Questions