前言

今天我们来谈谈条件编译。条件编译是非常有意思和方便的东西,它类似于函数重载,对于不同的类型使用不同的算法。这是传统的一个函数重载的示例,我们姑且将它算作条件编译:

1
2
3
4
5
6
7
8
9
10
int to_int(int i) { return i; }
int to_int(double f) { return f; }
int to_int(const char *s) { return atoi(s); }
int to_int(const std::string &s) { return std::stoi(s); }
int to_int(const std::vector<char> &v) {
int ret = 0;
for (char x : v)
ret = ret << 8 | static_cast<unsigned int>(ret);
return ret;
}

但是,这种传统的条件编译存在两个问题:

  • 不断有类型被定义,而函数重载可以接受的类型是有限的,我们必须要为其新增新的重载。
  • 在模板元编程中,模板元无条件接受一个类型,但我可能需要对不同类型进行不同处理,我们可以使用模板元特化,但它将存在与函数重载相同的问题,且更加严重。(主要原因)

于是我们将介绍 std::enable_if(C++ 11) / if constexpr(C++ 17) / concept(C++ 20) 用于处理这个问题。在后面的示例中,我们将用不同 C++ 版本来实现释放内存的操作,注意我们释放的内存可能是指针的,也可能是智能指针的。

std::enable_if(C++ 11)

C++ 11 开始支持编译时类型判断,这些函数被包含在 type_traits 标准库中。在 C++ 11 中引入的 enable_if 则允许我们启用或禁用模板,这结合 type_traits 非常方便。

1
2
3
4
5
6
7
8
9
10
11
// std::enable_if<bool, typename>::type

// 如果 std::is_pointer<T>::value 返回 true, 则启用该模板, 并返回 void
template <typename T>
typename std::enable_if<std::is_pointer<T>::value, void>::type
destroy(T &p) { delete p; }

// 如果 is_smart_ptr<T>::value 返回 true, 则启用该模板, 并返回 void
template <typename T>
typename std::enable_if<is_smart_ptr<T>::value, void>::type
destroy(T &p) { p.reset(); }

额,但是如果 is_smart_ptr 是被标准库支持的那么很方便,但是可能需要我们自己来支持一下 is_smart_ptr,这里有两种解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 模板特化的解决方案

// 其他
template <typename T>
class is_smart_ptr : public std::false_type {};

// 模板特化: shared_ptr<T>
template <typename T>
class is_smart_ptr<std::shared_ptr<T>> : public std::true_type {};

// 模板特化: unique_ptr<T>
template <typename T>
class is_smart_ptr<std::unique_ptr<T>> : public std::true_type {};

// 模板特化: unqiue_ptr<T, D>
template <typename T, typename D>
class is_smart_ptr<std::unique_ptr<T, D>> : public std::true_type {};

// 模板特化: weak_ptr<T>
template <typename T>
class is_smart_ptr<std::weak_ptr<T>> : public std::true_type {};
1
2
3
4
5
6
7
8
9
10
11
12
13
// SFINAE 探测 ::element_type 成员

// 触发 SFINAE 执行默认方法
template <typename T, typename = void>
class is_smart_ptr : public std::false_type {};

// 编译通过执行特化方法
template <typename T>
class is_smart_ptr<T, decltype(typename T::element_type(), void())> : public std::true_type {};

// 在 C++ 17 引入 std::void_t 为 SFINAE 提供了便利:
// template <typename T>
// class is_smart_ptr<T, std::void_t<typename T::element_type>> : public std::true_type {};

变量模板(C++ 14)

在上一个示例中,我们注意到一个问题:enable_if 每次都要 ::type,而 is_shared_ptr 每次都要 ::value。其实从 C++ 14 开始支持了 xxx_t / xxx_v 系列模板变量,其实际源于 C++ 14 支持了变量模板,提供了一些便利。

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T>
constexpr bool is_pointer_v = std::is_pointer<T>::value; // C++ 17 引入, C++ 14 要自己定义

template <typename T>
constexpr bool is_smart_ptr_v = is_smart_ptr<T>::value;

template <typename T>
typename std::enable_if_t<is_pointer_v<T>, void>
destroy(T &p) { delete p; }

template <typename T>
typename std::enable_if_t<is_smart_ptr_v<T>, void>
destroy(T &p) { p.reset(); }

if constexpr(C++ 17)

但是 C++ 11 提供的 enable_if 多少有点奇怪,我们必须写两个模板。而 C++ 17 开始支持的 if constexpr 允许你像使用条件控制语句一样使用函数,这更符合常规编程思维。当然它也不能完全替代 enable_if,毕竟完全分离具有更高的灵活性。

1
2
3
4
5
6
7
8
9
10
template <typename T>
void destroy(T &p) {
if constexpr (std::is_pointer_v<T>) {
cout << "delete p" << endl;
delete p;
} else {
cout << "p.reset" << endl;
p.reset();
}
}

if constexpr 还有一个优点,就是 enable_if 必须保证是没有二义性的,而 if_constexpr 可以使用 else

concept(C++ 20)

C++ 20concept 是对于 C++ 11enable_if 的进一步规范化。使用 concept 提高了条件模板的复用性和可读性。另外我们几乎不再需要显式触发 SFINAE,这降低了语法难度,而且你还可以得到更友好的 IDE 支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 使用布尔表达式定义约束
template <typename T>
concept Pointer = std::is_pointer_v<T>;

// 使用 requires 定义约束
template <typename T>
concept SmartPointer = requires(T t) {
// requires 的存在不再需要显式触发 SFINAE
typename T::element_type;
t.reset();
};

// 复用约束 Pointer
template <Pointer T>
void destroy(T &p) { delete p; }

// 复用约束 SmartPointer
template <SmartPointer T>
void destroy(T &p) { p.reset(); }