元组的运行时索引

Run-time indexing of tuple

本文关键字:索引 运行时 元组      更新时间:2023-10-16

假设我有一个变量constructors,它是用可变参数泛型lambda表示的构造函数元组。

// types for constructors 
using type_tuple = std::tuple<ClassA, ClassB, ClassC>;
// Get a tuple of constructors(variadic generic lambda) of types in type_tuple
auto constructors = execute_all_t<type_tuple>(get_construct());
// For definitions of execute_all_t and get_construct, see link at the bottom.

我可以使用以下命令实例化一个对象:

// Create an object using the constructors, where 0 is index of ClassA in the tuple.
ClassA a = std::get<0>(constructors)(/*arguments for any constructor of ClassA*/);

是否可以在运行时使用如下所示的magic_get为类型编制索引?

auto obj = magic_get(constructors, 0)(/*arguments for any constructor of ClassA*/);
// Maybe obj can be a std::variant<ClassA, ClassB, ClassC>, which contains object of ClassA?

编辑:理想情况下,obj应该是ClassA的实例。如果不可能,我可以接受objstd::variant<ClassA, ClassB, ClassC>.

请查看最小可重现示例:在线试用!


类似的问题:C++11 在运行时不使用 switch 索引元组的方法 .

你可能会让你的运行时得到返回std::variant,如下所示:

template <typename ... Ts, std::size_t ... Is>
std::variant<Ts...> get_impl(std::size_t index,
std::index_sequence<Is...>,
const std::tuple<Ts...>& t)
{
using getter_type = std::variant<Ts...> (*)(const std::tuple<Ts...>&);
getter_type funcs[] = {+[](const std::tuple<Ts...>& tuple)
-> std::variant<Ts...>
{ return std::get<Is>(tuple); } ...};
return funcs[index](t);
}
template <typename ... Ts>
std::variant<Ts...> get(std::size_t index, const std::tuple<Ts...>& t)
{
return get_impl(index, std::index_sequence_for<Ts...>(), t);
}

然后,您可能会std::visit变体来执行您想要的操作。

演示

或者对于您的"工厂"示例:

int argA1 = /*..*/;
std::string argA2 = /*..*/;
int argB1 = /*..*/;
// ...
auto obj = std::visit(overloaded{
[&](const A&) -> std::variant<A, B, C> { return A(argA1, argA2); },
[&](const B&) -> std::variant<A, B, C> { return B(argB1); },
[&](const C&) -> std::variant<A, B, C> { return C(); },
}, get(i, t))

这可能做得更好,但这里是根据您在评论中的要求进行的尝试。

需要 C++17,适用于 Clang,但在 GCC 上给出内部编译器错误。

不过,它确实需要使构造函数 SFINAE 友好,否则无法检查是否可以调用它:

所以使用

return [](auto... args) -> decltype(U(args)...) { return U(args...); };

而不是

return [](auto... args) { return U(args...); };

给定参数tupindex的此函数的行为如下:

它返回一个 lambda,当使用参数列表调用该 lambda 时,将返回可能由std::get<i>(tup)(/*arguments*/)形式的调用导致的所有类型的std::variant。其中哪一个实际调用并存储在返回的变体中,在运行时通过index参数决定。如果index引用的元组元素不能被std::get<index>(tup)(/*arguments*/)调用,则在运行时会抛出异常。

中间 lambda 可以存储并在以后调用。但请注意,它会保存对tup参数的引用,因此如果您不立即调用并丢弃它,则需要确保该参数的寿命超过 lambda。

#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)...);
};
}

在 C++20 中,您可以通过以下方式简化此操作

  • 使用std::remove_cvref_t<Tup>而不是std::remove_const_t<std::remove_reference_t<Tup>>

  • unwrap的定义更改为:

    template<auto A>
    using unwrap = typename decltype(A)::type;
    

    并将其用作unwrap<...>而不是unwrap<+...>,这也允许从wrap_t中删除operator+


wrap/unwrap的目的:

wrap_t旨在将类型转换为一个值,我可以将其传递给函数并从中返回,而无需创建原始类型的对象(这可能会导致各种问题(。它实际上只是一个在类型上模板化的空结构和一个返回类型的类型别名type

wrap编写为全局内联变量,以便我可以编写wrap<int>而不是wrap<int>{},因为我认为额外的大括号很烦人。

unwrap<...>并不是真正需要的。typename decltype(...)::type执行相同的操作,它只是返回 wrap 实例表示的类型。

但是我再次想要一些更简单的编写方式,但是如果没有C++20,这实际上是不可能的。在 C++20 中,我可以将wrap对象直接作为模板参数传递,但这在 C++17 中不起作用。

因此,在 C++17 中,我将对象"衰减"为指针,该指针可以是非类型模板参数,带有重载operator+,使用一元运算符模仿常见的 lambda 到函数指针技巧的语法+(但我可以使用任何其他一元运算符(。

实际的指针值无关紧要,我只需要类型,但模板参数必须是常量表达式,所以我让它成为空指针。后一个要求是为什么我不使用内置的运算符 address-of 运算符&而不是重载的+