jeudi 1 mars 2018

Transitioning away from std::string, std::ostream, etc. in a library's public API

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