Binary compatibility when using pass-by-reference instead of pass-by-pointer
Asked Answered
P

3

6

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.

Patty answered 12/2, 2015 at 13:22 Comment(3)
Even if it is binary compatible, the function names would be mangled differently, hence it will not link.Verecund
Clarified the question, I hope the problem becomes clear now.Patty
POD is gone (well broken apart) in C++11: now we have standard layout, which allows a lot of power in-class. You could pass regularized complex objects about as reliably as POD types.Dysprosium
C
6

Binary ABI compatibility is determined by whatever compiler you are using. The C++ standard does not cover the issue of binary ABI compatibility.

You will need to check your C++ compiler's documentation, to see what it says about binary compatibility.

Cosma answered 12/2, 2015 at 13:28 Comment(0)
D
3

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.

template<class T>
struct value_ptr {
  T* raw;
  // ...      
};

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 (while unique_ptr cannot). This is basically a unique_ptr that supports ->clone(). Also have your own unique_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:

struct my_regular_foo {
  value_ptr< IFoo > ptr;
  bool some_method() const { return ptr->some_method(); } // calls pure virtual method in IFoo
};

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 the IFoo at the end of the type (which under most ABIs is backward compatible (!) -- try it), then add a new method to my_regular_foo that forwards to it. As we did not change the layout of our my_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 new virtual parent the layout of the virtual table can change, and this only works reliably if all inheritance to your abstract classes is virtual 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.

Dysprosium answered 12/2, 2015 at 15:46 Comment(1)
I wish I would've taken a closer into generalized POD in C++11, it would have solved some of our API problems in the first place. To learn more generalized POD and have a deeper understanding for your suggestions I read some basics on Wikipedia and on Bjarnes excellent C++11 FAQ. Digging deeper thanks to your answer.Patty
V
2

No, it will not work regardless of which compiler you are using.

Consider a class Foo that exports two functions:

class Foo
{
public:
     void f(int*);
     void f(int&);
};

The compiler has to convert (mangles) the names of the two functions f to a ABI-specific string, so that the linker can distinguish between the two.

Now, since the compiler needs to support overload resolution, even if references were implemented exactly like pointers, the two function names will need to have a different mangled name.

For example GCC mangles these names to:

void Foo::f(int*) => _ZN3Foo1fEPi
void Foo::f(int&) => _ZN3Foo1fERi

Notice P vs R.

So if you change the signature of the function your application will fail to link.

Verecund answered 12/2, 2015 at 14:4 Comment(1)
Clarified the question, I hope the problem becomes clear now.Patty

© 2022 - 2024 — McMap. All rights reserved.