在尝试自己制作一个std::get<N>
(std::tuple)
方法之后,我不太确定它是如何由编译器实现的。我知道std::tuple
有这样的构造函数,
tuple(Args&&... args);
但具体args...
分配给什么?我认为这对于了解如何工作很有用,std::get
因为需要将参数放置在某个地方才能访问它们......
这是一个类似tuple
类的粗略玩具实现。
首先,一些元编程样板,用于表示整数序列:
template<int...> struct seq {};
template<int max, int... s> struct make_seq:make_seq< max-1, max-1, s... > {};
template<int... s> struct make_seq<0, s...> {
typedef seq<s...> type;
};
template<int max> using MakeSeq = typename make_seq<max>::type;
接下来,实际存储数据的标记类:
template<int x, typename Arg>
struct foo_storage {
Arg data;
};
每当我们想在编译时将数据与某个标记(在本例中为整数)相关联时,这种标记技术是一种常见的模式。标签(an int
here)通常不在存储中的任何地方使用,它仅用于标记存储。
foo_helper
将一个序列和一组参数解包成一堆foo_storage
,并以线性方式从它们继承。这是一种非常常见的模式——如果你经常这样做,你最终会创建为你做这件事的元编程工具:
template<typename Seq, typename... Args>
struct foo_helper {};
template<int s0, int... s, typename A0, typename... Args>
struct foo_helper<seq<s0, s...>, A0, Args...>:
foo_storage<s0, A0>,
foo_helper<seq<s...>, Args...>
{};
我的粗略tuple
类型,foo
创建了一个包含一系列索引和参数的包,并将其传递给上面的助手。助手然后创建一堆包含父类的标记数据:
template<typename... Args>
struct foo: foo_helper< MakeSeq<sizeof...(Args)>, Args... > {};
我从 的主体中删除了所有内容foo
,因为不需要实现get
.
get
非常简单:我们采用存储类型(不是元组类型),并且显式template
参数N
消除了foo_storage<n, T>
我们要访问的歧义。现在我们有了存储类型,我们只需返回数据字段:
template<int N, typename T>
T& get( foo_storage<N, T>& f )
{ return f.data; }
template<int N, typename T>
T const& get( foo_storage<N, T> const& f )
{ return f.data; }
我们正在使用 C++ 语言的重载机制来完成繁重的工作。当您使用类实例调用函数时,该实例作为每个父类都将检查是否可以使它们中的任何一个匹配。在N
固定的情况下,只有一个父类是有效参数,因此父类(因此T
)被自动推导。
最后,一些基本的测试代码:
#include <iostream>
int main() {
foo<int, double> f;
get<0>( f ) = 7;
get<1>( f ) = 3.14;
std::cout << get<0>(f) << "," << get<1>(f) << "\n";
}
定义具有可变数量和类型的数据成员的类或结构通常很有用,这些数据成员是在编译时定义的。典型示例是 std::tuple,但有时需要定义自己的自定义结构。这是一个使用复合定义结构的示例(而不是像std::tuple
.
从一般(空)定义开始,它也可作为后续专业化中递归终止的基本情况:
template<typename ... T>
struct tuple {};
这已经允许我们定义一个空结构,tuple<>
数据,尽管它还不是很有用。
接下来是递归案例专业化:
template<typename T, typename ... Rest>
struct tuple<T, Rest ...>
{
tuple(const T& first, const Rest& ... rest)
: first(first)
, rest(rest...)
{}
T first;
tuple<Rest ... > rest;
};
现在这足以让我们创建任意数据结构,例如 tuple<int, float, std::string> data(1, 2.1, "hello")。
发生什么了?首先,请注意,这是一种专业化,其要求是至少T
存在一个可变参数模板参数(即上述),而不关心 pack 的具体构成Rest
。知道 T 存在允许定义它的数据成员,first
。其余数据递归打包为tuple<Rest ... >
rest。构造函数初始化这两个成员,包括对 rest 成员的递归构造函数调用。
您可以将其可视化如下:
tuple <int, float>
-> int first
-> tuple <float> rest
-> float first
-> tuple <> rest
-> (empty)
等到助手类。这次我们需要一个空的前向声明和两个专业化。首先声明:
template<size_t idx, typename T>
struct helper;
现在是基本情况(当idx==0
)。在这种情况下,我们只返回第一个成员:
template<typename T, typename ... Rest>
struct helper<0, tuple<T, Rest ... >>
{
static T get(tuple<T, Rest...>& data)
{
return data.first;
}
};
在递归的情况下,我们递减 idx 并为其余成员调用助手:
template<size_t idx, typename T, typename ... Rest>
struct helper<idx, tuple<T, Rest ... >>
{
static auto get(tuple<T, Rest...>& data)
{
return helper<idx-1, tuple<Rest ...>>::get(data.rest);
}
};
举例来说,假设我们有tuple<int, float>
数据并且我们需要data.get<1>()
. 这会调用helper<1, tuple<int, float>>::get(data)
(第二个专业化),然后调用helper<0, tuple>::get(data.rest)
,最终返回(通过第一个专业化,因为现在 idx 为 0)data.rest.first。
就是这样了!这是整个功能代码,在 main 函数中使用了一些示例:
#include <type_traits>
#include <iostream>
using namespace std;
namespace my {
template <typename ...Ts>
struct tuple {};
template <typename T, typename ...Ts>
struct tuple <T, Ts...> {
tuple(T first, Ts... rest) :
first(first), rest(rest...){}
T first;
tuple<Ts...> rest;
};
namespace detail {
template <int N, typename ...Ts>
struct helper;
template <typename T, typename ...Ts>
struct helper <0, tuple<T, Ts...>> {
static auto get(tuple<T, Ts...> ds){
return ds.first;
}
};
template <int N, typename T, typename ...Ts>
struct helper <N, tuple<T, Ts...>> {
static auto get(tuple<T, Ts...> ds){
return helper<N-1, tuple<Ts...>>::get(ds.rest);
}
};
}
template <int N, typename ...Ts>
auto get(tuple<Ts...> ds){
return detail::helper<N, decltype(ds)>::get(ds);
}
}
int main(){
my::tuple <int, bool, float> test = {5, false, 10.5};
std::cout << my::get<0>(test) << endl;
std::cout << my::get<1>(test) << endl;
std::cout << my::get<2>(test) << endl;
}