【问题标题】:C++11 way to index tuple at runtime without using switchC ++ 11在运行时索引元组而不使用开关的方法
【发布时间】:2015-05-13 20:54:50
【问题描述】:

我有一段类似如下的 c++11 代码:

switch(var) {
   case 1: dosomething(std::get<1>(tuple));
   case 2: dosomething(std::get<2>(tuple));
   ...
}

有什么办法可以去掉这个大开关吗?请注意,get&lt;var&gt; 不起作用,因为 var 不是常量,但我知道 var 的范围很小,即 (0-20)。

注意这里的重点是避免使用导致数组查找的数组...

编辑:

关于性能的问题,有讨论 Performance of array of functions over if and switch statements

出于我自己的目的,我不争论哪个更好。

【问题讨论】:

  • 你似乎在问如何在运行时索引一个元组。
  • 您是否进行过任何衡量,表明您的代码中的这种更改是值得的?我认为数组查找几乎肯定比 CPU 无法准确预测将采用哪个分支的 256 个单独分支更快。
  • 你的元组是否包含不同的类型? doSomething 是否对所有类型都进行了重载,或者是否需要隐式转换?
  • 哦,不,数组查找!这只是字面上可以执行的最快操作之一。
  • 使用数组。您添加的链接是指函数指针数组 vs 根据开关调用函数。这里要调用不同值的same函数,没有理由不使用数组。

标签: c++ c++11 tuples


【解决方案1】:

这是一个不使用索引序列的版本:

template <size_t I>
struct visit_impl
{
    template <typename T, typename F>
    static void visit(T& tup, size_t idx, F fun)
    {
        if (idx == I - 1) fun(std::get<I - 1>(tup));
        else visit_impl<I - 1>::visit(tup, idx, fun);
    }
};

template <>
struct visit_impl<0>
{
    template <typename T, typename F>
    static void visit(T& tup, size_t idx, F fun) { assert(false); }
};

template <typename F, typename... Ts>
void visit_at(std::tuple<Ts...> const& tup, size_t idx, F fun)
{
    visit_impl<sizeof...(Ts)>::visit(tup, idx, fun);
}

template <typename F, typename... Ts>
void visit_at(std::tuple<Ts...>& tup, size_t idx, F fun)
{
    visit_impl<sizeof...(Ts)>::visit(tup, idx, fun);
}

DEMO

【讨论】:

  • 很好,我没有想到将元组大小用作索引的范围...... doh。
  • 很好,但我认为该演示仅适用于 c++14,c++11 有问题吗?
  • @w00d 通用 lambda [](auto v) {...} 是 C++14 功能。你可以写一个通用仿函数:struct F { template &lt;class T&gt; operator()(T v) const {...} }; F doSomething;
  • 顺便说一句,这个答案提供的演示也可以使用从标准输入获得的任意整数,例如(因此在编译时不知道)。 ideone.com/ExUbjK
  • 这是如何工作的?编译器如何知道在编译时何时停止生成struct?它如何知道编译时idx == I-1 的时间?
【解决方案2】:

这是一个没有递归的难以理解的泛型实现。我不认为我会在生产中使用它——这是只写代码的一个很好的例子——但有趣的是它可以做到。 (DEMO):

#include <array>
#include <cstddef>
#include <initializer_list>
#include <tuple>
#include <iostream>
#include <type_traits>
#include <utility>

template <std::size_t...Is> struct index_sequence {};

template <std::size_t N, std::size_t...Is>
struct build : public build<N - 1, N - 1, Is...> {};

template <std::size_t...Is>
struct build<0, Is...> {
    using type = index_sequence<Is...>;
};

template <std::size_t N>
using make_index_sequence = typename build<N>::type;

template <typename T>
using remove_reference_t = typename std::remove_reference<T>::type;

namespace detail {
template <class Tuple, class F, std::size_t...Is>
void tuple_switch(const std::size_t i, Tuple&& t, F&& f, index_sequence<Is...>) {
  [](...){}(
    (i == Is && (
       (void)std::forward<F>(f)(std::get<Is>(std::forward<Tuple>(t))), false))...
  );
}
} // namespace detail

template <class Tuple, class F>
void tuple_switch(const std::size_t i, Tuple&& t, F&& f) {
  static constexpr auto N =
    std::tuple_size<remove_reference_t<Tuple>>::value;

  detail::tuple_switch(i, std::forward<Tuple>(t), std::forward<F>(f),
                       make_index_sequence<N>{});
}

constexpr struct {
  template <typename T>
  void operator()(const T& t) const {
      std::cout << t << '\n';
  }
} print{};

int main() {

  {
    auto const t = std::make_tuple(42, 'z', 3.14, 13, 0, "Hello, World!");

    for (std::size_t i = 0; i < std::tuple_size<decltype(t)>::value; ++i) {
      tuple_switch(i, t, print);
    }
  }

  std::cout << '\n';

  {
    auto const t = std::array<int, 4>{{0,1,2,3}};
    for (std::size_t i = 0; i < t.size(); ++i) {
      tuple_switch(i, t, print);
    }
  }
}

【讨论】:

  • 很好地使用...让我们称之为空可变参数 lambda 包扩展成语。我认为阅读并不难;完美转发在可读性方面的成本最高。
  • 非常好!但是您不想为逗号运算符问题强制转换为无效吗?
  • @w00d 对不起,我很懒。我在实现中使用了 C++14 的 integer_sequenceremove_reference_t,在演示代码中使用了通用 lambda。它在 just C++11 中。
【解决方案3】:

这是可能的,但它很丑:

#include <tuple>
#include <iostream>

template<typename T>
void doSomething(T t) { std::cout << t << '\n';}

template<int... N>
struct Switch;

template<int N, int... Ns>
struct Switch<N, Ns...>
{
  template<typename... T>
    void operator()(int n, std::tuple<T...>& t)
    {
      if (n == N)
        doSomething(std::get<N>(t));
      else
        Switch<Ns...>()(n, t);
    }
};

// default
template<>
struct Switch<>
{
  template<typename... T>
    void operator()(int n, std::tuple<T...>& t) { }
};

int main()
{
  std::tuple<int, char, double, int, int, const char*> t;
  Switch<1, 2, 4, 5>()(4, t);
}

只需在 Switch 特化的模板参数列表中列出原始 switch 中的 case 标签中的每个常量。

要编译,doSomething(std::get&lt;N&gt;(t)) 必须是 Switch 特化的参数列表中每个 N 的有效表达式......但 switch 语句也是如此。

对于少数情况,它编译为与 switch 相同的代码,我没有检查它是否适用于大量情况。

如果您不想输入Switch&lt;1, 2, 3, 4, ... 255&gt; 中的每个数字,那么您可以创建一个std::integer_sequence,然后使用它来实例化Switch

template<size_t... N>
Switch<N...>
make_switch(std::index_sequence<N...>)
{
  return {};
}

std::tuple<int, char, double, int, int, const char*> t;
make_switch(std::make_index_sequence<4>{})(3, t);

这会创建一个Switch&lt;0,1,2,3&gt;,因此如果您不想要0 的情况,您需要操作index_sequence,例如这会将列表前面的零去掉:

template<size_t... N>
Switch<N...>
make_switch(std::index_sequence<0, N...>)
{
  return {};
}

不幸的是,GCC 在尝试编译 make_index_sequence&lt;255&gt; 时崩溃,因为它涉及太多递归并使用太多内存,并且 Clang 默认也拒绝它(因为它对-ftemplate-instantiation-depth 的默认值非常低)所以这不是一个非常实用的解决方案!

【讨论】:

  • 是的.. 很丑,有没有消除在 Switch 中指定数字的需要?因为我知道我的价值在一个范围内。
  • 嘿,崩溃是否表明解决方案不实用或 libstdc++ 的make_index_sequence 存在 QoI 问题? ;) IIRC 有一个 log N 实现...
  • @T.C.呵呵,部分是我在 libstdc++ 中的幼稚实现,部分是 G++ 的限制......但无论如何它是一个非常疯狂的模板实例化。我想如果是我的话,我可能只是使用代码生成器来发送 case 语句。我仍然坚持使用编译器内在函数,这将使 log N 实现变得不必要。
【解决方案4】:

在 c++17 中无需获取所有 cray cray。

// Calls your func with tuple element.
template <class Func, class Tuple, size_t N = 0>
void runtime_get(Func func, Tuple& tup, size_t idx) {
    if (N == idx) {
        std::invoke(func, std::get<N>(tup));
        return;
    }

    if constexpr (N + 1 < std::tuple_size_v<Tuple>) {
        return runtime_get<Func, Tuple, N + 1>(func, tup, idx);
    }
}

还有运行时tuple_element 好玩。

// Calls your func with a pointer to the type.
// Uses a pointer so the element is not initialized.
template <class Tuple, class Func, size_t N = 0>
void runtime_tuple_element(Func func, size_t idx) {
    if (N == idx) {
        std::tuple_element_t<N, Tuple>* ptr = nullptr;
        std::invoke(func, ptr);
        return;
    }

    if constexpr (N + 1 < std::tuple_size_v<Tuple>) {
        return runtime_tuple_element<Tuple, Func, N + 1>(func, idx);
    }
}

【讨论】:

  • 光滑。我还不知道这些功能。顺便说一句,如果您将 constexpr 条件放在函数的开头,则可能会因超出范围的索引而引发错误。
【解决方案5】:

我修改了 Oktalist 的答案,使其更加健壮:

  • 制作visit_at方法constexpr
  • 允许访问者传递任意数量的参数(访问的元组元素仍然需要第一个参数)
  • 允许访问者返回值
  • 使visit_at 方法与任何std::get 兼容类型兼容(例如std::array

为了完整起见,我也将其设为 noexcept,尽管那是一团糟(noexcept(auto) 已经在哪里了?)。

namespace detail
{
    template<std::size_t I>
    struct visit_impl
    {
        template<typename Tuple, typename F, typename ...Args>
        inline static constexpr int visit(Tuple const &tuple, std::size_t idx, F fun, Args &&...args) noexcept(noexcept(fun(std::get<I - 1U>(tuple), std::forward<Args>(args)...)) && noexcept(visit_impl<I - 1U>::visit(tuple, idx, fun, std::forward<Args>(args)...)))
        {
            return (idx == (I - 1U) ? (fun(std::get<I - 1U>(tuple), std::forward<Args>(args)...), void(), 0) : visit_impl<I - 1U>::visit(tuple, idx, fun, std::forward<Args>(args)...));
        }

        template<typename R, typename Tuple, typename F, typename ...Args>
        inline static constexpr R visit(Tuple const &tuple, std::size_t idx, F fun, Args &&...args) noexcept(noexcept(fun(std::get<I - 1U>(tuple), std::forward<Args>(args)...)) && noexcept(visit_impl<I - 1U>::template visit<R>(tuple, idx, fun, std::forward<Args>(args)...)))
        {
            return (idx == (I - 1U) ? fun(std::get<I - 1U>(tuple), std::forward<Args>(args)...) : visit_impl<I - 1U>::template visit<R>(tuple, idx, fun, std::forward<Args>(args)...));
        }
    };

    template<>
    struct visit_impl<0U>
    {
        template<typename Tuple, typename F, typename ...Args>
        inline static constexpr int visit(Tuple const&, std::size_t, F, Args&&...) noexcept
        {
            return 0;
        }

        template<typename R, typename Tuple, typename F, typename ...Args>
        inline static constexpr R visit(Tuple const&, std::size_t, F, Args&&...) noexcept(noexcept(R{}))
        {
            static_assert(std::is_default_constructible<R>::value, "Explicit return type of visit_at method must be default-constructible");
            return R{};
        }
    };
}

template<typename Tuple, typename F, typename ...Args>
inline constexpr void visit_at(Tuple const &tuple, std::size_t idx, F fun, Args &&...args) noexcept(noexcept(detail::visit_impl<std::tuple_size<Tuple>::value>::visit(tuple, idx, fun, std::forward<Args>(args)...)))
{
    detail::visit_impl<std::tuple_size<Tuple>::value>::visit(tuple, idx, fun, std::forward<Args>(args)...);
}

template<typename R, typename Tuple, typename F, typename ...Args>
inline constexpr R visit_at(Tuple const &tuple, std::size_t idx, F fun, Args &&...args) noexcept(noexcept(detail::visit_impl<std::tuple_size<Tuple>::value>::template visit<R>(tuple, idx, fun, std::forward<Args>(args)...)))
{
    return detail::visit_impl<std::tuple_size<Tuple>::value>::template visit<R>(tuple, idx, fun, std::forward<Args>(args)...);
}

DEMO(demo不是C++11(由于懒惰),但上面的实现应该是)

【讨论】:

  • noexcept 说明符中应该是&amp;&amp; 而不是||
  • 我意识到我误解了重载逗号运算符的工作原理,因此我更正了第一个实现,使其在所述重载逗号运算符存在时不会中断。
【解决方案6】:

我知道这个线程已经很老了,但我在尝试通过代码库中的静态调度替换虚拟调度时偶然发现了它。

与目前提供的所有解决方案相比,此解决方案使用二进制搜索而不是线性搜索,因此在我的理解中应该是O(log(n)) 而不是O(n)。除此之外,它只是solution presented by Oktalist的修改版

#include <tuple>
#include <cassert>

template <std::size_t L, std::size_t U>
struct visit_impl
{
    template <typename T, typename F>
    static void visit(T& tup, std::size_t idx, F fun)
    {
        static constexpr std::size_t MEDIAN = (U - L) / 2 + L;
        if (idx > MEDIAN)
            visit_impl<MEDIAN, U>::visit(tup, idx, fun);
        else if (idx < MEDIAN)
            visit_impl<L, MEDIAN>::visit(tup, idx, fun);
        else
            fun(std::get<MEDIAN>(tup));
    }
};

template <typename F, typename... Ts>
void visit_at(const std::tuple<Ts...>& tup, std::size_t idx, F fun)
{
    assert(idx <= sizeof...(Ts));
    visit_impl<0, sizeof...(Ts)>::visit(tup, idx, fun);
}

template <typename F, typename... Ts>
void visit_at(std::tuple<Ts...>& tup, std::size_t idx, F fun)
{
    assert(idx <= sizeof...(Ts));
    visit_impl<0, sizeof...(Ts)>::visit(tup, idx, fun);
}

/* example code */

/* dummy template to generate different callbacks */
template <int N>
struct Callback
{
    int Call() const
    {
        return N;
    }
};

template <typename T>
struct CallbackTupleImpl;

template <std::size_t... Indx>
struct CallbackTupleImpl<std::index_sequence<Indx...>>
{
    using type = std::tuple<Callback<Indx>...>;
};

template <std::size_t N>
using CallbackTuple = typename CallbackTupleImpl<std::make_index_sequence<N>>::type;

int main()
{
    CallbackTuple<100> myTuple;
    int value{};
    visit_at(myTuple, 42, [&value](auto& pc) { value = pc.Call(); });
    assert(value == 42);
}

使用此解决方案,对visit_impl 的调用次数为7。使用线性搜索方法,它将改为 58

here 提出的另一个有趣的解决方案甚至设法提供O(1) 访问。然而,以更多存储为代价,因为生成了大小为O(n) 的函数映射。

【讨论】:

    【解决方案7】:

    C++17 非递归

    template <typename T>
    inline constexpr size_t tuple_size_v = std::tuple_size<T>::value;
    
    template <typename T, typename F, std::size_t... I>
    constexpr void visit_impl(T& tup, const size_t idx, F fun, std::index_sequence<I...>)
    {
        assert(idx < tuple_size_v<T>);
        ((I == idx ? fun(std::get<I>(tup)) : void()), ...);
    }
    
    template <typename F, typename... Ts, typename Indices = std::make_index_sequence<sizeof...(Ts)>>
    constexpr void visit_at(std::tuple<Ts...>& tup, const size_t idx, F fun)
    {
        visit_impl(tup, idx, fun, Indices {});
    }
    
    template <typename F, typename... Ts, typename Indices = std::make_index_sequence<sizeof...(Ts)>>
    constexpr void visit_at(const std::tuple<Ts...>& tup, const size_t idx, F fun)
    {
        visit_impl(tup, idx, fun, Indices {});
    }
    

    使用:

    auto tuple = std::tuple { 1, 2.5, 3, 'Z' };
    // print it to cout
    for (size_t i = 0; i < tuple_size_v<decltype(tuple)>; ++i) {
        visit_at(tuple, i, [](auto&& arg) {
            using T = std::decay_t<decltype(arg)>;
            std::cout << *typeid(T).name() << arg << ' ';
        });
    }
    

    输出: i1 d2.5 i3 cZ

    【讨论】:

      【解决方案8】:

      对于 c++11,这里有一个返回指针的简洁方法:

      template <typename Tuple, long template_index = std::tuple_size<Tuple>::value>
      struct tuple_address {
        static void * of(Tuple & tuple, long function_index) {
          if (template_index - 1 == function_index) {
            return &std::get<template_index - 1>(tuple);
          } else {
            return tuple_address<Tuple, template_index - 1>::of(tuple, function_index);
          }
        }
      };
      template <typename Tuple>
      struct tuple_address<Tuple, 0> {
        static void * of(Tuple & tuple, long function_index) {
          return 0;
        }
      };
      template <typename Tuple>
      void * tuple_address_of(Tuple & tuple, long index) {
        return tuple_address<Tuple>::of(tuple, index);
      }
      

      【讨论】:

        猜你喜欢
        • 2021-05-24
        • 1970-01-01
        • 1970-01-01
        • 2014-09-20
        • 2023-03-23
        • 1970-01-01
        • 1970-01-01
        • 2020-06-10
        • 2013-08-13
        相关资源
        最近更新 更多