This question is intended as a follow up question to this one: What are the differences between a pointer variable and a reference variable in C++?
Having read the answers and some further discussions I found on stackoverflow I know that the compiler should treat pass-by-reference the same way it treats pass-by-pointer and that references are nothing more than syntactic sugar. One thing I haven't been able to figure out yet if there is any difference considering binary compatibility.
In our (multiplatform) framework we have the requirement to be binary compatible between release and debug builds (and between different releases of the framework). In particular, binaries we build in debug mode must be usable with release builds and vice versa. To achieve that, we only use pure abstract classes and POD in our interfaces.
Consider the following code:
class IMediaSerializable
{
public:
virtual tResult Serialize(int flags,
ISerializer* pSerializer,
IException** __exception_ptr) = 0;
//[…]
};
ISerializer
and IException
are also pure abstract classes. ISerializer
must point to an existing object, so we always have to perform a NULL-pointer check. IException
implements some kind of exception handling where the address the pointer points to must be changed. For this reason we use pointer to pointer, which must also be NULL-pointer checked.
To make the code much clearer and get rid of some unnecessary runtime checks, we would like to rewrite this code using pass-by-reference.
class IMediaSerializable
{
public:
virtual tResult Serialize(int flags,
ISerializer& pSerializer,
IException*& __exception_ptr) = 0;
//[…]
};
This seems to work without any flaws. But the question remains for us whether this still satisfies the requirement of binary compatibility.
UPDATE: To clarify things up: This question is not about binary compatibility between the pass-by-pointer version of the code and the pass-by-reference version. I know this can't be binary compatible. In fact we have the opportunity to redesign our API for which we consider using pass-by-reference instead of pass-by-pointer without caring about binary compatibilty (new major release). The question is just about binary compatibility when only using the pass-by-reference version of the code.
Generally references are implemented as pointers under-the-hood, so there will usually be ABI compatibility. You will have to check your particular compiler's documentation and possibly implementation to make sure.
However, your restriction to pure-abstract classes and POD types is over zealous in the age of C++11.
C++11 split the concept of pod into multiple pieces. Standard Layout covers most, if not all, of the "memory layout" guarantees of a pod type.
But Standard Layout types can have constructors and destructors (among other differences).
So you can make a really friendly interface.
Instead of a manually managed interface pointer, write a simple smart pointer.
that
->clone()
s on copy, moves the pointer on move, deletes on destroy, and (because you own it) can be guaranteed to be stable over compiler library revisions (whileunique_ptr
cannot). This is basically aunique_ptr
that supports->clone()
. Also have your ownunique_ptr
for values that cannot be duplicated.Now you can replace your pure virtual interfaces with a pair of types. First, the pure virtual interface (with a
T* clone() const
usually), and second a regular type:the end result is you have a type that behaves like a regular, everyday type, but it is implemented as a wrapper around a pure virtual interface class. Such types can be taken by value, taken by reference, and returned by value, and can hold arbitrary complex state within them.
These types live in header files that the library exposes.
And interface expansion of
IFoo
is fine. Just add a new method to both theIFoo
at the end of the type (which under most ABIs is backward compatible (!) -- try it), then add a new method tomy_regular_foo
that forwards to it. As we did not change the layout of ourmy_regular_foo
, even though the library and the client code may disagree about what methods it has, that is fine -- those methods are all compiled inline and never exported -- and clients who know they are using the newer version of your library can use it, and those who do not know but are using it are fine (without rebuilding).There is one careful gotcha: if you add an overload to
IFoo
of a method (not an override: an overload) the order of the virtual methods changes, and if you add a newvirtual
parent the layout of the virtual table can change, and this only works reliably if all inheritance to your abstract classes isvirtual
in your public API (with virtual inheritance, the vtable has pointers to the start of each vtable of the sub-classes: so each sub-class can have a bigger vtable without messing up the address other functions virtual functions. And if you carefully only append to the end of a sub-class vtable code using the earlier header files can still find the earlier methods).This last step -- allowing new methods on your interfaces -- might be a bridge to far, as you'd have to investigate the ABI guarantees (in practice and not) on vtable layout for every supported compiler.