Duck-typing API changes in C++

You may have run across this situation before: your project is dependent on a third-party library. Someone modifies the library with a breaking Application Programming Interface (API) change but the library is not strict about semantic versioning. It seems there is no way for you to conditionally compile code that depends on the library API. Your project can work with one API the library presents, but not both.

But there is a way, with C++ template metaprogramming! The Substitution Failure Is Not An Error (SFINAE) paradigm for templated functions allows you to force the compiler to choose a version of your project’s code that will compile with the matching API presented by the library, while not causing errors when methods are missing. You can use template argument deduction rules[1] and partial ordering of overloaded function templates[2] to prefer the new API when it is available; and decltype() and std::is_same to detect which API the library provides at compile time.

This approach of ignoring the type of objects in favor of using their behavior to determine their proper use is called duck typing. The name comes from the archetypal example: rather than asking whether you’ve been given a duck, simply observe whether it behaves like a duck (i.e., quacks and walks with a waddle).

Our solution will wrap two or more versions of your project’s code (the pieces that must vary, anyway) inside a structure templated on the third-party’s API-breaking class(es). Whichever version the compiler prefers is how we know what’s a duck!

Of course, just because this post shows you how to work without access to proper versioning macros doesn’t mean you should give up versioning macros! Kitware is working on automating our own versioning macros, so even large projects like VTK that do not use semantic versioning will at least provide some way to discern what API a set of header files provide.

Types of API changes

Let’s see what a breaking API change looks like. There are two principal ways that things change:

  • Method Variations: A method is removed, the method name changes, a method is broken into several calls that must now be made in series (or vice versa, where several calls are now replaced with one); and
  • Signature Variations: A method accepts parameters of different types, or a different number of parameters, or different parameter values than it did before.

Changes within each category above have similar solutions, while the two categories sometimes require different approaches. For our purposes, let’s consider a single class that has two breaking API changes: one in each category above.

Let’s suppose that we had been using the library’s Example class before it changed like so:

We can’t just switch to

because that would break previous versions. And we don’t want to keep using deprecated (or non-existent) API; even if the old methods exist they may contain bugs that justified the API change in the first place. Instead, we’ll create a templated API “adaptor” structure that will call the proper methods for us:

The next sections look at what the APIAdaptor implementations must look like to work for both Example API changes above.

Method variations

If the name of a method or the number/order of method invocations changes in your third-party library, you can use SFINAE to force some templated methods on the adaptor to fail to compile. However, when these methods do compile, they are written so as to be preferred over the version that works with the old API.

Signature variations

When the signature of a method changes in the library’s API — rather than its name — we cannot always use the same pattern above because the decltype(&Test::bar) specification of our templated function’s parameter type exists in both the old and new API — it just has a different signature. While SFINAE still applies, it is possible that implicit type conversion could make the overloading ambiguous. In these cases, we can use some newer compiler and library features:

Conclusions

Now you have a couple different ways to adapt to API changes in third-party libraries. Although the adaptors above might seem like a lot of code, once you remove the comments they are pretty concise.

The full example source code is below. You can compile and run it against the “before” version of the Example class like so:

and against the “after” version of the Example class like so:

References

[1]: Template argument deduction rules are discussed in Effective Modern C++ by Scott Meyers, chapter 1 and on cppreference.com.

[2]: Some discussion of partial template ordering is on cppreference.com. However, the language specification is the only place we’ve found that thoroughly goes over the rules. It’s just hard to read. If you are really dedicated, take a look at the c++11 specification, section 14.8.2.4 Deducing template arguments during partial ordering [temp.deduct.partial] available in draft form here.

Example Code

One Response to Duck-typing API changes in C++

  1. Dan Lipsa says:

    Very cool!

Questions or comments are always welcome!

X