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
ortrait_t
– type aliases make it unnecessary. Let’s not copy that mistake here.
This is the behavior of value_type_of
that I want:
- If the type has a
::value_type
member (like standard library containers), return that. - If the type is an array, return the element type of the array.
- Otherwise, the type trait is ill-formed.
For simplicity, let’s not worry about oddities like
const Container
or arrays without sizeT[]
. 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 forT[N]
above answer the edge case I’ve hinted at earlier. What isvalue_type_of<int[2][2]>
?int[2]
orint
? My implementation saysint[2]
; if you wantint
you need more specializations in the first attempt andstd::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>();
}());
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>());
But still, I prefer that to the implementation using specializations.