【问题标题】:Run-time indexing of tuple元组的运行时索引
【发布时间】:2020-01-31 12:13:54
【问题描述】:

假设我有一个变量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&lt;ClassA, ClassB, ClassC&gt;

请查看最小可重现示例:Try it online!


类似的问题:C++11 way to index tuple at runtime without using switch

【问题讨论】:

标签: c++ c++11 templates reflection tuples


【解决方案1】:

您的运行时可能会返回 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你的变种来做你想做的事。

Demo

或者对于您的“工厂”示例:

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))

【讨论】:

  • 感谢您的更新。我有 gcc 7.4.0 未能编译您的代码。根据gccgcc7 应该实现了所有c++17 功能。 c++17 中不包含您在此处使用的哪些功能?
  • 另一个问题:是否有可能创建一个magic_get 函数来启用auto obj = magic_get(constructors, 0)(/*arguments for any constructor of ClassA*/);
  • 这主要是我在上次更新中所做的:从动态索引i构造一个std::variant
  • 我希望不必为BC 指定参数,因为A 类是唯一重要的事情。抱歉,我仍然看不到如何将您的建议转换为auto obj = magic_get(constructors, 0)(/*arguments for any constructor of ClassA*/); 的形式。
  • 如前所述,如果(运行时)索引解析为 B 而不是 A,会发生什么?
【解决方案2】:

这可能会做得更好,但这里是根据您在 cmets 中的要求的尝试。

需要 C++17,适用于 Clang,但在 GCC 上提供 Internal Compiler Error

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

所以用

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

而不是

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

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

它返回一个 lambda,当使用参数列表调用该 lambda 时,将返回 std::variant,其中包含可能由 std::get&lt;i&gt;(tup)(/*arguments*/) 形式的调用产生的所有类型。其中哪一个被实际调用并存储在返回的变体中是在运行时通过index 参数决定的。如果index 引用的元组元素不能像std::get&lt;index&gt;(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&lt;Tup&gt; 代替std::remove_const_t&lt;std::remove_reference_t&lt;Tup&gt;&gt;

  • unwrap的定义改为:

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

    并将其用作unwrap&lt;...&gt; 而不是unwrap&lt;+...&gt;,这也允许从wrap_t 中删除operator+


wrap/unwrap的目的:

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

我把wrap写成一个全局内联变量,这样我就可以写wrap&lt;int&gt;而不是wrap&lt;int&gt;{},因为我认为额外的大括号很烦人。

unwrap&lt;...&gt; 不是真的需要的。 typename decltype(...)::type 也是如此,它只是返回 wrap 实例所代表的类型。

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

所以在 C++17 中,我将对象“衰减”为一个指针,该指针可以是一个非类型模板参数,并带有一个重载的 operator+,模仿了常见的 lambda-to-function-pointer 技巧的语法使用一元 + 运算符(但我可以使用任何其他一元运算符)。

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

【讨论】:

  • @ZhengQu 一个 ICE 始终是一个编译器错误。我不会报告此代码,因为它太长且包含不相关的细节。如果我有时间,我会将其简化为一个最小的示例并报告(如果还没有针对同一问题的错误报告)。
  • @ZhengQu 你的包装有点违背了所有这些的目的。你在结果上调用std::get&lt;0&gt;,如果index != 0,它总是会抛出异常。正如我在有关问题的 cmets 中提到的,您可以通过简单的if/else 来实现这一点。另请注意,如果您愿意在std::variant 中包含未使用的类型,则该函数可以编写得更简单。为了避免这种情况,我走了一些弯路。
  • @ZhengQu 啊,是的,对不起。我的意思是,如果索引与元组中可以使用提供的参数调用的第一个元素不匹配,它总是会抛出异常。它不允许用除该索引之外的任何东西来调用它。其他一切都会抛出。如果这就是你想要的,你可以更简单地实现它(实际上索引将是多余的)。我的magic_get 将返回所有类型的变体,该变体可以通过使用提供的参数调用元组元素而产生,并且可以在运行时使用索引选择其中一个被调用的类型。
  • 我建议你看看编译时库 boost.hana,它有一些类似于我所说的 wrap:hana::type_c。见boost.org/doc/libs/1_72_0/libs/hana/doc/html/…
猜你喜欢
  • 1970-01-01
  • 2023-04-05
  • 1970-01-01
  • 2021-08-12
  • 1970-01-01
  • 2015-05-13
  • 2020-10-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多