For API/ABI compatibility, it is well known that STL containers, std::string
, and other standard library classes like iostreams are verboten in public headers.
If one already had a published library API that did not follow this rule (asking for a friend), what is the best path forward while maintaining as much backwards compatibility as I reasonably can and favoring compile-time breakages where I can't? I need to support Windows and Linux.
I have three cases with two proposed solutions for comment and one request for help below.
Old public API:
// Case 1: Non-virtual functions with containers
void Foo( const char* );
void Foo( const std::string& );
// Case 2: Virtual functions
class Bar
{
public:
virtual ~Bar() = default;
virtual void VirtFn( const std::string& );
};
// Case 3: Serialization
std::ostream& operator << ( std::ostream& os, const Bar& bar );
Case 1: Non-virtual functions with containers
In theory we can convert std::string
uses to a class very much like std::string_view
but under our library's API/ABI control. It will convert within our library header from a std::string
so that the compiled library still accepts but is independent of the std::string
implementation and is backwards compatible:
New API:
class MyStringView
{
public:
MyStringView( const std::string& ) // Implicit and inline
{
// Convert, possibly copying
}
MyStringView( const char* ); // Implicit
// ...
};
void Foo( MyStringView ); // Ok! Mostly backwards compatible
Most client code that is not doing something abnormal like taking the address of Foo
will work without modification. Likewise, we can create our own std::vector
replacement, though it may incur a copying penalty in some cases.
Abseil's ToW #1 recommends starting at the util code and working up instead of starting at the API. Any other tips or pitfalls here?
Case 2: Virtual functions
But what about virtual functions? We break backwards compatibility if we change the signature. I suppose we could leave the old one in place with final
to force breakage:
// Introduce base class for functions that need to be final
class BarBase
{
public:
virtual ~BarBase() = default;
virtual void VirtFn( const std::string& ) = 0;
};
class Bar : public BarBase
{
public:
void VirtFn( const std::string& str ) final
{
VirtFn( MyStringView( str ) );
}
// Add new overload, also virtual
virtual void VirtFn( MyStringView );
};
Now an override of the old virtual function will break at compile-time but calls with std::string
will be automagically converted. Overrides should use the new version instead and will break at compile-time.
Any tips or pitfalls here?
Case 3: Serialization
I'm not sure what to do with iostreams. Halp!
Aucun commentaire:
Enregistrer un commentaire