STL tuple源码分析
和pair一样,tuple也是STL中非常常见的数据结构。pair是个二元组,只支持两个类型参数,tuple则是个多元组,可以支持多个类型参数。因此,在具体实现上,要比pair复杂一些。我们还是以MSVC提供的源码为基础,对tuple进行分析。
tuple是多元组,意味着它的模板参数是不固定的,是一个可变参数模板类。STL提供了两种tuple,一种tuple是特化后无参的,一种是可变参数的。在可变参数模板类的实现里,只取出一个参数进行处理,使用继承的方式,把剩下的工作交给少了一个参数的模板父类去做。如此下去,最后就会走到无参版本的tuple,完成最后一步的工作。STL的设计还是非常巧妙的。
template <>
class tuple<> {
};
template <class _This, class... _Rest>
class tuple<_This, _Rest...> : private tuple<_Rest...> { // recursive tuple definition
};
还是先从构造函数看起。无参版本的tuple构造函数都非常简单:
template <>
class tuple<> { // empty tuple
public:
constexpr tuple() noexcept = default; /* strengthened */
constexpr tuple(const tuple&) noexcept /* strengthened */ {} // TRANSITION, ABI: should be defaulted
template <class _Alloc>
constexpr tuple(allocator_arg_t, const _Alloc&) noexcept /* strengthened */ {}
template <class _Alloc>
constexpr tuple(allocator_arg_t, const _Alloc&, const tuple&) noexcept /* strengthened */ {}
template <class _Tag, enable_if_t<is_same_v<_Tag, _STD _Exact_args_t>, int> = 0>
constexpr tuple(_Tag) noexcept /* strengthened */ {}
template <class _Tag, class _Alloc, enable_if_t<is_same_v<_Tag, _STD _Alloc_exact_args_t>, int> = 0>
constexpr tuple(_Tag, const _Alloc&) noexcept /* strengthened */ {}
};
可以看到构造函数中有一些形如_Exact_args_t
和_Alloc_exact_args_t
的tag字段,它们是用来区分构造函数的用途的。如果没有这些tag,tuple构造的时候就会直接把每个参数都作为tuple的一部分进行处理。tag类型的定义很简单,就是一个空的struct,STL的注释中也标注了它们的用途。
struct _Exact_args_t {
explicit _Exact_args_t() = default;
}; // tag type to disambiguate construction (from one arg per element)
struct _Unpack_tuple_t {
explicit _Unpack_tuple_t() = default;
}; // tag type to disambiguate construction (from unpacking a tuple/pair)
struct _Alloc_exact_args_t {
explicit _Alloc_exact_args_t() = default;
}; // tag type to disambiguate construction (from an allocator and one arg per element)
struct _Alloc_unpack_tuple_t {
explicit _Alloc_unpack_tuple_t() = default;
}; // tag type to disambiguate construction (from an allocator and unpacking a tuple/pair)
多参数版本的tuple构造函数要复杂得多了,首先是默认构造函数:
template <class _This2 = _This,
enable_if_t<conjunction_v<_STD is_default_constructible<_This2>, _STD is_default_constructible<_Rest>...>,
int> = 0>
constexpr explicit(
!conjunction_v<_Is_implicitly_default_constructible<_This2>, _Is_implicitly_default_constructible<_Rest>...>)
tuple() noexcept(conjunction_v<is_nothrow_default_constructible<_This2>,
is_nothrow_default_constructible<_Rest>...>) // strengthened
: _Mybase(), _Myfirst() {}
enable_if_t限制了只有传入参数类型有默认构造函数时,这个tuple的默认构造函数才成立;explicit里的一坨代码则表示,只要传入参数类型中,有一个类型是必须explicit才能默认构造的,那么tuple也得是explicit默认构造的;noexcept里的一坨代码说明了只有传入参数类型的默认构造函数都不会抛出异常时,tuple的默认构造函数才不会抛出异常。这个吟唱的代码属实是有点长的,它具体的实现却是十分简洁,就是调用基类的默认构造函数,和取出的第一个参数类型的默认构造函数。
然后就是tuple最常用的构造函数了,所有传入的参数都视为tuple的一部分,传入的参数支持常量左值引用和右值引用。
template <class _This2 = _This, enable_if_t<_Tuple_constructible_v<tuple, const _This2&, const _Rest&...>, int> = 0>
constexpr explicit(_Tuple_conditional_explicit_v<tuple, const _This2&, const _Rest&...>) tuple(
const _This& _This_arg, const _Rest&... _Rest_arg) noexcept(conjunction_v<is_nothrow_copy_constructible<_This2>,
is_nothrow_copy_constructible<_Rest>...>) // strengthened
: tuple(_Exact_args_t{}, _This_arg, _Rest_arg...) {}
template <class _This2, class... _Rest2,
enable_if_t<conjunction_v<_STD _Tuple_perfect_val<tuple, _This2, _Rest2...>,
_STD _Tuple_constructible_val<tuple, _This2, _Rest2...>>,
int> = 0>
constexpr explicit(_Tuple_conditional_explicit_v<tuple, _This2, _Rest2...>) tuple(_This2&& _This_arg,
_Rest2&&... _Rest_arg) noexcept(_Tuple_nothrow_constructible_v<tuple, _This2, _Rest2...>) // strengthened
: tuple(_Exact_args_t{}, _STD forward<_This2>(_This_arg), _STD forward<_Rest2>(_Rest_arg)...) {}
同样需要在编译期间检查构造函数是否合法。_Tuple_constructible_v
就是做这个的,它长这样:
template <class _Dest, class... _Srcs>
_INLINE_VAR constexpr bool _Tuple_constructible_v =
_Tuple_constructible_v0<tuple_size_v<_Dest> == sizeof...(_Srcs), _Dest, _Srcs...>;
可以看出_Tuple_constructible_v0
的第一个模板参数是个bool值,要么为true要么为false:
template <bool _Same, class _Dest, class... _Srcs>
_INLINE_VAR constexpr bool _Tuple_constructible_v0 = false;
template <class... _Dests, class... _Srcs>
_INLINE_VAR constexpr bool _Tuple_constructible_v0<true, tuple<_Dests...>, _Srcs...> =
conjunction_v<is_constructible<_Dests, _Srcs>...>;
如果为false,则_Tuple_constructible_v0
的值直接就是false了,如果为true,则当所有_Dests
参数都可以被_Srcs
参数所构造时,它的值才为true。那么这里其实就可以推断出来了,_Tuple_constructible_v
首先判断tuple的size是否和传入参数的数量一致,如果不一致则构造函数非法;如果一致,进一步判断tuple的每个参数是否能用传入的参数进行构造,只有都满足条件时构造函数才合法。比如下面这个例子:
#include <iostream>
#include <utility>
#include <tuple>
#include <type_traits>
using namespace std;
class A
{
};
class B
{
public:
B(int x) {}
};
int main()
{
tuple<int, int> t1(1, 2); // ok
tuple<int, int> t2(1); // error
tuple<int, A> t3(1, 2); // error
tuple<int, B> t4(1, 2); // ok
return 0;
}
_Tuple_conditional_explicit_v
也差不多,用来判断构造函数是不是explicit的,只要tuple的某个参数是explicit的,那么tuple的构造函数就是explicit的。这里判断某个参数是否是explicit,只要判断传入参数能不能直接convert为目标参数就行了,两者其实是等价的。
我们注意到这里调用的构造函数是带有_Exact_args_t
这个tag的,实际上真正执行构造的函数为
template <class _Tag, class _This2, class... _Rest2, enable_if_t<is_same_v<_Tag, _STD _Exact_args_t>, int> = 0>
constexpr tuple(_Tag, _This2&& _This_arg, _Rest2&&... _Rest_arg)
: _Mybase(_Exact_args_t{}, _STD forward<_Rest2>(_Rest_arg)...), _Myfirst(_STD forward<_This2>(_This_arg)) {}
利用这个tag,就可以逐一使用参数递归地构造完tuple了。这里的构造函数参数是右值引用,实现里又使用了forward转发,这样保证无论传入参数是左值引用还是右值引用都不会出现问题。
再来看看tuple的拷贝构造函数和移动构造函数:
tuple(const tuple&) = default;
tuple(tuple&&) = default;
template <class... _Other, enable_if_t<conjunction_v<_STD _Tuple_constructible_val<tuple, const _Other&...>,
_STD _Tuple_convert_val<tuple, const tuple<_Other...>&, _Other...>>,
int> = 0>
constexpr explicit(_Tuple_conditional_explicit_v<tuple, const _Other&...>)
tuple(const tuple<_Other...>& _Right) noexcept(
_Tuple_nothrow_constructible_v<tuple, const _Other&...>) // strengthened
: tuple(_Unpack_tuple_t{}, _Right) {}
template <class... _Other, enable_if_t<conjunction_v<_STD _Tuple_constructible_val<tuple, _Other...>,
_STD _Tuple_convert_val<tuple, tuple<_Other...>, _Other...>>,
int> = 0>
constexpr explicit(_Tuple_conditional_explicit_v<tuple, _Other...>)
tuple(tuple<_Other...>&& _Right) noexcept(_Tuple_nothrow_constructible_v<tuple, _Other...>) // strengthened
: tuple(_Unpack_tuple_t{}, _STD move(_Right)) {}
如果传入参数的类型和tuple的类型完全一致,那么直接让编译器合成一个就好了。如果不一致,则要进行参数检查,首先_Tuple_constructible_val
判断传入tuple的每个参数是否可以拿来构造当前tuple的每个参数,它的作用和前面提到的_Tuple_constructible_v
本质上是一样的,只不过由于这里用在了conjunction_v
上,需要传入一个struct,struct的value表示要判断的内容:
template <class _Dest, class... _Srcs>
struct _Tuple_constructible_val : bool_constant<_Tuple_constructible_v<_Dest, _Srcs...>> {};
接下来,这里还有一个_Tuple_convert_val
。它的定义看上去蛮奇怪的:
// Constrain tuple's converting constructors
template <class _Myself, class _OtherTuple, class... _Other>
struct _Tuple_convert_val : true_type {};
template <class _This, class _OtherTuple, class _Uty>
struct _Tuple_convert_val<tuple<_This>, _OtherTuple, _Uty>
: bool_constant<!disjunction_v<is_same<_This, _Uty>, is_constructible<_This, _OtherTuple>,
is_convertible<_OtherTuple, _This>>> {};
如果tuple的元素个数不止一个,那结果直接就是true,而只有一个元素时,需要进行一系列的判断,首先两个tuple的类型不能相同,然后,传入的tuple参数不能直接构造tuple的参数类型,也不能直接convert到tuple的参数类型。只要有一个条件不满足,这个构造函数就非法了。字面意义上解释非常拗口,不如来看一个例子:
#include <iostream>
#include <utility>
#include <tuple>
#include <type_traits>
using namespace std;
class A
{
};
class B
{
public:
B(const A&) { cout << "B(const A&)" << endl; }
B(const tuple<A>&) { cout << "B(const tuple<A>&)" << endl; }
};
int main()
{
tuple<A> t1;
tuple<B> t2(t1);
cout << _Tuple_convert_val<tuple<B>, tuple<A>, A>::value << endl;
tuple<A, A> t3;
tuple<B, B> t4(t3);
cout << _Tuple_convert_val<tuple<B, B>, tuple<A, A>, A, A>::value << endl;
return 0;
}
例子中,B类包含一个const tuple<A>&
的构造函数。换句话说,tuple<A>
类型可以直接转换为B类型。此时拿tuple<A>
类型去构造tuple<B>
类型,那么_Tuple_convert_val
的值应该为false,也就是拷贝构造函数和移动构造函数不再合法。但是我们实际运行这段代码,却发现编译不会报错,输出的结果如下:
>test.exe
B(const tuple<A>&)
0
B(const A&)
B(const A&)
1
_Tuple_convert_val
的值的确为false,那为什么编译会通过呢?我们发现,B(const tuple<A>&)
这个构造函数被调用了。也就是说,tuple<A>
类型先通过这个构造函数,转换到了B类型,然后使用这个B类型完成了tuple<B>
类型的构造。
拷贝构造函数和移动构造函数最终调用的是带有_Unpack_tuple_t
这个tag的构造函数,这个tag指示tuple要把传入的tuple参数进行unpack,然后再进行构造:
template <class _Tag, class _Tpl, enable_if_t<is_same_v<_Tag, _STD _Unpack_tuple_t>, int> = 0>
constexpr tuple(_Tag, _Tpl&& _Right)
: tuple(_Unpack_tuple_t{}, _STD forward<_Tpl>(_Right),
make_index_sequence<tuple_size_v<remove_reference_t<_Tpl>>>{}) {}
这个tag跟着构造函数继续传了下去,传给了更加通用的(可以指定具体unpack tuple的哪些元素)构造函数:
template <class _This, class... _Rest>
template <class _Tag, class _Tpl, size_t... _Indices, enable_if_t<is_same_v<_Tag, _STD _Unpack_tuple_t>, int> /* = 0 */>
constexpr tuple<_This, _Rest...>::tuple(_Tag, _Tpl&& _Right, index_sequence<_Indices...>)
: tuple(_Exact_args_t{}, _STD get<_Indices>(_STD forward<_Tpl>(_Right))...) {}
这个更加通用的构造函数最终内部调用回_Exact_args_t
版本的构造函数。
tuple也接受pair类型的参数进行构造,对于pair类型,就是unpack出它的first和second,作为tuple的两个元素。例如:
#include <iostream>
#include <utility>
#include <tuple>
#include <type_traits>
using namespace std;
int main()
{
pair<int, int> p1(1, 2);
tuple<int, int> t1(p1);
cout << get<0>(t1) << " " << get<1>(t1) << endl; // 1 2
return 0;
}
tuple还支持带有自定义allocator的构造函数,这样传入的参数类型就可以使用这个allocator进行构造:
#include <iostream>
#include <utility>
#include <tuple>
#include <type_traits>
#include <vector>
using namespace std;
int main()
{
using my_alloc = allocator<int>;
vector<int, my_alloc> v{5, 1, my_alloc{}};
tuple<int, vector<int, my_alloc>, double> t
{allocator_arg, my_alloc{}, 42, v, -3.14};
return 0;
}
这里tuple的第二个元素,vector在构造时,除了使用传入的参数v,还用到了自定义的allocator,my_alloc。这里用到的tuple构造函数定义如下:
template <class _Alloc, class _This2 = _This,
enable_if_t<_Tuple_constructible_v<tuple, const _This2&, const _Rest&...>, int> = 0>
_CONSTEXPR20 explicit(_Tuple_conditional_explicit_v<tuple, const _This2&, const _Rest&...>)
tuple(allocator_arg_t, const _Alloc& _Al, const _This& _This_arg, const _Rest&... _Rest_arg)
: tuple(_Alloc_exact_args_t{}, _Al, _This_arg, _Rest_arg...) {}
可以看到实际调用的是带有_Alloc_exact_args_t
这个tag的构造函数:
template <class _Tag, class _Alloc, class _This2, class... _Rest2,
enable_if_t<is_same_v<_Tag, _STD _Alloc_exact_args_t>, int> = 0>
constexpr tuple(_Tag, const _Alloc& _Al, _This2&& _This_arg, _Rest2&&... _Rest_arg)
: _Mybase(_Alloc_exact_args_t{}, _Al, _STD forward<_Rest2>(_Rest_arg)...),
_Myfirst(_Al, allocator_arg, _STD forward<_This2>(_This_arg)) {}
这里其实就是递归了,每次只对一个参数进行构造,这也是使用继承来实现tuple的妙处。不过,我们传入的参数,有的是支持allocator的,例如vector,有的又是不支持的,例如int,double,这里是如何做到区分的呢?
答案就在这个_Myfirst
上,它不是简单地对应第一个参数类型的value,而是一个_Tuple_val<_This>
类型的。_Tuple_val
是一个包装类:
template <class _Ty>
struct _Tuple_val { // stores each value in a tuple
constexpr _Tuple_val() : _Val() {}
template <class _Other>
constexpr _Tuple_val(_Other&& _Arg) : _Val(_STD forward<_Other>(_Arg)) {}
template <class _Alloc, class... _Other, enable_if_t<!uses_allocator_v<_Ty, _Alloc>, int> = 0>
constexpr _Tuple_val(const _Alloc&, allocator_arg_t, _Other&&... _Arg) : _Val(_STD forward<_Other>(_Arg)...) {}
template <class _Alloc, class... _Other,
enable_if_t<conjunction_v<_STD uses_allocator<_Ty, _Alloc>,
_STD is_constructible<_Ty, _STD allocator_arg_t, const _Alloc&, _Other...>>,
int> = 0>
constexpr _Tuple_val(const _Alloc& _Al, allocator_arg_t, _Other&&... _Arg)
: _Val(allocator_arg, _Al, _STD forward<_Other>(_Arg)...) {}
template <class _Alloc, class... _Other,
enable_if_t<conjunction_v<_STD uses_allocator<_Ty, _Alloc>,
_STD negation<_STD is_constructible<_Ty, _STD allocator_arg_t, const _Alloc&, _Other...>>>,
int> = 0>
constexpr _Tuple_val(const _Alloc& _Al, allocator_arg_t, _Other&&... _Arg)
: _Val(_STD forward<_Other>(_Arg)..., _Al) {}
_Ty _Val;
};
从代码中得知,当参数类型不支持传入的allocator类型时,构造_Val
就会使用不带allocator的版本,只有参数类型支持allocator时,带allocator版本的构造函数才会变得合法。
tuple也提供了一大堆赋值操作符的重载函数,不过内部实现基本一致,比如
template <class _Myself = tuple, class _This2 = _This,
enable_if_t<conjunction_v<_STD _Is_copy_assignable_no_precondition_check<_This2>,
_STD _Is_copy_assignable_no_precondition_check<_Rest>...>,
int> = 0>
_CONSTEXPR20 tuple& operator=(_Identity_t<const _Myself&> _Right) noexcept(
conjunction_v<is_nothrow_copy_assignable<_This2>, is_nothrow_copy_assignable<_Rest>...>) /* strengthened */ {
_Myfirst._Val = _Right._Myfirst._Val;
_Get_rest() = _Right._Get_rest();
return *this;
}
一样也是递归赋值,自身只处理_Myfirst
的赋值,其余的就交给基类处理了。
由于pair只有两个元素,STL就直接暴露了first和second来访问它们。而tuple里的元素数量是不固定的,就得使用get函数来访问指定index的元素,STL对左值引用,右值引用,常量左值引用,常量右值引用各实现了一份get:
template <size_t _Index, class... _Types>
friend constexpr tuple_element_t<_Index, tuple<_Types...>>& get(tuple<_Types...>& _Tuple) noexcept;
template <size_t _Index, class... _Types>
friend constexpr const tuple_element_t<_Index, tuple<_Types...>>& get(const tuple<_Types...>& _Tuple) noexcept;
template <size_t _Index, class... _Types>
friend constexpr tuple_element_t<_Index, tuple<_Types...>>&& get(tuple<_Types...>&& _Tuple) noexcept;
template <size_t _Index, class... _Types>
friend constexpr const tuple_element_t<_Index, tuple<_Types...>>&& get(const tuple<_Types...>&& _Tuple) noexcept;
常量右值引用算是非常少见了,对一个常量的左值进行move操作,返回的就是常量右值引用,比如下面这个例子:
#include <iostream>
#include <utility>
#include <tuple>
#include <type_traits>
using namespace std;
void f(int&& x) { cout << "rvalue" << endl; }
void f(const int&& x) { cout << "const rvalue" << endl; }
int main()
{
const int x = 1;
f(1); // rvalue
f(move(x)); // const rvalue
return 0;
}
get函数的实现同样也十分地巧妙,利用模板找到从指定index开始的tuple基类,因为index之前的元素都在这个基类的子类里面,那么基类的_Myfirst
就是要找的元素。接下来只要做下强制类型转换就结束了。
template <class _This, class... _Rest>
struct _MSVC_KNOWN_SEMANTICS tuple_element<0, tuple<_This, _Rest...>> { // select first element
using type = _This;
// MSVC assumes the meaning of _Ttype; remove or rename, but do not change semantics
using _Ttype = tuple<_This, _Rest...>;
};
template <size_t _Index, class _This, class... _Rest>
struct _MSVC_KNOWN_SEMANTICS tuple_element<_Index, tuple<_This, _Rest...>>
: tuple_element<_Index - 1, tuple<_Rest...>> {}; // recursive tuple_element definition
_EXPORT_STD template <size_t _Index, class _Tuple>
using tuple_element_t = typename tuple_element<_Index, _Tuple>::type;
_EXPORT_STD template <size_t _Index, class... _Types>
_NODISCARD constexpr tuple_element_t<_Index, tuple<_Types...>>& get(tuple<_Types...>& _Tuple) noexcept {
using _Ttype = typename tuple_element<_Index, tuple<_Types...>>::_Ttype;
return static_cast<_Ttype&>(_Tuple)._Myfirst._Val;
}
C++14之后,tuple还提供了根据参数类型来寻找tuple元素的get函数:
template <class _Ty, class... _Types>
friend constexpr _Ty& get(tuple<_Types...>& _Tuple) noexcept;
template <class _Ty, class... _Types>
friend constexpr const _Ty& get(const tuple<_Types...>& _Tuple) noexcept;
template <class _Ty, class... _Types>
friend constexpr _Ty&& get(tuple<_Types...>&& _Tuple) noexcept;
template <class _Ty, class... _Types>
friend constexpr const _Ty&& get(const tuple<_Types...>&& _Tuple) noexcept;
get函数只能使用tuple中唯一的参数类型,如果不止一个参数的类型相同,或者没有这样的参数类型,编译时就会报错:
#include <iostream>
#include <utility>
#include <tuple>
#include <type_traits>
using namespace std;
int main()
{
tuple<int, int, double> t(1, 2, 3.0);
cout << get<double>(t) << endl; // ok
cout << get<int>(t) << endl; // error
cout << get<float>(t) << endl; // error
return 0;
}
背后的实现也类似,找到第一个符合的元素类型的tuple基类,强制类型转换取出_Myfirst
即可。只不过,需要对参数类型进行编译期检查,保证tuple中符合的参数类型只有一个。
template <class _This, class... _Rest>
struct _Tuple_element<_This, tuple<_This, _Rest...>> { // select first element
static_assert(!_Is_any_of_v<_This, _Rest...>, "duplicate type T in get<T>(tuple)");
using _Ttype = tuple<_This, _Rest...>;
};
template <class _Ty, class _This, class... _Rest>
struct _Tuple_element<_Ty, tuple<_This, _Rest...>> { // recursive _Tuple_element definition
using _Ttype = typename _Tuple_element<_Ty, tuple<_Rest...>>::_Ttype;
};
_EXPORT_STD template <class _Ty, class... _Types>
_NODISCARD constexpr _Ty& get(tuple<_Types...>& _Tuple) noexcept {
using _Ttype = typename _Tuple_element<_Ty, tuple<_Types...>>::_Ttype;
return static_cast<_Ttype&>(_Tuple)._Myfirst._Val;
}
C++17之后开始支持类模板参数推导(Class Template Argument Deduction),tuple这里也做了推导指引:
template <class... _Types>
tuple(_Types...) -> tuple<_Types...>;
template <class _Ty1, class _Ty2>
tuple(pair<_Ty1, _Ty2>) -> tuple<_Ty1, _Ty2>;
template <class _Alloc, class... _Types>
tuple(allocator_arg_t, _Alloc, _Types...) -> tuple<_Types...>;
template <class _Alloc, class _Ty1, class _Ty2>
tuple(allocator_arg_t, _Alloc, pair<_Ty1, _Ty2>) -> tuple<_Ty1, _Ty2>;
template <class _Alloc, class... _Types>
tuple(allocator_arg_t, _Alloc, tuple<_Types...>) -> tuple<_Types...>;
这样写代码的时候就不必显式声明tuple的参数类型了:
#include <iostream>
#include <utility>
#include <tuple>
#include <type_traits>
#include <vector>
using namespace std;
int main()
{
tuple t1{1, 2, 3, 4, 5};
tuple t2{pair<int, int>(1, 2)};
using my_alloc = allocator<int>;
vector<int, my_alloc> v{5, 1, my_alloc{}};
tuple t3{allocator_arg, my_alloc{}, 42, v, -3.14};
tuple t4{allocator_arg, my_alloc{}, pair<int, int>(1, 2)};
tuple t5{allocator_arg, my_alloc{}, t1};
return 0;
}
STL还提供了两个便捷的函数创建tuple,一个是make_tuple,一个是tie,两者的区别在于返回的tuple类型有差异,一个返回参数类型为值类型的tuple(除非传入的参数是reference_wrapper),一个返回的是引用类型的tuple。换言之一个修改tuple自身的值不会影响传入的参数,另一个传入参数也被修改了。
_EXPORT_STD template <class... _Types>
_NODISCARD constexpr tuple<_Unrefwrap_t<_Types>...> make_tuple(_Types&&... _Args) { // make tuple from elements
using _Ttype = tuple<_Unrefwrap_t<_Types>...>;
return _Ttype(_STD forward<_Types>(_Args)...);
}
_EXPORT_STD template <class... _Types>
_NODISCARD constexpr tuple<_Types&...> tie(_Types&... _Args) noexcept { // make tuple from elements
using _Ttype = tuple<_Types&...>;
return _Ttype(_Args...);
}
配合例子一起食用更佳:
#include <iostream>
#include <utility>
#include <tuple>
#include <type_traits>
using namespace std;
int main()
{
int x = 1, y = 2, z = 3;
auto t1 = make_tuple(x, y, z);
get<0>(t1) = 10;
get<1>(t1) = 20;
get<2>(t1) = 30;
cout << x << " " << y << " " << z << endl; // 1 2 3
auto t2 = tie(x, y, z);
get<0>(t2) = 100;
get<1>(t2) = 200;
get<2>(t2) = 300;
cout << x << " " << y << " " << z << endl; // 100 200 300
auto t3 = make_tuple(ref(x), ref(y), ref(z));
get<0>(t3) = 1000;
get<1>(t3) = 2000;
get<2>(t3) = 3000;
cout << x << " " << y << " " << z << endl; // 1000 2000 3000
return 0;
}
最后,STL中没有提供打印输出tuple的函数,如果直接使用标准输出操作符<<,编译会报错:
#include <iostream>
#include <utility>
#include <tuple>
#include <type_traits>
using namespace std;
int main()
{
int x = 1, y = 2, z = 3;
auto t = make_tuple(x, y, z);
cout << t << endl; // error
return 0;
}
当然,我们可以自己手写一个,实现tuple的输出。但是这里很容易想当然地写出一个错误的版本:
template<typename... Args>
ostream& operator<<(ostream& os, tuple<Args...> t)
{
os << "(";
os << get<0>(t);
for (int i = 1; i < tuple_size<tuple<Args...>>::value; ++i)
os << ", " << get<i>(t); // error
os << ")";
return os;
}
很不幸编译会报错,原因是tuple设计之初就不是寻常的容器,它本身是不允许迭代元素的,意味着必须要在编译期间就得确定元素的索引值,运行期间传入一个动态的索引值i是不被允许的。
那么要怎么才能输出一个tuple呢?我们可以参考STL的思路,用template metaprogramming实现:
#include <iostream>
#include <utility>
#include <tuple>
#include <type_traits>
using namespace std;
template<int I, int N, typename... Args>
struct Print_Tuple
{
static void print(ostream& os, const tuple<Args...>& t)
{
os << get<I>(t) << (I + 1 == N ? "" : ", ");
Print_Tuple<I + 1, N, Args...>::print(os, t);
}
};
template<int N, typename... Args>
struct Print_Tuple<N, N, Args...>
{
static void print(ostream& os, const tuple<Args...>& t)
{
}
};
template<typename... Args>
ostream& operator<<(ostream& os, tuple<Args...> t)
{
os << "(";
Print_Tuple<0, sizeof...(Args), Args...>::print(os, t);
os << ")";
return os;
}
int main()
{
int x = 1, y = 2, z = 3;
auto t = make_tuple(x, y, z);
cout << t << endl; // (1, 2, 3)
return 0;
}