BC API tricks

Making C++ API changes Backwards Compatible

This project contains “tricks” on how to make backwards compatible API changes. (Eg. renaming a class, changing parameter types, converting an enum to a variant etc.)

It contains tests (see /tests) to ensure that these tricks are indeed backwards compatible and showcase what cases (if any) will they break.

Some of them also have negative tests (see /neg-tests) to showcase some rare cases where the API change breaks code that previously compiled, even with the “trick”.

Contributing

This project just got created.

Please feel free to create Issues and Pull Requests to improve this list.

Warning

These tricks assume the users of your API don’t require ABI compatibility, or use forward declarations or function pointer aliases for your types (In general, they shouldn’t forward declare foreign types).

Project internals

/some_unstable_lib contains a header file for each API change in the list further down. Each header either exposes the old API or the changed one based on the macro BC_API_CHANGED.

/tests represents the users of the library whose code must compile before and after the API change.

/neg-tests contains code that should not compile after the API change. One file per each such case.

All tests are run twice:

  1. with the macro BC_API_CHANGED OFF (before the API change)
  2. and ON (after).

.github/workflows/deploy.yml pushes new changes to this README to Github Pages.

Overview

How to:

Rename a namespace

Initial code:

namespace path::to::v1 { ... }

Scenario:

We maybe need to change the namespace name to fix a typo. We will change it from path::to::v1 to path::to::v2.

Solution:

Rename the old namespace to the new one and add a namespace alias for the old one.

+ namespace path::to::v2 {}
+ namespace path::to {
+   namespace v1 = path::to::v2;
+ }
+ 
- namespace path::to::v1 { ... }
+ namespace path::to::v2 { ... }

Remarks:

Relevant Files:

Rename a type

Initial code:

struct OldName { ... };

Scenario:

We maybe need to update the struct name to fix a typo. We will change it to NewName.

Solution:

We can use a type alias.

- struct OldName { ... };
+ struct NewName { ... };
+ using OldName = NewName;

Remarks:

Relevant Files:

Rename a header

Initial code:

// v1/OldName.hpp:
...

Scenario:

We need to rename the header to v2/NewName.hpp.

Solution:

  1. Rename the header:
- // v1/OldName.hpp:
+ // v2/NewName.hpp:
...
  1. Create a compatibility header file in the old location that includes the renamed one.
// v1/OldName.hpp: <- created to only include the renamed header + deprecation notice
#include "v2/NewName.hpp"

// You can also deprecate it by inserting a compilation warning:
// #warning OldName.hpp is deprecated, include "v2/NewName.hpp".`
// Don't use #error since there is no way for users to silence it.

Remarks:

Rename using your versioning tool (Git/SVN) so you don’t lose blame history. For Git, do the change 2 steps in 2 different commits.

Change default parameters

Initial code:

void SomeMethod(
    int mandatory,
    bool opt1 = false,
    float opt2 = 1e-6f,
    int opt3 = 42
) { ... }

Scenario:

This method receives too many default parameters, and it only becomes harder for users to call it with only 1 or 2 parameters changed. We need to change the method to receive a struct containing these parameters instead.

Solution:

If you would just overload SomeMethod with the default parameters changed, users calling SomeMethod with just the mandatory parameters will now have the compiler complain about ambiguity (that it doesn’t know which of the 2 methods to call).

To tell the compiler to prefer the newer method we need to make the old one less specialized by making it a template.

+ template<int = 0>
void SomeMethod(
    int mandatory,
    bool opt1 = false,
    float opt2 = 1e-6f,
    int opt3 = 42
+ ) {
+  // Call the new implementation now
+  SomeMethod(mandatory, SomeMethodOpts{opt1, opt2, opt3});
+ }
+ 
+ struct SomeMethodOpts { bool opt1 = false; float opt2 = 1e-6; int opt3 = 42; };
+ void SomeMethod(
+     int mandatory,
+     SomeMethodOpts opts = {}
) { ... }

Remarks:

template<> DLL_EXPORT void SomeMethod<0>(
    int mandatory, bool opt1 = false, float opt2 = 1e-6f, int opt3 = 42);

Relevant Files:

Change the return type

Warning:

Prefer to just add a new method called slightly different instead. What’s about to follow is over-engineered.

In short: we will overload the implicit cast operator of the returned type, and if the returned type is a primitive, we will create a new type that wraps it.

Initial code:

// (1) change some primitive `T` to `NewUserDefT`
bool CheckPassword(std::string);

// (2) change some primitive `const T&` to primitive `T`
struct Strukt {
  const float& GetMemF() const { return m_memF; }
private:
  float m_memF;
};

Scenario:

  1. CheckPassword method returns true if it succeeds, otherwise false. Make this method return some meaningful error message so the user knows why it failed (why it returned false).

  2. Strukt::GetMemF returns a primitive type as const& which is bad for multiple reasons (performance, lifetime, complexity issues). We need to return by value.

Unfortunately, we cannot just overload a function by return type and then deprecate it.

Solution:

For situation (1): Add operator bool() so that the new type can be implicitly casted to bool.

// (1) change primitive `T` to `NewUserDefT`
+ struct CheckPasswordResult { // mimics std::expected<void, std::string>
+     operator bool() const { return !m_errMsg.has_value(); }
+     const std::string& error() const { return m_errMsg.value(); }
+ private:
+     std::optional<std::string> m_errMsg;
+ };
- bool CheckPassword(std::string);
+ CheckPasswordResult CheckPassword(std::string);

For situation (2): Add a new class GetterRetT with 2 implicit cast operators to NewRetT and to OldRetT. “Mark” the implicit cast operator to OldRetT as deprecated and as “less specialized” (i.e. as template, so that the compiler will choose at “overload resolution” the NewRetT overload).

Additionally, inside the Strukt return GetterRetT by const& so that we avoid runtime exceptions from dangling references in user’s code in case they have a StruktWrapper class that also has a const float& GetMemF() that called and returned the result of our GetMemF().

// (2) change primitive `const T&` to primitive `T`
+ struct GetterRetT {
+   template <int = 0> // (2.1)
+   operator OldRetT () const { ... }
+   operator NewRetT () const { ... }
+ };

struct Strukt {
-   const float& GetMemF() const { return m_memF; }
+   const GetterRetT& GetMemF() const { return m_memF; }
private:
-   float m_memF = 3.f;
+   GetterRetT m_memF = 3.f;
};

Relevant Files:

Change old-style enum to enum class

Warning:

Changing the enum to enum class will inherently breaks implicit conversions to integers (e.g. when the enum is used as bit flags: STYLE_BOLD | STYLE_ITALLIC results in a int).

Initial code:

enum Style {
    STYLE_BOLD,
    STYLE_ITALLIC,
    STYLE_STRIKE_THROUGH,
};

Scenario:

We need to modernize the API to use enum class instead.

Solution:

In order to not break scoped uses of the enum (e.g. auto style = Style::STYLE_BOLD) we will duplicate the enum fields with the enum class’s naming style, and make sure their value is assigned to the old enum fields.

In order to not break unscoped uses of the enum (e.g. auto style = STYLE_BOLD), we will define static variables for each enum entry.

- enum Style {
+ enum class Style {
+   Bold,
+   Itallic,
+   StrikeThrough,
-   STYLE_BOLD,
-   STYLE_ITALLIC,
-   STYLE_STRIKE_THROUGH,
+   STYLE_BOLD = Bold,
+   STYLE_ITALLIC = Itallic,
+   STYLE_STRIKE_THROUGH = StrikeThrough,
};

+ static inline Style STYLE_BOLD = Style::Bold;
+ static inline Style STYLE_ITALLIC = Style::Itallic;
+ static inline Style STYLE_STRIKE_THROUGH = Style::StrikeThrough;

Remarks:

// Add `friend` if the enum lies inside a `struct`
[friend] inline Style operator|(Style lhs, Style rhs) {
    return static_cast<Style>(static_cast<int>(lhs) | static_cast<int>(rhs));
}

Relevant Files:

Other reasonably safe changes:

TODOs