Let's consider as close to an apples to apples comparison as we can, meaning you are using this base class only to derive it one time in a class that's not publicly visible and discounting the extensibility of this solution. The sole purpose of using an abstraction here is to hide one concrete implementation, nothing more (as with the pimpl case).
So we have something like this (abstract interface):
// In some header file:
class IFoo
{
public:
virtual ~IFoo() {}
virtual void do_something() = 0;
};
std::unique_ptr<IFoo> create_foo();
// Inside source file (hidden from the outside world):
class Foo: public IFoo
{
...
};
std::unique_ptr<IFoo> create_foo()
{
return std::unique_ptr<IFoo>(new Foo);
}
vs. Pimpl:
// In some header file:
class Foo
{
public:
Foo();
Foo(const Foo& other);
Foo& operator=(const Foo& other);
~Foo();
void do_something();
private:
struct Impl;
std::unique_ptr<Impl> priv;
};
// Inside source file (hidden from the outside world):
struct Foo::Impl
{
...
};
Foo::Foo(): priv(new Impl)
{
}
...
In Common
What these both have in common is that they serve the same fundamental premise: they hide the implementation details from the outside world, they reduce compile-time dependencies.
The abstract interface arguably does this ever so slightly more, not even exposing a Pimpl at all, though typically this is getting into really moot pedantic territory. It might be a favorable aesthetic to some, making only the public interface/documentation visible in the header (if header aesthetics are valued).
Both suffer from the need for dynamic allocation, an extra pointer, and disjointed memory representation from pointer to pointee as well as a potential loss of spatial locality from one pointee to the next. The memory inefficiency can be mitigated considerably for both cases by a fixed allocator to pool chunks of memory from a contiguous pool for pointees to use.
Different
Some things different between these two:
- The abstract interface version requires a dynamic (aka virtual) dispatch overhead per member function call. This may be moot for deeper pipelines where a reasonable amount of work is done in a member function. It may turn into a minor hotspot for teeny member functions called in huge numbers (ex: millions and millions of times) in tight loops for some simple accessor function (both due to shallow pipeline and the optimization barrier which prevents inlining). I generally wouldn't worry about this too much pre-measurements, since you always have the option of turning
IFoo
from a pure interface into an abstract base class and inlining some critical functions if absolutely necessary without redesigning the whole thing.
- The abstract version requires a
vptr
overhead per instance and a vtable
to be generated per class.
- The pimpl version can provide value/copy/move semantics directly without the need for yet another wrapper (the abstract version would require it, along with either virtual methods like
clone
or external functions like clone
that are implemented inside the source file where Foo
is visible).
- The abstract version is arguably a little more straight-forward to implement (and arguably also read), as it lets you avoid dealing with managing this separate pimpl storing the implementation details and just lets you define a new class and override some virtual functions all in one go.
- The abstract version is a little more flexible (ex: we can have two or more separate concrete implementations of
IFoo
living in the same codebase). This is getting away from striving for the apples to apples comparison, however, and noting more that this is apples to oranges.
- The abstract version has a more fragile ABI, as adding any new virtual function anywhere will break the vtable ABI. This is only important in contexts like software development kits used by plugin developers where the binaries aren't all being built/distributed together.
I think that covers the main differences. Note that object slicing isn't a concern here provided that IFoo
cannot be instantiated on its own.