在 cppreference 站点上检查
std::tuple
的构造函数时,我遇到了下面定义的移动构造函数,该构造函数在 C++23 中可用。
template< class... UTypes >
constexpr tuple( const tuple<UTypes...>&& other )
这里,
constexpr
表示如果可能的话,可以在编译时处理构造函数。然而,参数是const
,这不是让移动语义变得毫无意义,还是我完全误解了?
std::tuple
(或std::pair
)是标准库中的基本构建块之一,我们可以使用它来保存值、引用甚至它们的混合的元组。也就是说,tuple
可以表现得像值类型或引用类型。如果我们只关心值元组,那么您的感觉有些正确: const tuple<Us...>&&
重载基本上没有意义。然而,当我们引入引用元组时,事情会变得有点复杂。
正如
int&
可以绑定到 int
类型的左值一样,很自然地假设 tuple<int&>
可以从 tuple<int>
类型的左值(即 tuple<int>&
)初始化。此外,由于引用被烘焙到 tuple
的类型中,因此从 tuple<int&>
的左值(即 tuple<int&>
)初始化 tuple<int&>&
是否合理?答案是肯定的。由于引用折叠,get<0>
的 tuple<int&>&
的返回类型为 int&
。下表显示了一些示例:
|
|
---|---|
[ ]
|
|
[ ]
|
|
[ ]
|
|
[ ]
|
|
请注意,应用于
const
的顶级 tuple
不会影响返回类型,因为引用本身已经类似于 const
(一个引用只能绑定到一个对象)。
P2214R2中给出的引用元组和值元组之间转换的一个激励性示例是views::zip
。
zip
使用
tuple
(或
pair
)来包装多个
ranges::range_value_t
(通常为
T
)和
ranges::range_reference_t
(通常为
T&
)底层视图。例如,两个
zip
中的
std::vector<int>
的
tuple<int, int>
为
range_value_t
,其
tuple<int&, int&>
为
range_reference_t
(请注意,在这种情况下,
tuple<int, int>&
不适合,因为我们有
int
的两个视图)而不是
tuple<int, int>
的一种视图)。我们真的希望这两种类型能够对
T
和
T&
进行建模。因此,P2214R2建议添加以下重载:
template<class... Us>
tuple(tuple<Us...>&); // (1)
template<class... Us>
tuple(const tuple<Us...>&&); // (2)
添加重载 (2) 只是为了一致性和完整性。
P2165R4建议添加以下通用构造函数:
template<tuple-like U>
tuple(U&&);
那么我们可以摆脱所有那些冗余的单tuple
/
pair
构造函数吗?出于向后兼容性的原因,可能不会。我们就保留它们吧,因为它们在所有情况下看起来都是“无害的”。不要惊讶
tuple
现在有 12 个构造函数(不包括复制和移动构造函数)。然而,确实存在这些冗余构造函数是不受欢迎的并且可能有害的情况。考虑以下代码(Godbolt):
using T = std::tuple<int &>;
using U = std::tuple<int &&>;
T t = U(42);
上面的代码调用 const tuple<int&&>&
重载并立即创建一个悬空引用。在这种情况下,
tuple<int&&>&&
重载不可行,因为
int&&
无法绑定到
int&
。然后我们回到
const tuple<int&&>&
,如上所示,它会产生一个
int&
。如果没有这些重载,一个通用构造函数就可以正确拒绝上述代码。