A.8. std::conditional 条件类型选择

有时我们需要根据一个在编译时已知的条件,在两种类型之间做出选择。考虑以下示例:希望实现对某种类型 T 的两个值之间的比较,对于浮点类型和其它类型(如 int,为简单起见统一归为一类)采取不同的行为方式:

#include <cmath>

// 我们将允许基于所谓的标签类型(用于区分函数
// 签名的空类)在精确表示或浮点表示之间进行比较
struct floating {};
struct exact {};

// 三参数版本不打算直接从用户代码中调用
template <class T>
bool close_enough(T a, T b, exact) {
  return a == b; // 适用于 int, short, bool 等类型
}

template <class T>
bool close_enough(T a, T b, floating) {
  // 注意:这可以受益于更严格的实现,但这与我们的讨论无关
  return std::abs(a - b) < static_cast<T>(0.000001);
}

// 这个两参数版本是用户代码应该调用的
template <class T>
bool close_enough(T a, T b) {
  // 我们的目标:对于 float, double 和 long double 类型,
  // 调用 "floating" 版本;对于其他类型,调用 "exact" 版本
}

我们在 close_enough() 函数中没有为类型 exact 和 floating 的参数命名。这没问题,因为根本不会使用这些对象;这些参数存在的目的是确保两个函数具有不同的签名(以便正确重载)。

在 <type_traits> 头文件中有一个 std::is_floating_point<T> 类型特性,当 T 是浮点类型时其值为 true,否则为 false。如果没有这个特性,也可以自己实现:

// 我们可以如下方式编写 is_floating_point<T>
// (但请使用 std::is_floating_point<T> 代替!)
template <class> struct is_floating_point
  : std::false_type {}; // 一般情况

// 特化版本
template <> struct is_floating_point<float>
  : std::true_type {};
template <> struct is_floating_point<double>
  : std::true_type {};
template <> struct is_floating_point<long double>
  : std::true_type {};

// 为了简化用户代码而提供的便利别名
template <class T>
constexpr bool is_floating_point_v =
  is_floating_point<T>::value;

我们可以使用这个特性来做出判断。但在这里不希望在运行时做判断,因为类型 T 的性质在编译时就已完全可知,没人希望在比较整数时还要付出分支指令的代价!

这时就可以使用 std::conditional<B, T, F> 类型特性来做这样的选择。如果我们自己实现一个,它可能看起来像这样:

// 示例:手动实现的 conditional<B,T,F> 类型特征
// (更推荐使用 <type_traits> 中的 std:: 版本)
// 一般情况(不完整类型)
template <bool, class T, class F> struct conditional;
// 特化版本
template < class T, class F>
  struct conditional<true, T, F> {
    using type = T; // 布尔值为 true,选择类型 T
  };
template < class T, class F>
struct conditional<false, T, F> {
using type = F; // 布尔值为 false,选择类型 F
};

// 为了简化用户代码而提供的便利别名
template <bool B, class T, class F>
using conditional_t = typename conditional<B,T,F>::type;

有了这个特性,我们就可以在编译时根据一个布尔常量的值,在两个类型之间做出选择 —— 这正是我们想要实现的功能:

// ...
// 这个版本将由用户代码调用
template <class T>
bool close_enough(T a, T b) {
  return close_enough(
    a, b, conditional_t<
      is_floating_point_v<T>,
      floating,
      exact
    > {}
  );
}

这段代码的含义是:在调用 close_enough() 函数(即我们面向用户的那个双参数版本)时,第三个参数的类型要么是 floating,要么是 exact,具体选择哪一个取决于 std::is_floating_point_v<T> 这个编译时常量的值。最终结果是:实例化其中一个空类对象,调用相应的算法,然后让函数内联优化掉整个“支架结构”(scaffolding)。