Component of the Week #13: bsls_compilerfeatures

Summary:
  • Provide a set of preprocessor macros to detect the current compiler, C++ version, and its supported features.

Since C++11 was released, the C++ Standards Committee (WG21) has been reliably following a train-based model for releasing new Standards — every three years, all features that are ready to ship get on the train and ship as part of a new Standard. Each compiler supported by BDE then has its own release cycle where features are implemented incrementally with varying levels of quality. On top of that, each compiler allows users to select the C++ Standard to compile with.

The many different possibilities for which C++ features are actually reasonable to use lead to a maze of possibilities to navigate. In C++20, the C++ Standard officially introduced official feature test macros — such as __cpp_concepts — to identify whether particular compiler features are available and to what extent they have been implemented. These macros, however, were for a long time before that not standard, and they don’t take into account when a compiler says it does support a feature but it is not yet actually stable enough to use reliably in production.

To navigate these complexities, BDE provides a single component with macros that reliably expose the language features that are available — bsls_compilerfeatures. This component is typically most valuable when used to construct higher-level facilities that can be used portably, e.g. the macros in bsls_keyword, bslma_movableref, or any of the components in the bsla package. When considering the use of bsls_compilerfeatures (either directly or indirectly) it is worth understanding the range of languages variants the software is actually intended to support, and the trade off in terms of maintainability that comes with the use of conditional compilation.

Consider, for example, a simple class written long ago that does nothing special in its default constructor:

class PickleHandle
{
// ...
public:
    PickleHandle();

// ...

private:
    bsl::shared_ptr<Pickle> d_pickle;
};

PickleHandle::PickleHandle()
: d_pickle()
{}

Now we realize that we don’t intend to do more in this constructor, but we need to keep declaring it because of the other constructors on PickleHandle. In C++11, we can reduce our maintenance burden by defaulting this constructor, so we can evolve our code to take advantage of that when possible:

class PickleHandle
{
// ...
public:
    PickleHandle()
#ifdef BSLS_COMPILERFEATURES_SUPPORT_DEFAULTED_FUNCTIONS
        = default;
#else
        ;
#endif

// ...

private:
    bsl::shared_ptr<Pickle> d_pickle;
};

#ifndef BSLS_COMPILERFEATURES_SUPPORT_DEFAULTED_FUNCTIONS
PickleHandle::PickleHandle()
: d_pickle()
{}
#endif

Of course, looking at this one might question the wisdom of our change, given that we’ve added significantly more lines of code to achieve no concrete benefit for us or our users. That might be true, but this is an interim step. At some point, our pickle-focused library will stop supporting platforms that do not support defaulted functions, and we can then rather mechanically remove a pile of clearly marked code by stating that requirement once at the top of our header, changing our source code to cleanly use modern C++ syntax and features:

#ifndef BSLS_COMPILERFEATURES_SUPPORT_DEFAULTED_FUNCTIONS
#error Pickle Lib requires defaulted functions
#endif

class PickleHandle
{
// ...
public:
    PickleHandle() = default;

// ...

private:
    bsl::shared_ptr<Pickle> d_pickle;
};

This same approach can be used for a wide variety of other modern C++ features, such as concepts, the spaceship operator, various attributes, and many aspects of compile-time programming with constexpr.

If you’d like to know more, check out: