This can probably be done more nicely, but here is an attempt according to your requirements in the comments.
Requires C++17, works on Clang, but gives an Internal Compiler Error on GCC.
It does require though, that you make the constructing function SFINAE-friendly, otherwise there is no way of checking whether it can be called:
So use
return [](auto... args) -> decltype(U(args)...) { return U(args...); };
instead of
return [](auto... args) { return U(args...); };
The behavior of this function given arguments tup
and index
is as follows:
It returns a lambda that when called with a list of arguments will return a std::variant
of all the types that could result from calls of the form std::get<i>(tup)(/*arguments*/)
. Which one of these is actually called and stored in the returned variant is decided at runtime through the index
argument. If index
refers to a tuple element that cannot be called as if by std::get<index>(tup)(/*arguments*/)
, then an exception is thrown at runtime.
The intermediate lambda can be stored and called later. Note however that it saves a reference to the tup
argument, so you need to make sure that the argument out-lives the lambda if you don't call and discard it immediately.
#include <tuple>
#include <type_traits>
#include <variant>
#include <utility>
#include <stdexcept>
template<auto V> struct constant_t {
static constexpr auto value = V;
using value_type = decltype(value);
constexpr operator value_type() const {
return V;
}
};
template<auto V>
inline constexpr auto constant = constant_t<V>{};
template<auto V1, auto V2>
constexpr auto operator+(constant_t<V1>, constant_t<V2>) {
return constant<V1+V2>;
}
template<typename T>
struct wrap_t {
using type = T;
constexpr auto operator+() const {
return static_cast<wrap_t*>(nullptr);
}
};
template<typename T>
inline constexpr auto wrap = wrap_t<T>{};
template<auto A>
using unwrap = typename std::remove_pointer_t<decltype(A)>::type;
template <typename Tup>
auto magic_get(Tup&& tup, std::size_t index) {
return [&tup, index](auto&&... args) {
// Get the input tuple size
constexpr auto size = std::tuple_size_v<std::remove_const_t<std::remove_reference_t<Tup>>>;
// Lambda: check if element i of tuple is invocable with given args
constexpr auto is_valid = [](auto i) {
return std::is_invocable_v<decltype(std::get<i>(tup)), decltype(args)...>;
};
// Lambda: get the wrapped return type of the invocable element i of tuple with given args
constexpr auto result_type = [](auto i) {
return wrap<std::invoke_result_t<decltype(std::get<i>(tup)), decltype(args)...>>;
};
// Recursive lambda call: get a tuple of wrapped return type using `result_type` lambda
constexpr auto valid_tuple = [=]() {
constexpr auto lambda = [=](auto&& self, auto i) {
if constexpr (i == size)
return std::make_tuple();
else if constexpr (is_valid(i))
return std::tuple_cat(std::make_tuple(result_type(i)), self(self, i + constant<1>));
else
return self(self, i + constant<1>);
};
return lambda(lambda, constant<std::size_t{0}>);
}();
// Lambda: get the underlying return types as wrapped variant
constexpr auto var_type =
std::apply([](auto... args) { return wrap<std::variant<unwrap<+args>...>>; }, valid_tuple);
/**
* Recursive lambda: get a variant of all underlying return type of matched functions, which
* contains the return value of calling function with given index and args.
*
* @param self The lambda itself
* @param tup A tuple of functions
* @param index The index to choose from matched (via args) functions
* @param i The running index to reach `index`
* @param j The in_place_index for constructing in variant
* @param args The variadic args for callling the function
* @return A variant of all underlying return types of matched functions
*/
constexpr auto lambda = [=](auto&& self, auto&& tup, std::size_t index, auto i, auto j,
auto&&... args) -> unwrap<+var_type> {
if constexpr (i == size)
throw std::invalid_argument("index too large");
else if (i == index) {
if constexpr (is_valid(i)) {
return unwrap<+var_type>{std::in_place_index<j>,
std::get<i>(tup)(decltype(args)(args)...)};
} else {
throw std::invalid_argument("invalid index");
}
} else {
return self(self, decltype(tup)(tup), index, i + constant<1>, j + constant<is_valid(i)>,
decltype(args)(args)...);
}
};
return lambda(lambda, std::forward<Tup>(tup), index, constant<std::size_t{0}>,
constant<std::size_t{0}>, decltype(args)(args)...);
};
}
In C++20, you can simplify this by
using std::remove_cvref_t<Tup>
instead of std::remove_const_t<std::remove_reference_t<Tup>>
changing the definition of unwrap
to:
template<auto A>
using unwrap = typename decltype(A)::type;
and using it as unwrap<...>
instead of unwrap<+...>
, which also allows removing the operator+
from wrap_t
.
The purpose of wrap
/unwrap
:
wrap_t
is meant to turn a type into a value that I can pass into functions and return from them without creating an object of the original type (which could cause all kinds of issues). It is really just an empty struct templated on the type and a type alias type
which gives back the type.
I wrote wrap
as a global inline variable, so that I can write wrap<int>
instead of wrap<int>{}
, since I consider the additional braces annoying.
unwrap<...>
isn't really needed. typename decltype(...)::type
does the same, it just gives back the type that an instance of wrap represents.
But again I wanted some easier way of writing it, but without C++20 this is not really possible in a nice way. In C++20 I can just pass the wrap
object directly as template argument, but that doesn't work in C++17.
So in C++17 I "decay" the object to a pointer, which can be a non-type template argument, with an overloaded operator+
, mimicking the syntax of the common lambda-to-function-pointer trick using the unary +
operator (but I could have used any other unary operator).
The actual pointer value doesn't matter, I only need the type, but the template argument must be a constant expression, so I let it be a null pointer. The latter requirement is why I am not using the built-in address-of operator &
instead of an overloaded +
.