Technique: Immediately-Invoked Function Expression for Metaprogramming

Common C++ guidelines are to initialize variables on use and to make variables const whenever possible. But sometimes a variable is unchanged once initialized and the initialization is complex, like involving a loop. Then an IIFE – immediately-invoked function expression – can be used: the variable is initialized by a lambda that computes the value, which is then immediately invoked to produce the value. Then the variable is initialized on use and can also be made const.

I’ve been recently working on a meta-programming library where I found IIFEs useful in a slightly different context – computing type information.

TL;DR: decltype([] { ... } ())!

The Challenge: value_type_of

For the sake of this blog post, let’s suppose we want to implement a type trait that given a container computes the value type of the container.

template <typename Container>
using value_type_of = ; // TBD

value_type_of<std::vector<int>> i; // int
value_type_of<float[3]> x; // float

I really dislike the standard library convention of having either typename trait::type or trait_t – type aliases make it unnecessary. Let’s not copy that mistake here.

This is the behavior of value_type_of that I want:

For simplicity, let’s not worry about oddities like const Container or arrays without size T[]. Even so, this specification is somewhat vague about an edge case; can you see it?

We can make an immediate observation: as the type trait should be ill-formed if we pass it something that is neither an array nor has ::value_type, we don’t need to do the compile-time – and (pre C++20) syntactical – expensive check for ::value_type. We can just handle arrays in one way and use ::value_type for everything else. If the type doesn’t have ::value_type, the trait is ill-formed automatically.

First Attempt

This is a very straightforward implementation of value_type_of:

template <typename Container>
struct value_type_of_impl // default, non-array
{
    using type = typename Container::value_type; 
};

template <typename T, std::size_t N>
struct value_type_of_impl<T[N]> // arrays
{
    using type = T;
};

template <typename Container>
using value_type_of = typename value_type_of_impl<Container>::type;

As we don’t have if for types, we need specialization to distinguish between arrays and non-arrays. And as we can’t specialize type aliases, we need to introduce a helper class template.

It works, but is verbose. Let’s try something better.

Second Attempt

While we don’t have if for types, we do have std::conditional(_t…). It takes a bool and two types and selects either the first one or the second, depending on the bool. Look at that, this is what we want!

template <typename Container>
using value_type_of =
  std::conditional_t<std::is_array_v<Container>, // if
                  std::remove_extent_t<Container>, // then
                  typename Container::value_type>; // else

We’re checking whether the container is an array using std::is_array(_v…). If so, we’re using std::remove_extent(_t…) to get the element type, otherwise, we’re taking Container::value_type.

The choice of using std::remove_extent here and the specialization for T[N] above answer the edge case I’ve hinted at earlier. What is value_type_of<int[2][2]>? int[2] or int? My implementation says int[2]; if you want int you need more specializations in the first attempt and std::remove_all_extents(_t…) in the second attempt.

This is more concise than the first attempt, but ugly.

More importantly, it doesn’t work!

Consider what happens when we write value_type_of<float[3]>:

std::conditional_t<std::is_array_v<float[3]>, // true
                std::remove_extent_t<float[3]>, // float
                typename float[3]::value_type>; // error! 

Even though the second argument to std::conditional_t doesn’t matter, it is still there! And typename float[3]::value_type is ill-formed, because a float array doesn’t have ::value_type.

So we need to do better.

Third Attempt

What we need is some sort of if constexpr based version of std::conditional_t. While something like that is possible, let’s finally use IIFE which allows the actual if constexpr:

template <typename Container>
using value_type_of = decltype([]{
      if constexpr (std::is_array_v<Container>)
          return std::remove_extent_t<Container>{};
      else
          return typename Container::value_type{};
  }());

Just like in the traditional use case of IIFE, we initialize the alias with a lambda that we’re immediately invoking to get the value. But here we need a type, not a value, so we need to surround the whole thing with decltype(). The advantage of this syntactic noise is that we can have the full power of the language - in this case if constexpr to implement the type trait.

Alas, we’re not quite done. Note that we need to return a value of the appropriate type, as that’s what the language rules require. Here, we’re just returning a default constructed object, which doesn’t work if the type does not have a default constructor.

Final Solution

As the lambda isn’t actually executed – it is only there to compute a return type – it doesn’t really matter how we’ve obtained the value we return. This is what std::declval was designed for: to obtain a value in a context where the value isn’t actually used, only its type. Unfortunately, the value is used “too much” for std::declval; we need our own:

template <typename T>
T type(); // no definition

template <typename Container>
using value_type_of = decltype([]{
      if constexpr (std::is_array_v<Container>)
          return type<std::remove_extent_t<Container>>();
      else
          return type<typename Container::value_type>();
  }());

godbolt

It doesn’t matter that my_declval has no definition - only its return type is important.

I actually have a DECLVAL macro like the move and forward one discussed in the last post. But if I wrote two macro posts one after the other, I’d lose my C++ blog licence.

Conclusion

Using lambdas to compute types is definitely less verbose and can allow for clearer code than the classical TMP way of using specializations. The downside is some syntactic noise around the definition – although you’re definitely skipping it after a while if you’re getting used to the pattern. It’s also a bit verbose to return the type information, because C++ functions can’t return typename (yet).

I should point out that using lambdas in decltype() is a C++20 feature; if you need to support older versions you need a regular named function with auto return type:

template <typename Container>
auto value_type_of_()
{
    if constexpr (std::is_array_v<Container>)
        return type<std::remove_extent_t<Container>>();
    else
        return type<typename Container::value_type>();
}

template <typename Container>
using value_type_of = decltype(value_type_of_<Container>());

godbolt

But still, I prefer that to the implementation using specializations.