模板参数包访问第N个类型和第N个元素

60
以下文章是我找到的第一个模板参数包提案。
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1603.pdf的第16页中,它讨论了引入两个新运算符[]和<>来访问参数包元素和参数包类型。
建议的语法涉及两个新运算符:.[]用于访问值,.<>用于访问类型。例如:
template<int N, typename Tuple> struct tuple_element;
template<int N, ... Elements>
struct tuple_element<tuple<Elements...> >
{
    typedef Elements.<N> type;
};

template<int N, ... Elements>
Elements.<N>& get(tuple<Elements...>& t)
{ return t.[N]; }

template<int N, ... Elements>
const Elements.<N>& get(const tuple<Elements...>& t)
{ return t.[N]; }

这些操作员在哪里?如果没有,它们的替代品是什么?

2
它们的替代方案可能是将参数包装入std::tuple中,并使用std::getstd::tuple_element,这些都是递归实现的。还可以参考此答案 - dyp
2
@DyP:std::getstd::tuple_element并不一定需要递归实现,它们可以通过索引、重载解析和派生到基类的转换来实现而不需要递归。 - Xeo
@Xeo:由于可变参数列表目前不支持通过索引访问,我不知道任何一种实现方式不是递归的或者不是列出所有类型(然后回退到递归方法来覆盖理论上无限数量的参数)。虽然我会通过基类实现成员访问,但确定要访问的正确类型将使用std::tuple_element<...>,这将是递归函数。 - Dietmar Kühl
@Xeo,从下面链接的答案中可以看出,我使用了您的O(logN)索引生成器实现了一个等价于std::tuple_element的类型推导和索引 -- 但是索引需要递归(或手动)创建。 - dyp
5个回答

61

已经有其他人回答了,可以通过 std::tuple 来实现。如果你想访问参数包的第 N 个类型,你可能会发现下面的元函数很方便:

其他人已经回答过,可以使用std::tuple来完成。如果您想要访问参数包的第N个类型,则可以使用以下元函数:

template<int N, typename... Ts> using NthTypeOf =
        typename std::tuple_element<N, std::tuple<Ts...>>::type;

使用方法:

using ThirdType = NthTypeOf<2, Ts...>;

1
有没有办法在运行时使它工作?即接受非constexpr作为第一个参数? - narengi
@narengi,我需要了解更多关于你想做什么的信息。最好是你提出一个单独的问题,并附上这些细节(如果还没有被问过的话)。 - Emile Cormier
发布:https://dev59.com/uVcP5IYBdhLWcg3wVok6如何在运行时索引C++11模板参数包以访问第n个类型? - narengi

39
C++11没有相应的运算符,这就是它们被提出的原因。在C++11中,您需要自己提取相应的信息或使用已经执行必要操作的类。最简单的方法可能是只使用已经实现了相应逻辑的std::tuple<T...>
如果你想知道std::tuple<T...>如何实现这些操作:它基本上是一个使用相当糟糕的函数式编程符号的函数式编程练习。一旦您知道如何获取序列的第n个类型,通过从基于索引和类型参数化的基类继承来获取第n个元素就很容易了。实现类似tuple_element<N, T...>的东西可能看起来像这样:
template <int N, typename... T>
struct tuple_element;

template <typename T0, typename... T>
struct tuple_element<0, T0, T...> {
    typedef T0 type;
};
template <int N, typename T0, typename... T>
struct tuple_element<N, T0, T...> {
    typedef typename tuple_element<N-1, T...>::type type;
};

在实现像 std::tuple<T...> 这样的东西时,更具挑战性的部分是构造出一个索引列表,以便您得到类型和整数的并行列表,然后可以对其进行扩展。例如,对于一组基类,可以使用以下内容(内部细节的实际外观可能会有所不同,但类型和索引的并行参数包的基本思想将在某种程度上存在):

template <typename... T, int... I>
class tuple_base<tuple_types<T...>, tuple_indices<I...>>:
     public tuple_field<T, I>... {
};

26

如何访问第N个元素?

使用std::forward_as_tuple

template <int I, class... Ts>
decltype(auto) get(Ts&&... ts) {
  return std::get<I>(std::forward_as_tuple(ts...));
}

使用示例:

template<class...Ts>
void foo(Ts&&...ts){

  auto& first = get<0>(ts...);
  auto second = get<1>(ts...);

  first = 'H';
  second = 'E';

  (std::cout << ... << ts);
}

foo('h','e','l','l','o');
// prints "Hello"
这个答案是为了补充Emile Cormier的答案,后者仅提供了第n种类型。

ts 应该总是通过前向传递进行传递:forward<Ts>(ts)... - Chen Li
你能解释一下为什么吗?我认为在这种情况下并不重要,因为ts...的元素是char,所以额外的复制而不是移动并不重要。事实上,如果你使用forward<Ts>(ts)...,那么第二个元素的内容就会被移动到second中。因此,如果ts...的元素是更复杂的对象,则在命令(std::cout << ... << ts);中访问无效内存。 - tom
抱歉,这是我的错。字符串字面值始终是左值。 - Chen Li
1
这是一个很好的解决方案。虽然当我尝试使用0个可变参数时,std::get会给出一个静态断言:static_assert failed due to requirement '_Always_false<std::integral_constant<unsigned long long, 0>>' "tuple index out of bounds"。我创建了另一个来处理0个参数:template <int n> decltype(auto) get() { return 0; } - Saleem
应该把 ts ...std::forward 转发到 forward_as_tuple 中。否则它们总是作为引用传递。 - davidhigh

7

获取打包中的第N个元素,可以这样写:

选项1

使用tuple_element获取第N个元素的返回类型:

template<size_t index, typename T, typename... Ts>
inline constexpr typename enable_if<index==0, T>::type
get(T&& t, Ts&&... ts) {
    return t;
}

template<size_t index, typename T, typename... Ts>
inline constexpr typename enable_if<(index > 0) && index <= sizeof...(Ts),
          typename tuple_element<index, tuple<T, Ts...>>::type>::type
get(T&& t, Ts&&... ts) {
    return get<index-1>(std::forward<Ts>(ts)...);
}

// below is optional - just for getting a more readable compilation error
// in case calling get with a bad index

inline template<long long index, typename... Ts>
constexpr bool index_ok() {
    return index >= 0 && index < sizeof...(Ts);
}

template<long long index, typename T, typename... Ts>
inline constexpr
typename enable_if<!index_ok<index, T, Ts...>(), T>::type
get(T&& t, Ts&&... ts) {
    static_assert(index_ok<index, T, Ts...>(),
        "bad index in call to get, smaller than zero or above pack size");
    return t;
}

选项2

不使用 tuple,依赖于 auto 返回类型,具体来说是在 C++14 中使用 decltype(auto),并且将 enable_if 作为模板参数而不是返回类型:

template<size_t index, typename T, typename... Ts,
    typename enable_if<index==0>::type* = nullptr>
inline constexpr decltype(auto) get(T&& t, Ts&&... ts) {
    return std::forward<T>(t); 
}

template<size_t index, typename T, typename... Ts,
    typename enable_if<(index > 0 && index <= sizeof...(Ts))>::type* = nullptr>
inline constexpr decltype(auto) get(T&& t, Ts&&... ts) {
    return get<index-1>(std::forward<Ts>(ts)...);
}

template<long long index, typename... Ts>
inline constexpr bool index_ok() {
    return index >= 0 && index < (long long)sizeof...(Ts);
}

// block (compilation error) the call to get with bad index,
// providing a readable compilation error
template<long long index, typename T, typename... Ts,
    typename enable_if<(!index_ok<index, T, Ts...>())>::type* = nullptr>
inline constexpr decltype(auto) get(T&& t, Ts&&... ts) {
    static_assert(index_ok<index, T, Ts...>(),
        "bad index in call to get, smaller than zero or above pack size");
    return std::forward<T>(t); // need to return something...
                               // we hope to fail on the static_assert above
}

使用示例:

template<size_t index, typename... Ts>
void resetElementN(Ts&&... ts) {
    get<index>(std::forward<Ts>(ts)...) = {}; // assuming element N has an empty ctor
}

int main() {
    int i = 0;
    string s = "hello";
    get<0>(i,2,"hello","hello"s, 'a') += get<0>(2);
    get<1>(1,i,"hello",4) += get<1>(1, 2);
    get<3>(1,2,"hello",i) += get<2>(0, 1, 2);    
    get<2>(1,2,s,4) = get<2>(0, 1, "hi");
    cout << i << ' ' << s << endl;    
    resetElementN<1>(0, i, 2);
    resetElementN<0>(s, 1, 2);
    cout << i << ' ' << s << endl;    

    // not ok - and do not compile
    // get<0>(1,i,"hello","hello"s) = 5;
    // get<1>(1,i*2,"hello") = 5;
    // get<2>(1,i*2,"hello")[4] = '!';
    // resetElementN<1>(s, 1, 2);

    // ok
    const int j = 2;
    cout << get<0>(j,i,3,4) << endl;

    // not ok - and do not compile
    // get<0>(j,i,3,4) = 5;    

    // not ok - and do not compile
    // with a readable compilation error
    // cout << get<-1>("one", 2, '3') << endl;
    // cout << get<3>("one", 2, '3') << endl;
}
代码
选项 1:http://coliru.stacked-crooked.com/a/60ad3d860aa94453
选项 2:http://coliru.stacked-crooked.com/a/09f6e8e155612f8b

3
我们可以实现一个简单的函数来直接获取第n个参数而不需要任何递归调用,但是在编译时期有很多纯类型操作。
首先让我们来看一下核心代码:
template<class...Ts>
struct GetImp {
  template<class T, class...Us>
  static decltype(auto) impl(Ts&&..., T&& obj, Us&&...) {
    return std::forward<T>(obj);
  }
};

template<size_t n, class...Ts>
decltype(auto) get(Ts&&...args) {
  static_assert(n<sizeof...(args), "index over range");
  return Transform<GetImp, Before_s<n, Seq<Ts...>> >
    ::impl(std::forward<Ts>(args)...);
}

Transform是什么意思?

例如,如果我们有一个类型T,它是std::tuple<int,double,float>, 那么Transform<GetImp,T>将是GetImp<int,double,float>。 注意,我定义了另一个空结构体“Seq”,而不是std::tuple,以更少的编译时间实现相同的功能。 (实际上,它们都可以很快地编译,但我认为一个空结构体会更有效)因此,Before_s<n,Seq<Ts...>> 生成一个Seq<?>, 然后我们将其转换为GetImp,这样我们就可以知道[0]〜[n-1]参数的类型,并直接索引第n个参数。 例如,Before_s<3,Seq<T0,T1,T2,T3,T4...>>Seq<T0,T1,T2>Before_s<2,Seq<T0,T1,T2,T3,T4...>>Seq<T0,T1> 等等。 我们使用Before_s来处理我们的Seq类型,以减少编译时间,当我们使用一个元函数实现另一个元函数时,可以用更少的编译时间。

实现

#define OMIT_T(...) typename __VA_ARGS__::type

template<class...Args>
struct Seq { };

template< template<class...> class Dst >
struct TransformImp{
    template< template<class...>class Src, class...Args >
    static Dst<Args...> from(Src<Args...>&&);
};
template< template<class...> class Dst, class T>
using Transform = decltype(TransformImp<Dst>::from(std::declval<T>()));
template<class T>
using Seqfy = Transform<Seq, T>;


template<class...>struct MergeImp;
template<class...Ts, class...Others>
struct MergeImp<Seq<Ts...>, Seq<Others...>>
{
  using type = Seq<Ts..., Others...>;
};
template<class first, class second>
using Merge = OMIT_T(MergeImp<Seqfy<first>, Seqfy<second> >);
template<class T, class U>
using Merge_s = OMIT_T(MergeImp<T, U>);

template<size_t, class...>struct BeforeImp;

template<size_t n, class T, class...Ts>
struct BeforeImp<n, Seq<T, Ts...>> {
    static_assert(n <= sizeof...(Ts)+1, "index over range");
    using type = Merge_s<Seq<T>, OMIT_T(BeforeImp<n - 1, Seq<Ts...>>)>;
};

template<class T, class...Ts>
struct BeforeImp<1, Seq<T, Ts...>> {
    using type = Seq<T>;
};
template<class T, class...Ts>
struct BeforeImp<0, Seq<T, Ts...>> {
    using type = Seq<>;
};
template<size_t n>
struct BeforeImp<n, Seq<>> {
    using type = Seq<>;
};

template<size_t n, class T>
using Before = OMIT_T(BeforeImp<n, Seqfy<T>>);
template<size_t n, class T>
using Before_s = OMIT_T(BeforeImp<n, T>);

编辑:高级实现

我们不需要使用Before_s来计算第n个类型之前的n-1种类型,而是可以忽略它们:

struct EatParam{
    constexpr EatParam(...)noexcept{}
};

template<size_t n>
struct GenSeqImp {
  using type = Merge_s<OMIT_T(GenSeqImp<n / 2>), OMIT_T(GenSeqImp<n - n / 2>)>;
};
template<>
struct GenSeqImp<0> {
  using type = Seq<>;
};
template<>
struct GenSeqImp<1> {
  using type = Seq<EatParam>;
};

template<size_t n>
using GenSeq = OMIT_T(GenSeqImp<n>);


template<class...Ts>
struct GetImp {
  template<class T>
  static constexpr decltype(auto) impl(Ts&&..., T&& obj, ...)noexcept {
    return std::forward<T>(obj);
  }
};


template<size_t n, class...Ts>
constexpr decltype(auto) get(Ts&&...args)noexcept {
  static_assert(n<sizeof...(args), "index over range.");
  //return Transform<GetImp, Before_s<n, Seq<Ts...>> >
  return Transform<GetImp, GenSeq<n>>
    ::impl(std::forward<Ts>(args)...);
}

此外,这里有一篇关于实现获取第n个类型的非常有趣的文章。
感谢他们的工作,我以前不知道我们可以使用(...)来做这个技巧。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接