Skip to content →

Updating the Lucena Utilities Library for C++17, Part 2—Feature Detection

This is the continuation of a series on the Lucena Utilities Library (LUL).

The backbone of any sort of compile-time abstraction is a set of feature-detection flags. More generally, it’s a set of feature-detection tests, but ideally, you want to run the tests once, store the results, and then just refer to those forever after. These sorts of tests are necessary because different compilers and Standard Library implementations support different subsets of the developing C++ Standard; furthermore, they all have differing methods of communicating this information to the user. Note that determining feature availability is separate from detecting feature correctness—even if the former were completely standardized, the latter would still necessarily require case-by-case management—and good feature detection should handle both.

Standardized Feature-Detection Flags

The formal effort to standardize feature testing is in the form of WG21’s sixth Standing Document (SD-6, <>). It is intended to supersede similar efforts in pretty much every C++ library that ever needed to run in more than one environment. It’s built on three key features: the __has_include preprocessor function, which reports the presence of the requested header file; the __has_cpp_attribute preprocessor function, which simply reports support for the requested attribute; and the very large and evolving set of __cpp_XXX macros, split between identifying compiler features and library features. __has_include proved to be non-controversial, and was quickly supported by all of our target compilers. The rest of the SD-6 features, though, have been a mess.

Identifying support for Standard Library changes is not as simple as looking for a header. One example is Apple’s libc++ implementation’s support for features like std::shared_mutex and std::optional. For various reasons, Apple includes the llvm headers for these, but does not always provide binaries for the constructor definitions of various bits of code. For that you have to query the appropriate _LIBCPP_AVAILABILITY macro; these macros track Apple’s OS SDKs as a proxy for whether the supplied runtime library contains the required object code. Unfortunately, these macros do not track the equivalent __cpp_XXX macros, where such macros are even defined.

This raises a larger issue: the Standard implementation vendors’ loathing to couple the compiler and the Standard Library. Their concerns are reasonable. The naive approach to defining feature detection flags would be to have the compiler always supply them, but the compiler, being non-magical, needs a way to query the library, and since the library being used is theoretically arbitrary, it would help if that was done in a standard way. Unfortunately, SD-6, the closest thing to an actual standard, does not provide much guidance here: header writers have a half-hearted recommendation to define feature macros in the affected header. This implies that library feature detection must be three-phase: first, call __has_include on a header, then include the header, then check the __cpp_XXX macro. This can be a bit heavy, depending on context, and in practice can cause some compile-time failures, for example if the included header barfs up some #error calls because a header isn’t actually supported by the current configuration for some reason. This latter issue is easy to work around, but the workaround essentially requires all the old-school boilerplate of the feature test we’re trying to simplify.

All this is to say that SD-6 is a lovely idea, but it’s currently worthless for general use. Feature testing will rely on traditional methods for the time being, and places where we attempted to use controversial portions of SD-6 will be converted.

Flag Updates

When the Library was first scoped out, it made sense to determine how backward-compatible it should be, as this would affect its design. While it seems that feature detection should be straightforward—run some tests to determine when a feature is supported, and then consider it supported from then on—in practice, it can get very convoluted. The poster child for long-term multi-compiler support for all the dusty corners of C++ is Boost (<>), and the situation there shows just how ugly things can get: features pop in and out of existence, detection methods change, whole implementations can get replaced. This leads to the pragmatic question of, “Whose needs is the Library serving?” All things considered, it makes sense to support configurations for actively supported client cases, but it makes little sense to leave historical configurations in as they just increase code complexity and will eventually increase the testing burden unreasonably.

With this in mind, the new support cut-off is demarcated by full compliance with the C++14 Standard. As such, it’s not necessary to track granular feature support before C++17 (though an exception is made for C++11 garbage collection support, as it’s optional). Additionally, the minimum supported language target is C++17. Code must be compiled with whatever switch(es) will enable C++17 or higher; our feature detection is intended primarily to determine commonality between target platforms or, in the case of library features, places where we need to supplement the Standard Library with reference implementations. All the old tests for individual C++11 and C++14 features—aside from the aforementioned garbage collection—have been stripped out. However, a large number of C++17 and C++2a tests need to be added; the go-to sources for identifying distinct “features” are SD-6 and the cppreference compiler support page (<>), which aggregates data from a number of sources we’d originally tracked individually.

We also track C99 and C11 feature support, as it‘s not unusual to bring C code in to a C++ code base (though this is not always the transparent operation it’s often treated as). Most of C99’s non-optional bits have made their way into C++, but we track support for the C99 preprocessor specifically; Visual Studio, to this day, does not properly implement it, although it is finally a design priority and should be available within the next year. C11 support is largely vestigial, as LUL was originally put together at a time when it looked like C++ and C would track each other a bit more closely. Instead, C11 offers a number of alternative ways to get some C++11-equivalent features into C; this will probably end up getting removed after a review prior to shipping the Library update.

Next Time

In the next post, I’ll talk about preprocessor macro updates and discuss the mechanism for referencing new Standard Library features that fall outside of our common denominator subset.

Published in Programming Projects