1. if constexpr (C++17)

if constexpr 是 C++17 引入的一种编译期条件语句。它与普通的 if 语句不同,普通的 if 语句是在运行时评估条件,并根据结果执行相应的代码块。而 if constexpr 的条件必须是一个常量表达式,并且在编译时进行评估。

核心思想:
if constexpr 的主要目的是在编译时根据条件选择性地编译代码路径。这意味着不满足条件的 if constexpr 分支在编译时会被完全丢弃,不会参与到最终的可执行文件中。这对于模板编程来说非常有用,因为它允许我们根据模板参数的特性(例如,是否是某个类型,是否支持某个操作)来生成不同的代码。

语法:
if constexpr (constant_expression)

为什么需要 if constexpr
在 C++17 之前,为了实现编译期条件选择,我们通常需要使用模板特化、SFINAE (Substitution Failure Is Not An Error) 或标签分发等技术,这些方法往往比较复杂和冗长。if constexpr 提供了一种更简洁、更直观的方式来完成同样的事情。

使用场景:

  1. 根据类型特性选择实现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <iostream>
    #include <type_traits> // 用于 std::is_integral, std::is_floating_point

    template <typename T>
    void process(T value) {
    if constexpr (std::is_integral_v<T>) { // 如果 T 是整数类型
    std::cout << "Processing integral: " << value * 2 << std::endl;
    } else if constexpr (std::is_floating_point_v<T>) { // 如果 T 是浮点类型
    std::cout << "Processing floating point: " << value * 1.5 << std::endl;
    } else { // 其他类型
    std::cout << "Processing unknown type: " << value << std::endl;
    }
    }

    int main() {
    process(10); // 调用第一个分支
    process(3.14); // 调用第二个分支
    process("hello"); // 调用第三个分支
    return 0;
    }

    在这个例子中,当 Tint 时,只有 std::cout << "Processing integral: " << value * 2 << std::endl; 这一行会被编译到 process<int> 的实现中。其他分支的代码会被编译器完全忽略。

  2. 避免编译错误:
    当某些操作只对特定类型有效时,if constexpr 可以避免在不适用的类型上尝试编译这些操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #include <iostream>
    #include <string>
    #include <vector>

    template <typename T>
    void print_size(const T& obj) {
    if constexpr (std::is_arithmetic_v<T>) { // 如果是算术类型
    std::cout << "Arithmetic type, value: " << obj << std::endl;
    } else if constexpr (requires { obj.size(); }) { // C++20 concept-like syntax for checking if .size() exists
    // C++17 之前可能用 SFINAE 或 std::void_t 来检查
    std::cout << "Type with .size() method, size: " << obj.size() << std::endl;
    } else {
    std::cout << "Type without .size() method, cannot determine size." << std::endl;
    }
    }

    int main() {
    print_size(10);
    print_size(std::string("hello"));
    print_size(std::vector<int>{1, 2, 3});
    print_size(true); // bool 也是算术类型
    // print_size(nullptr); // 会走到 else 分支
    return 0;
    }

    在这个例子中,如果 Tint,编译器不会尝试编译 obj.size(),从而避免了编译错误。

与普通 if 的区别:

  • 编译期 vs 运行时: if constexpr 在编译期决定执行哪个分支,不执行的分支不会被编译;普通 if 在运行时决定。
  • 条件要求: if constexpr 的条件必须是常量表达式;普通 if 的条件可以是任何可转换为 bool 的表达式。
  • 代码生成: if constexpr 导致不同的代码生成路径;普通 if 生成包含所有分支的运行时条件跳转代码。
  • 适用场景: if constexpr 主要用于模板元编程和泛型编程,根据类型特性生成特化的代码;普通 if 用于运行时逻辑控制。

2. decltype (C++11)

decltype 是 C++11 引入的一个关键字,用于推断表达式的类型。它在编译时评估其操作数,并返回该表达式的类型,而不实际执行该表达式。

核心思想:
decltype 的主要用途是获取一个表达式的精确类型,包括其引用性(lvalue/rvalue reference)和常量性(const/volatile)。这在泛型编程中非常有用,例如在编写一个函数模板时,我们可能需要根据其参数的类型来推断返回类型。

语法:
decltype(expression)

使用场景:

  1. 推断函数返回类型 (Trailing Return Type):
    在 C++11 之前,如果函数的返回类型依赖于其参数的类型,特别是当参数是模板类型时,很难直接指定返回类型。decltype 与 C++11 的尾置返回类型语法结合,解决了这个问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    template <typename T, typename U>
    auto add(T t, U u) -> decltype(t + u) { // 返回 t + u 的类型
    return t + u;
    }

    int main() {
    int a = 5;
    double b = 3.14;
    auto result = add(a, b); // result 的类型是 double
    std::cout << "result: " << result << ", type: " << typeid(result).name() << std::endl;
    return 0;
    }
  2. 获取变量的精确类型:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    int x = 10;
    const int& y = x;
    decltype(x) a = 20; // a 是 int
    decltype(y) b = x; // b 是 const int&
    decltype(std::move(x)) c = 30; // c 是 int&& (xvalue)
    decltype((x)) d = x; // d 是 int& (因为 (x) 是一个左值表达式)

    std::cout << "type of a: " << typeid(a).name() << std::endl;
    std::cout << "type of b: " << typeid(b).name() << std::endl;
    std::cout << "type of c: " << typeid(c).name() << std::endl;
    std::cout << "type of d: " << typeid(d).name() << std::endl;

    注意 decltype((x))decltype(x) 的区别:

    • decltype(variable):如果操作数是一个没有括号的变量名,decltype 返回该变量的声明类型。
    • decltype((expression)):如果操作数是任何其他表达式(包括带括号的变量名),decltype 返回该表达式的值类别(value category)对应的类型。如果表达式是左值,则返回 T&;如果表达式是右值,则返回 T&&
  3. 在模板中获取成员类型或嵌套类型:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <vector>
    #include <map>

    template <typename Container>
    void print_first_element_type(const Container& c) {
    // 获取容器中元素的类型
    using ElementType = decltype(*c.begin());
    std::cout << "Element type: " << typeid(ElementType).name() << std::endl;
    }

    int main() {
    std::vector<int> v = {1, 2, 3};
    print_first_element_type(v); // Element type: i (int)

    std::map<std::string, double> m;
    print_first_element_type(m); // Element type: St4pairIKSt6stringdE (std::pair<const std::string, double>)
    return 0;
    }

decltype 的推断规则:

decltype(e) 的推断规则比较复杂,但可以简化为:

  1. 如果 e 是一个没有括号的变量名decltype(e) 的类型就是该变量的声明类型。
    1
    2
    int i = 0;
    decltype(i) var1; // var1 是 int
  2. 如果 e 是一个右值表达式decltype(e) 的类型是 T
    1
    2
    decltype(10) var2;       // var2 是 int
    decltype(i + 10) var3; // var3 是 int
  3. 如果 e 是一个左值表达式decltype(e) 的类型是 T&
    1
    2
    3
    int i = 0;
    decltype((i)) var4 = i; // var4 是 int&
    decltype(++i) var5 = i; // ++i 是左值表达式,var5 是 int&

总结对比

特性 if constexpr (C++17) decltype (C++11)
功能 编译期条件分支,选择性编译代码 编译期推断表达式的类型
作用时机 编译时 编译时
操作对象 常量表达式作为条件 任何表达式
主要用途 模板元编程,根据类型特性生成不同代码,避免编译错误 泛型编程,推断函数返回类型,获取精确类型,实现类型依赖
结果 编译后的代码中只包含满足条件的分支 返回一个类型
与普通 if 编译期选择,不满足条件的分支不编译 无直接关系

共同点:

  • 两者都是在编译时工作的。
  • 两者都极大地增强了 C++ 的泛型编程能力,使得编写更灵活、更通用的模板代码成为可能。

在现代 C++ 中,if constexprdecltype 常常结合使用,以实现更高级的模板编程技巧。例如,你可以使用 decltype 来推断一个表达式的类型,然后使用 if constexpr 根据这个类型来选择不同的实现路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <type_traits>
#include <string>

template <typename T>
void inspect_and_print(T val) {
// 使用 decltype 获取表达式的类型
using ExprType = decltype(val + 1);

if constexpr (std::is_integral_v<ExprType>) {
std::cout << "Expression (val + 1) results in an integral type: " << val + 1 << std::endl;
} else if constexpr (std::is_floating_point_v<ExprType>) {
std::cout << "Expression (val + 1) results in a floating point type: " << val + 1 << std::endl;
} else {
std::cout << "Expression (val + 1) results in an unknown type." << std::endl;
}
}

int main() {
inspect_and_print(5); // ExprType is int
inspect_and_print(3.5f); // ExprType is float
// inspect_and_print(std::string("hello")); // 编译错误,string + int 不合法
return 0;
}

这个例子展示了如何结合使用 decltype 推断 val + 1 的类型,然后使用 if constexpr 根据推断出的类型选择不同的打印行为。