C++17的 if constexpr
是如何简化模板元编程的?它与常规的 if
语句以及#if
预处理指令有何根本不同?
if constexpr
之前的std::enable_if
的复杂性
eg:编写一个函数,能将任意类型的值转为字符串
- 如果传入的是指针,则解引用后再转换
- 如果传入的是普通值,则直接转换
std::enable_if
的C++11/14写法:
c++
#include <string>
#include <type_traits> // for std::is_pointer
// 版本A:当T是指针时,此模板被启用
template<typename T>
typename std::enable_if<std::is_pointer<T>::value, std::string>::type
to_string_advanced(T value) {
std::cout << "Pointer version called: ";
return std::to_string(*value); // 解引用
}
// 版本B:当T不是指针时,此模板被启用
template<typename T>
typename std::enable_if<!std::is_pointer<T>::value, std::string>::type
to_string_advanced(T value) {
std::cout << "Value version called: ";
return std::to_string(value);
}
缺点:
- 代码冗长:逻辑相似的代码被迫拆分到两个完全独立的函数模板中
- 可读性差 :
typename std::enable_if<...>::type
可读性差 - 可读性差 :
typename std::enable_if<...>::type
这种语法非常晦涩难懂
if constexpr 的解决方案:简洁的编译期分支
if constexpr
允许我们将这些分散的逻辑重新整合到一个函数模板中,代码变得极其清晰
c++
#include <string>
#include <type_traits>
#include <iostream>
template<typename T>
std::string to_string_advanced_cpp17(T value) {
// 条件必须是编译期常量表达式
if constexpr (std::is_pointer<T>::value) {
// 如果T是指针类型,编译器只编译这部分代码
std::cout << "Pointer branch compiled: ";
return std::to_string(*value);
} else {
// 如果T不是指针类型,编译器只编译这部分代码
std::cout << "Value branch compiled: ";
return std::to_string(value);
}
}
int main() {
int x = 10;
int* p = &x;
to_string_advanced_cpp17(p); // T = int*, is_pointer为true, 编译if分支
to_string_advanced_cpp17(x); // T = int, is_pointer为false, 编译else分支
}
if constexpr
- 判断在编译期进行
- 编译器会根据判断结果,只编译 其中一个分支(
if
或else
),另一个分支会被完全丢弃 - "丢弃"意味着编译器甚至不会检查 被丢弃分支的语法和语义。例如,在上面的代码中,当传入
int
类型时,if
分支被丢弃,编译器根本不会去看*value
这行代码,因此即使对int
类型进行*value
操作是错误的,也不会产生编译错误
特性 | #if(预处理器) | if(常规语句) | if constexpr(编译期语句) |
---|---|---|---|
工作阶段 | 预处理阶段(编译前) | 运行期 | 编译期 |
判断依据 | 预处理宏(#define) | 变量的运行时值 | 编译期常量和类型信息 |
分支处理 | 只将一个分支的文本交给编译器 | 两个分支都会被编译,运行时根据条件选择一个执行 | 只有一个分支会被编译,另一个分支被完全丢弃 |
类型感知 | 完全不感知 C++ 类型 | 感知类型,但两个分支都必须对当前类型合法 | 感知类型,且只要求被选中的分支对当前类型合法 |