Functional APIs are a joy to work with. Not only do they help eliminate certain bug categories, but they tend to be very flexible and reusable. Today I present a technique that has emerged while I was simplifying some lambda based APIs. C++17 makes template meta-programming much more palatable, I dare not imagine what this would look like in C++11.
The usual disclaimer : This code was written and compiled on Visual Studio 15.7 only. I make no guarantees it will work with other compilers, though it probably will if you are on bleeding releases. I use the word lambda instead of closure to make the post more approachable. The examples are simple demonstrations, not meant to be production ready. If you copy-paste this in your code base, shame on you. SHAME SHAME SHAME.
Update : User redditsoaddicting (it is) points out the new std::is_invocable
trait in C++17 removes the need for is_detected
. The post has been updated, and greatly simplified, accordingly.
A Flexible Visitor
Lets assume you have a container class wrapping some funky data. Maybe a variant, union or some snow-flake containers. For example, I developed this technique while wrapping some gsl::span<const T>
s pointing to read-only memory-mapped files. The spans were contained in a variant-like wrapper. In this scenario, you would like to offer a visitor-like API. The user provides a closure, this closure accepts some form of data as input, and everyone is happy.
But what if you want to offer a visitor customization point? Lets say you are storing a vector of said data, and an optional meta-data string vector along-side it. How does the user select which visitor API to call?
You could offer various visitor functions, like visit(...)
and visit_with_metadata(...)
. But this gets out of control real fast. Another solution would be a bit-mask option. This is quite elegant and is used throughout the stl or boost. But can we do better?
Yes, yes we can.
Lets take the previous example, here’s what it may look like if we offer visitor alternatives.
struct secret_garden final {
template <class Invocable>
void visit(const Invocable& invocable) const {
for (const auto& d : _data) {
std::invoke(invocable, d);
}
}
template <class Invocable>
void visit_with_metadata(const Invocable& invocable) const {
// assert sizes here
for (size_t i = 0; i < _data.size(); ++i) {
std::invoke(invocable, _data[i], _meta_data[i]);
}
}
private:
std::vector<int> _data{ 0, 1, 2, 3, 4, 5 };
std::vector<std::string> _meta_data{ "zero", "one", "two", "three", "four",
"five" };
};
/* ... */
// Use the APIs.
secret_garden garden{};
garden.visit([](const auto& data) { printf("%d\n", data); });
garden.visit_with_metadata([](const auto& data, const auto& meta_data) {
printf("%d - %s\n", data, meta_data.c_str());
});
If you are wondering why generic lambdas are used, there is no specific reason other than laziness.
So how can we improve this API to be more declarative? If you think about it, the user inherently declares what version of the visitor API he would like to use. The lambda parameters contain all the information we need to make a choice here. If you provide a lambda which accepts 1 parameter, you would like to use visit
. If you provide a lambda which accepts 2 parameters, you would like to use visit_with_metadata
. In the hopes of helping programmers with tendinitis around the world, lets make it happen.
In the past, one would have used SFINAE techniques to detect what arguments the provided lambda accepts. This is no longer needed, as C++17 provides a builtin solution with std::is_invocable
. More details on cppreference. Combined with if constexpr
, all we need is a simple modification to visit
so it selects the appropriate call. The following is some seriously readable template meta-programming. What a time to be alive!
#include <cstdio>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
struct secret_garden final {
template <class Invocable>
void visit(const Invocable& invocable) const {
if constexpr (std::is_invocable_v<Invocable, const int&,
const std::string&>) {
for (size_t i = 0; i < _data.size(); ++i) {
std::invoke(invocable, _data[i], _meta_data[i]);
}
} else {
for (const auto& d : _data) {
std::invoke(invocable, d);
}
}
}
private:
std::vector<int> _data{ 0, 1, 2, 3, 4, 5 };
std::vector<std::string> _meta_data{ "zero", "one", "two", "three", "four",
"five" };
};
/* ... */
secret_garden garden{};
garden.visit([](const auto& data) { printf("%d\n", data); });
garden.visit([](const auto& data, const auto& meta_data) {
printf("%d - %s\n", data, meta_data.c_str());
});
This will work for many variations, though if you have 2 “overloads” that accepts the same type, you will need another way to differentiate those APIs.
Having fun yet? We are just getting started!
Digging Our Rabbit Hole - Flexible Variadic Selection
If the previous technique left you a little perplexed, I recommend taking a little break right about now. Maybe prepare a nice coffee and relax with a good book before reading the following. All bets are off starting now.
Lets assume you have a variadic class that does useful things and contains variadic data (a tuple for example). You offer a functional API to execute lambdas on its members. The user can select the data he desires using a variadic API. For example.
template <class... Args>
struct some_tuple_wrapper final {
template <class... Ts, class Invocable>
void execute(Invocable&& invocable) const {
// static_assert things here
std::apply(std::forward<Invocable>(invocable),
std::make_tuple(std::get<Ts>(_data)...));
}
private:
std::tuple<Args...> _data{ Args{ 65 }... };
};
/*...*/
some_tuple_wrapper<int, double, float, char, short> tuple_burrito{};
tuple_burrito.execute<char, short>(
[](char c, short s) { printf("%c, %d\n", c, s); });
It is certainly a usable API. But there is a serious problem with it : we have to repeat “char” and “short” twice. Furthermore, we need to use the ‘<’ and ‘>’ keys, which require non-optimal pinky finger usage. Unacceptable!
We would rather use some voodoo C++ to detect what the user provided lambda parameters are, and call the invocable with the appropriate data. I am almost certain there is a way to make this work with duplicate types, but to simplify the code and protect my sanity, I will block duplicate types in both the container and execute call using the following nifty trait.
// Vittorio Romeo
// https://stackoverflow.com/questions/47511415/checking-if-variadic-template-parameters-are-unique-using-fold-expressions
template <typename...>
constexpr bool is_unique = std::true_type{};
template <typename T, typename... Rest>
constexpr bool is_unique<T, Rest...> = std::bool_constant<
(!std::is_same_v<T, Rest> && ...) && is_unique<Rest...>>{};
template <class... Ts>
constexpr bool is_tuple_unique(std::tuple<Ts...>) {
return is_unique<Ts...>;
}
Lets start by calling the user lambda first.
template <class Invocable>
void execute_freedom(Invocable&& invocable) const {
std::apply(std::forward<Invocable>(invocable), /* dig rabbit hole here */);
}
We will use std::apply
on the user lambda. It seems like an elegant and readable way to go about things, and lets make things readable where we can. Before delving into lambda parameter type extraction, lets create our variadic getter plumbing.
We start with a simple getter that accepts a type, and returns a const reference (in this case).
template <class T>
const auto& get() const {
return std::get<T>(_data);
}
Next, we will need a variadic version which will pack the returned references in a tuple for std::apply
. To help our compiler, this overload is only enabled when the size of our variadic types is not 1.
template <class... Ts,
typename std::enable_if_t<sizeof...(Ts) != 1>* dummy = nullptr>
auto get() const {
return std::tuple<const Ts&...>{ get<Ts>()... };
}
This uses a simple parameter pack expansion to call multiple get()
s and pack the results in a tuple. But how can we figure out what types to use in our getter call?
To answer that, we will need some function traits. I used the function traits from Functional C++’s great post about them as a starting point. Here they are with some details about my modifications.
function_traits.hpp
|
|
These traits can be used to detect what arguments a lambda’s operator paren uses. The detected argument types will be stored in a tuple, so we will need to unpack it later on. Since the user will often use references or const references, we also need to store non-decorated args. Without them, we wouldn’t be able to default initialize a dummy tuple of arguments required to reconvert the args tuple as a parameter pack.
Let me rephrase that. Once the arguments are stored in a tuple type, we need some trickery to unpack them into a parameter pack again. This involves creating a dummy tuple and that is impossible when using references. We will use the dummy tuple to call yet another overload of our getter.
template <class... Ts>
auto get(std::tuple<Ts...>) const {
auto ret = get<Ts...>();
if constexpr (is_tuple_v<std::decay_t<decltype(ret)>>) {
return ret;
} else {
return std::make_tuple(ret);
}
}
This get function accepts a dummy tuple as a parameter. It expands the tuple inner types to a parameter pack again. We can then call the other get()
overloads. We have to check the case where only 1 parameter is used, as that will skip our variadic getter and wont return a tuple of const references but a raw const reference instead. Here is what the is_tuple
trait looks like.
template <class T>
struct is_tuple : std::false_type {};
template <class... Ts>
struct is_tuple<std::tuple<Ts...>> : std::true_type {};
template <class... Ts>
constexpr bool is_tuple_v = is_tuple<Ts...>::value;
Still hanging in there? You deserve some compensation… how about the final version of our execute call!
template <class Invocable>
void execute_freedom(Invocable&& invocable) const {
static_assert(is_tuple_unique(typename function_traits<Invocable>::args_decay{}),
"only unique parameters are accepted");
auto dummy = typename function_traits<Invocable>::args_decay{};
static_assert(std::tuple_size_v<decltype(dummy)> != 0, "tsk tsk tsk");
std::apply(std::forward<Invocable>(invocable), get(dummy));
}
/* ... */
ex2::some_tuple_wrapper<int, double, float, char, short> tuple_burrito{};
tuple_burrito.execute_freedom(
[](char c, short s) { printf("%c, %d\n", c, s); });
tuple_burrito.execute_freedom(
[](const char& c, const double& d, const int& i) {
printf("%c, %f, %d\n", c, d, i);
});
First we check if the user provided a lambda with duplicate parameters. We bail out if that is the case. Next we construct our dummy
tuple using the non-decorated function traits. We do a simple check for size 0. Finally, we call get
with our dummy tuple. This will unpack the tuple, call the other get
overloads and return a tuple of appropriate data. std::apply
will then unpack this tuple and call the user lambda with it. Neat!
That is all for today, I hope you found this technique interesting. Shout out to Kelvin for supporting me during my rabbit hole adventure.
A working example is hosted on my github. And here is the final container class. Enjoy!
some_tuple_wrapper.cpp
|
|