标准库提供了两个关于 std::tuple
的访问函数:std::get
和 std::get_if
,它们都能用下标或者是类型作为参数,要求标准库帮我们遍历并访问一个 std::tuple
的指定类型分量。
虽然两个函数都能访问 std::tuple
的某个分量,但是它们处理落空情况的逻辑却不同。
std::get
找到了我们期待的成员就返回它的引用,找不到时直接触发编译报错;std::get_if
在找到时会返回指向对应成员的指针,找不到就返回空指针。
无论是哪个版本的函数,在使用类型查找成员分量时,如果这个成员的类型在 std::tuple
中重复多次出现,都会导致程序行为不确定(标准中这叫“程序非良构”)。
假设有这么一个场景:我们有一个包含了许多类型、但绝不重复出现的 std::tuple
,我们需要从中拿走某些特定类型的成员分量的值,并且在这个类型不存在时选中我们指定的默认值。
因为已经确保了类型不重复,所以这个场景能够使用 std::get_if
解决;但 std::get_if
返回的是指针,我们需要二次判空再处理,而且指针显得不太好看也不太安全;那么有没有更简洁一点的方案?
有的 bro 有的,我们自己编写一个访问函数就是。
明确一下需求:我们要在一个 std::tuple
中查找指定类型的成员分量,并在找到时取出该成员的值,否则返回我们指定的一个默认值。
这很像 std::optional::value_or
,所以我们的函数就叫 value_or
。
为求简洁,实现这个函数时的遍历策略是取 std::tuple
的下标:
#include <tuple>
#include <type_traits>
template<typename T, std::size_t, typename D, typename Tuple>
constexpr T value_or( Tuple&&, D&&, const std::false_type& );
template<typename T, std::size_t Pos, typename D, typename Tuple>
constexpr typename std::enable_if<std::is_const<typename std::remove_reference<Tuple>::type>::value, T>::type
value_or( Tuple&& params, D&& fallback, const std::true_type& )
noexcept( std::is_nothrow_copy_constructible<T>::value )
{
static_assert( std::is_copy_constructible<T>::value, "T must be copy constructible when Tuple is const" );
return std::get<Pos>( params );
}
template<typename T, std::size_t Pos, typename D, typename Tuple>
constexpr typename std::enable_if<!std::is_const<typename std::remove_reference<Tuple>::type>::value, T>::type
value_or( Tuple&& params, D&& fallback, const std::true_type& )
noexcept( std::is_nothrow_move_constructible<T>::value )
{
static_assert( std::is_move_constructible<T>::value,
"T must be move constructible when Tuple is non-const" );
return std::move( std::get<Pos>( params ) );
}
template<typename T, std::size_t Pos = 0, typename D, typename Tuple>
constexpr
typename std::enable_if<( Pos == std::tuple_size<typename std::remove_reference<Tuple>::type>::value ),
T>::type
value_or( Tuple&&, D&& fallback )
noexcept( std::is_nothrow_move_constructible<T>::value && std::is_nothrow_constructible<T, D>::value )
{
static_assert( std::is_constructible<T, D>::value,
"The type of fallback must be able to be converted to type T" );
return T( std::forward<D>( fallback ) );
}
template<typename T, std::size_t Pos = 0, typename D, typename Tuple>
constexpr
typename std::enable_if<( Pos < std::tuple_size<typename std::remove_reference<Tuple>::type>::value ),
T>::type
value_or( Tuple&& params, D&& fallback )
{
return value_or<T, Pos>(
std::forward<Tuple>( params ),
std::forward<D>( fallback ),
std::is_same<T, typename std::tuple_element<Pos, typename std::decay<Tuple>::type>::type>() );
}
template<typename T, std::size_t Pos, typename D, typename Tuple>
constexpr T value_or( Tuple&& params, D&& fallback, const std::false_type& )
{
return value_or<T, Pos + 1>( std::forward<Tuple>( params ), std::forward<D>( fallback ) );
}
所有工作不涉及运行时副作用,因此每个函数都可以被标注为 constexpr
;编译后会受到常量优化、尾递归优化等优化手段而直接变成访问对应的成员分量,因而是零运行时开销。
因为需求是取出某个分量的值,递归终点处的处理方式就变成了先判断传进来的 std::tuple
是否带有 const
修饰,是就复制一个,否则调用它的移动构造函数。
这个函数可以被修改为传递 lambda 获得默认值的 value_or_else
函数。
代码只需要 C++11,点击这里在线运行。