Designing an API can be difficult and it is very unlikely you will get the design right from the start. One of the problems is that the header files of your library establish a contract between you and the users of your library. Once you publish a new version of your library, the function prototypes are set in stone and your users will simply expect them to work in future versions, as long as the major version number remains the same. At the same time you want to prevent stalling minor design improvements for the next major release of your library just because you forgot one additional argument for one of the functions. Preferably, you don't want to release a major overhaul of your library either, as not all users of your library are willing to spend the effort to migrate to the new version, or they simply don't have the time to do so.
One of the options is to simply introduce a new function or type with an incremented version number or a specific suffix indicating that the function or type is an extension of an older function or
struct in the library.
This is commonly seen in WinAPI, where many new functions have been introduced with
2 as a suffix.
Similarly, netapi32 knows many different versions of the same C structure, e.g.
While this allows you to preseve backward compatibility in a straight-forward approach, it has the drawback that this heavily pollutes the namespace and might cause confusion among developers in the sense that it becomes less obvious to determine which functions they should be using.
Therefore another solution is to simply mark functions as deprecated while introducing a new API. In gcc and Clang, this can be done by specifying the deprecated function attribute:
__attribute__((deprecated)) int foo(void);
A similar function attribute is available in Microsoft Visual C/C++ as well:
__declspec(deprecated) int foo(void);
This allows developers to easily figure out which functions they should be using and gives them some time to migrate to the new API before the functions get removed in a later release.
Another much more radical approach is to simply remove the old APIs or to replace them with a new one that is completely different immediately, instead of marking the old API as being deprecated. This approach is commonly seen for APIs on Mac OS X with the intention of ensuring that developers will be using all the latest features in their applications. While this might sound like a great idea, this will usually infuriate most developers instead.
In the remainder of this blog post we will be looking at yet another approach.
As mentioned before, in most cases it is simply not possible to plan out the entire design of your library before implementing it. Preferably, you want to introduce new parts of the API in small iterative steps, but how do we communicate to the user that these changes are still a work in progress, and hence experimental?
Both gcc and Clang offer function attributes that will cause the compiler to generate a custom warning or error message whenever the user tries to call a function.
In gcc, we can use
In Clang we can use
In Microsoft Visual C/C++, it is only possible to generate a warning by using
Using these function attributes, we can provide the user the option of using our experimental API, to generate warnings when using our experimental API or to simply restrict the API to the stable subset.
In the following code snippet, we allow the user to define
WARN_EXPERIMENTAL=1 on the command line or in a Makefile to indicate that he or she wants to use the experimental API and optionally to generate warning for every experimental function call:
#ifdef __clang__ #if defined(WARN_EXPERIMENTAL) && WARN_EXPERIMENTAL #define EXPERIMENTAL_API \ __attribute__((deprecated("may be subject to change"))) #elif defined(USE_EXPERIMENTAL) && USE_EXPERIMENTAL #define EXPERIMENTAL_API #else #define EXPERIMENTAL_API \ __attribute__((unavailable("may be subject to change"))) #endif #elif defined __GNUC__ #if defined(WARN_EXPERIMENTAL) && WARN_EXPERIMENTAL #define EXPERIMENTAL_API \ __attribute__((warning("may be subject to change"))) #elif defined(USE_EXPERIMENTAL) && USE_EXPERIMENTAL #define EXPERIMENTAL_API #else #define EXPERIMENTAL_API \ __attribute__((error("may be subject to change"))) #endif #elif defined _MSC_VER #if !defined(USE_EXPERIMENTAL) && USE_EXPERIMENTAL #define EXPERIMENTAL_API __declspec(deprecated("may be subject to change")) #else #define EXPERIMENTAL_API #endif #else #define EXPERIMENTAL_API #endif
To further support compilers that do not provide such function attributes, it is recommended to move the experimental API to its own header with a check that will generate an error whenever the header is being included. While not as fine-grained as using the function attributes, it still allows for similar functionality. This can be done by adding the following check at the top of your header:
#if !defined(USE_EXPERIMENTAL) && USE_EXPERIMENTAL #error attempt to include functions that may be subject to change #endif
With these changes in place, we can now communicate which functions are experimental to the user by simply specifying
EXPERIMENTAL_API in front of the function prototype:
EXPERIMENTAL_API int foo(void);
Once the API has been finalised, we can then move the functions to appropriate headers and omit the
Essentially allowing us to roll out new parts of the API while it is still a work in progress.