紧凑和简单的std :: tuple反演

时间:2015-01-30 16:36:57

标签: c++ c++11 template-meta-programming

我是元编程的新手。我看过其他类似的问题,但没有一个问题符合我的要求。

这是我尝试反转std :: tuple。我的主要问题是反转输入元组中的数据。

反转指数的逻辑并不合适,我无法从这个阶段开始。

到目前为止的代码:

//===========================================================!
// type inversion of a tuple

template < typename Tuple, typename T >
struct tuple_push;

template < typename T, typename ... Args >
struct tuple_push<std::tuple<Args...>, T>
{
    typedef std::tuple<Args..., T> type;
};

template < typename Tuple >
struct tuple_reverse;

template < typename T, typename ... Args >
struct tuple_reverse<std::tuple<T, Args...>>
{
    typedef typename tuple_push<typename tuple_reverse<std::tuple<Args...>>::type, T>::type type;
};

template < >
struct tuple_reverse<std::tuple<>>
{
    typedef std::tuple<> type;
};
//===========================================================!


template <typename First, typename ...Tails>
auto inverse(std::tuple<First, Tails...> & data) 
-> decltype(tuple_reverse<std::tuple<First,Tails...>>::type)
    {
        using reverse_tup = tuple_reverse<std::tuple<First, Tails...>>::type;
        static_assert(false, "Confused!")
        return reverse_tup();
    }

期待一个紧凑而简单的解决方案。

2 个答案:

答案 0 :(得分:7)

这是使用C ++ 14的可能解决方案:

template <typename T, std::size_t... indices>
auto invert(T &&tuple, std::index_sequence<indices...>) {
  // Using decay_t as the argument must be a tuple, and this shortens the code
  using tuple_t = std::decay_t<T>;
  constexpr auto tuple_size = std::tuple_size<tuple_t>{};
  return std::tuple<std::tuple_element_t<tuple_size - indices - 1, tuple_t>...>(
      std::get<tuple_size - indices - 1>(std::forward<T>(tuple))...);
}

template <typename T>
auto invert(T &&tuple) {
  return invert(std::forward<T>(tuple),
                std::make_index_sequence<std::tuple_size<std::decay_t<T>>{}>());
}

Demo。 对于C ++ 11,可以使用相同的过程,但必须提供诸如make_index_list之类的辅助模板。

答案 1 :(得分:1)

  

反转指数的逻辑并不合适,我无法从这个阶段开始。

我认为你指的是使用std::index_sequence(C ++ 14)的解决方案, 或类似的,such as Jonathan Wakeley's', 同样@Columbo的解决方案也是不合适的,即使它也是如此 这是一个C ++ 11。

可能你不喜欢&#34;反转指数的逻辑&#34;因为你认为它 有一个不必要的运行时成本。它没有运行时成本。它在编译时执行。

更有可能的是,你知道但只是认为这种解决方案不够优雅,而不是 像你喜欢的那样简单或紧凑。

嗯,你知道这个用于反转序列S的经典递归算法:连续取S项 并将它们推到另一个的前面,最初为空序列S'。最后,S'S相反。

几乎没有什么比这更简单,这里是一个紧凑的C ++ 11实现,应用于元组。 假设标题"tuple_plus.h"提供元函数tuple_reverse的现有定义 及其先决条件。

#include "tuple_plus.h"

namespace detail {

template <size_t I, class Wip, typename ...Ts>
Wip
inverse(std::tuple<Ts...> const & model, Wip && wip = std::tuple<>(),
            typename std::enable_if<(I >= sizeof...(Ts))>::type * = nullptr)
{
    return wip;
}

template <size_t I, class Wip, typename ...Ts>
typename tuple_reverse<std::tuple<Ts...>>::type
inverse(std::tuple<Ts...> const & model, Wip && wip = std::tuple<>(),
            typename std::enable_if<(I < sizeof...(Ts))>::type * = nullptr)
{
    return
    inverse<I + 1>(model,std::tuple_cat(std::make_tuple(std::get<I>(model)),wip));
}

} // namespace detail

template<typename ...Ts>
typename tuple_reverse<std::tuple<Ts...>>::type
inverse(std::tuple<Ts...> const & tup)
{
    return detail::inverse<0,std::tuple<>>(tup);
}

当然,我们不能循环而不是元组元素,因为它们只能通过std::get<I>(tup)访问, 对于常量索引I。因此,实施不能像std::vector<T>那样紧凑。我们 必须遵循索引常量的模板元递归的通常模式。我们需要一对SFINAE重载, 当I具有限制值(I >= sizeof...(Ts))时编译器选择一个,而I具有其他任何其他选项时选择另一个 值(I < sizeof...(Ts))。然后我们需要实现您真正想要的功能模板

template<typename ...Ts>
typename tuple_reverse<std::tuple<Ts...>>::type
inverse(std::tuple<Ts...> const & tup);

作为此实现的包装器来初始化和隐藏递归参数。但有了这些 不可避免的安排,这是经典的序列反转算法,适用于C ++ 11中的元组, 没有任何index_sequence拐杖。

N.B。该声明与您帖子中的声明略有不同: -

  • 它将接受0长度的元组std::tuple<>,以及更长的元组。这个 更好,因为你的返回类型tuple_reverse<Tuple>::type的通用定义 专门用于std::tuple<>。所以这种方式无论何时定义函数 return-type是。

  • 参数传递为const &,而不只是&。你不打算修改 输入元组,因此该函数应接受std::tuple<Ts...> const个参数。

现在,我将解释为什么这种优雅的解决方案与非常有效的解决方案相比较 在index_sequence设备上。

这是一个可以测试元组反转解决方案的程序 实现所需的界面。

#include "tuple_plus.h"
#include <type_traits>

///////////////////////////////////////////////////
//
// Paste your implementation here
//
///////////////////////////////////////////////////

///////////////////////////////////////////////////
// Testing it...

#include <iostream>

using namespace std;

struct item
{
    static unsigned ctor_count;
    static unsigned copy_count;
    item()
    : id(ctor_count++) {}
    item(item const & other)
    : id(other.id) {
        ++copy_count;
    }
    item & operator=(item const & other) {
        id = other.id;
        ++copy_count;
        return *this;
    }
    static void clear() {
        ctor_count = copy_count = 0;
    }
    unsigned id;
};

unsigned item::ctor_count;
unsigned item::copy_count;

ostream & operator<<(ostream & out, item const & e)
{
    return out << "item #" << e.id;
}

template<unsigned I = 0, typename ...Ts>
typename enable_if<(I >= sizeof...(Ts))>::type
tuple_print(tuple<Ts...> const &)
{
    cout << "\n";
}

template<unsigned I = 0, typename ...Ts>
typename enable_if<(I < sizeof...(Ts))>::type
tuple_print(tuple<Ts...> const & tup)
{
    cout << '[' << get<I>(tup) << "] ";
    tuple_print<I + 1>(tup);
}

template<unsigned I>
void test()
{
    auto tup_tup =
    tuple<
        tuple<>,
        tuple<item>,
        tuple<item,item>,
        tuple<item,item,item>,
        tuple<item,item,item,item>,
        tuple<item,item,item,item,item>,
        tuple<item,item,item,item,item,item>>();

    item::clear();
    auto const & tup = get<I>(tup_tup);
    cout << "In..." << endl;
    tuple_print(tup);
    cout << "Out..." << endl;
    tuple_print(inverse(tup));
    cout << "To invert a " << I << "-element tuple\n"
    << "I copied " << item::copy_count << " elements\n";
}

int main()
{
    test<0>(),test<1>(),test<2>(),test<3>(),test<4>(),test<5>(),test<6>();
    return 0;
}

将经典解决方案剪切并粘贴到:

    // Paste your implementation here

然后使用-std=c++11进行编译并运行。输出运行经典算法 长度为0到6的标本元组,并在每种情况下显示a)算法的结果 确实反转了元组和b)反转元组的成本,用元组复制操作来衡量。

In...

Out...

To invert a 0-element tuple
I copied 0 elements
In...
[item #20]
Out...
[item #20]
To invert a 1-element tuple
I copied 3 elements
In...
[item #19] [item #18]
Out...
[item #18] [item #19]
To invert a 2-element tuple
I copied 7 elements
In...
[item #17] [item #16] [item #15]
Out...
[item #15] [item #16] [item #17]
To invert a 3-element tuple
I copied 12 elements
In...
[item #14] [item #13] [item #12] [item #11]
Out...
[item #11] [item #12] [item #13] [item #14]
To invert a 4-element tuple
I copied 18 elements
In...
[item #10] [item #9] [item #8] [item #7] [item #6]
Out...
[item #6] [item #7] [item #8] [item #9] [item #10]
To invert a 5-element tuple
I copied 25 elements
In...
[item #5] [item #4] [item #3] [item #2] [item #1] [item #0]
Out...
[item #0] [item #1] [item #2] [item #3] [item #4] [item #5]
To invert a 6-element tuple
I copied 33 elements

调查费用的顺序:

0, 3, 7, 12, 18, 25, 33,...

你可以满足自己的功能:

Cost(n) = (n squared + 5n) / 2

所以这个算法在元组大小上是二次方的。

如果你想要练习,你可以找出另一个的元组实现 用于反转序列的库存递归算法。不那么简单或 紧凑就像这一样,它就是那个:交换第一个和最后一个元素 在你已经颠倒了它们之间的序列之后。成本可能会低一些,但仍然是二次方。

这令人痛苦,因为众所周知,扭转一系列事物 是线性问题:例如,经典算法的明显C ++改编 转换std::vector会有Cost(n) = 4n

众所周知的事实背后隐含的假设是序列是a 对于某些Container<T>ContainerT;所以反转算法是免费的 在一个位置修改包含的序列,而不在其他位置修改它,全部 同时还有Container<T>

但是,扭转std::tuple的麻烦就是 std::tuple<....,A,....,B,....>类型不同 std::tuple<....,B,....,A,....>std::tuple<....,A,....>。所以你不能 实际上通过推送另一个元素来反转std::tuple tup tup的前面;或者 A中交换 B tup 等。 要在tup上模拟任何此类成员操作,您必须构建一个整体 新元组,不同类型,复制所有 tup的元素,甚至更多。 要拿起香蕉,你必须抬起整个大猩猩。

考虑到这一点,将@Columbo的实现粘贴到测试程序中。你&#39; 11 需要将代码中的invert更改为inverse。编 与std=c++14并运行。 (对于此选项,您需要gcc 4.9或clang 3.5, 和gcc 4.9将发出一个不重要的编译器警告。

现在输出是:

In...

Out...

To invert a 0-element tuple
I copied 0 elements
In...
[item #20]
Out...
[item #20]
To invert a 1-element tuple
I copied 1 elements
In...
[item #19] [item #18]
Out...
[item #18] [item #19]
To invert a 2-element tuple
I copied 2 elements
In...
[item #17] [item #16] [item #15]
Out...
[item #15] [item #16] [item #17]
To invert a 3-element tuple
I copied 3 elements
In...
[item #14] [item #13] [item #12] [item #11]
Out...
[item #11] [item #12] [item #13] [item #14]
To invert a 4-element tuple
I copied 4 elements
In...
[item #10] [item #9] [item #8] [item #7] [item #6]
Out...
[item #6] [item #7] [item #8] [item #9] [item #10]
To invert a 5-element tuple
I copied 5 elements
In...
[item #5] [item #4] [item #3] [item #2] [item #1] [item #0]
Out...
[item #0] [item #1] [item #2] [item #3] [item #4] [item #5]
To invert a 6-element tuple
I copied 6 elements

费用的顺序是0,1,2,3,4,5,6,...。对于此解决方案,Cost(n) = n: 它是最有效的。

在此解决方案中使用index_sequence设备是 the essense 其最佳效率。如果要避免二次运行时成本 通过模拟成员操作来反转tup,唯一的选择是 只需构造反转元组,用元素初始化 已经在编译时从tup中的对应者映射了。{ 映射:

Index I -> tuple_size - I - 1

在运行时,没有任何反应,但反转元组的构造, 它很快就会完成。

但是要在编译时执行该映射,您必须拥有 indices 的列表 编译时tup的元素。这就是index_sequence 设备提供。

如果C ++ 14绝对禁止,你可以接受一个flyweight C ++ 11替代品 std::index_sequence courtesty of Andy Prowl

有了这个,你可以有以下解决方案:

namespace detail {

// This is...

template<int... Is>
struct seq { };

template<int N, int... Is>
struct gen_seq : gen_seq<N - 1, N - 1, Is...> { };

template<int... Is>
struct gen_seq<0, Is...> : seq<Is...> {};

// ... the flyweight index_sequence apparatus.
// You can put it in a header.

template<typename Tuple, int ...Is>
typename tuple_reverse<typename std::decay<Tuple>::type>::type
invert(Tuple && tup, seq<Is...>)
{
    return
        std::make_tuple(
            std::get<std::tuple_size<
                typename std::decay<Tuple>::type>::value - Is - 1>
                    (std::forward<Tuple>(tup))...);
}

} //namespace detail

template<typename ...Ts>
typename tuple_reverse<std::tuple<Ts...>>::type
inverse(std::tuple<Ts...> const & tup)
{
    return detail::invert(tup,detail::gen_seq<(sizeof...(Ts))>());
}

在测试程序中,其输出与@ Columbo相同。

道德:std::index_sequence(或更一般地,std::integer_sequence) 是一个非常优雅和基本的工具,可以在C ++中有效地使用TMP。 这就是为什么它在C ++ 14标准库中的原因。 (顺便说一下,Jonathan Wakeley is its author`