tagsc++metaprogramming

August 2023

Reacting to compile-time type properties in C++17

Warning: this post will make you very happy or very sad depending on which C++ standard you can use at work.

Like many other code kobolds, I follow the evolution of the C++ language with a mix of hopeful curiosity and hopeless desperation. It turns out that, if you mix design by committee with a “train model” of releases, the result is a relentless pile-up of middle-ground compromises that makes everyone unhappy. Which they do say is the hallmark of good compromises, but it seems like no way to advance a language.

So the Vasa is sinking, and even ISO C++ committee members like Timur Doumler have a hard time staying afloat:

C++ gets bigger and bigger faster than I can learn it, which means the proportion of C++ I know actually shrinks over time!

(from ADSP episode #136, around minute 44).

However!

With recent standards, template metaprogramming actually got nicer and simpler. Unarguably.

I love when this happens, because unlike mammals I have very limited brain space.

Consider these scenarios:

In all these cases we would like to produce different code depending on whether a certain type exposes or not a certain member, and possibly also depending on the compile-time value of the data member.

To fix ideas, let’s say we are writing generic code that needs to behave differently depending on whether a parameter type exposes a compile-time static_size data member or not:

given a generic_function<T>(vec), if T::static_size exists we want to instantiate `int buffer[T::static_size]`, otherwise a `new T[vec.size()]`

“When you are done with cave paintings, I’d love to see some code”

Yeah, yeah. Thanks to a synergic combination of variable templates (C++14), constexpr-if (C++17) and std::void_t (also C++17), solving the problem above requires relatively few lines of relatively readable code (open in Compiler Explorer):

// base case:
// the second template parameter is just a placeholder:
// we will need it in the specialization below
template <class T, class = void>
constexpr bool has_static_size = false;

// this is a specialization so it takes precedence over the base case...
// ...but only if the template parameter is well-formed (SFINAE)
// ...i.e. only if `void_t<decltype(T::static_size)>` is
template <class T>
constexpr bool has_static_size<T, std::void_t<decltype(T::static_size)>>
                = true;

void rest_of_function(int *buf);

template <class T>
void generic_function(const T &vec) {
  if constexpr (has_static_size<T>) {
    int buf[T::static_size];
    rest_of_function(buf);
  } else {
    // using vector as an exception-safe memory buffer
    std::vector<int> buf(vec.size());
    rest_of_function(buf.data());
  }
}

A similar technique can be used to detect member functions rather than data members.

It might be C++ Stockholm syndrome talking, but I find this solution quite nice, all things considered.

My original version wrapped has_static_size in helper structs. My friend Bernhard suggested using variable templates instead, which is much cleaner! He also pointed out to me that C++20 requires clauses simplify this even further. No need for helpers at all, you can just write:

if constexpr (requires { T::static_size; })

Thank you Bernhard!

Back in my day, or: the pain we leave behind

Before constexpr-if, std::void_t and variable templates, ingenious code kobolds would abuse a collection of seemingly unrelated features in order to trick the compiler to generate the code they wanted. In C++11, this is the nicest way I know of to achieve the same as the code above (open in Compiler Explorer):

void rest_of_function(int *buf);

// base case
// the catch-all second argument will be needed to disambiguate between overloads 
template <class T, class LowPriorityOverload>
void generic_function_impl(const T& vec, LowPriorityOverload) {
    std::vector<int> buf(vec.size());
    rest_of_function(buf.data());
}

// this overload only participates if its signature is valid,
// including the expression in the trailing return type, i.e. T::static_size
// when it participates, it will have priority if the second argument is an int
template <class T>
auto generic_function_impl(const T& vec, int)
    -> decltype(T::static_size, void())
{
    int buf[T::static_size];
    rest_of_function(buf);
}

template <class T>
void generic_function(const T &vec) {
    generic_function_impl(vec, 0);
}

Now, it might not seem like a lot more code than the C++17 solution, but this second version is more insidious, like a Mimic pretending to be a half-broken door (don’t worry if you don’t get the reference, just avoid dimly-lit dungeons and I’m sure you’ll be fine):

Some readers might be more comfortable implementing a C++11 version that performs SFINAE via std::enable_if rather than via expressions in trailing return types, like this. Similar arguments apply, but more vehemently.

I only put these examples here so that newer generations don’t forget the struggle of their ancestors. And to those of us that work on codebases that are restricted to C++11 or C++14 still today: godspeed to you, brave soul.

Did you find this useful? Does this get better in C++23? Let me know at codekobold@pm.me!