1. 简介

常量表达式的一个挑战是,常量表达式中不允许对普通函数进行函数调用。这意味着我们不能在需要常量表达式的地方使用此类函数调用。constexpr 函数是允许在常量表达式中调用的函数。要使函数成为 constexpr 函数,我们只需在函数的返回类型前面使用 constexpr 关键字即可。示例:

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

constexpr double calcCircumference(double radius) // now a constexpr function
{
constexpr double pi { 3.14159265359 };
return 2.0 * pi * radius;
}

int main()
{
constexpr double circumference { calcCircumference(3.0) }; // now compiles

std::cout << "Our circle has circumference " << circumference << "\n";

return 0;
}

在需要常量表达式的上下文中需要常量表达式在编译时进行计算。如果所需的常量表达式包含 constexpr 函数调用,则该 constexpr 函数调用必须在编译时计算。在上面的示例中,变量 circumference 是 constexpr,因此需要一个常量表达式初始化器。由于 calcCircumference() 是此必需常量表达式的一部分,因此必须在编译时计算 calcCircumference()。在编译时计算函数调用时,编译器将在编译时计算函数调用的返回值,然后将函数调用替换为返回值。

要在编译时计算,还必须满足另外两个条件:

  • 对 constexpr 函数的调用必须具有在编译时已知的参数(例如,是常量表达式)。

  • constexpr 函数中的所有语句和表达式都必须在编译时可计算。

constexpr 函数也可以在运行时进行计算,在这种情况下,它们将返回非 constexpr 结果。例如:

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

constexpr int greater(int x, int y)
{
return (x > y ? x : y);
}

int main()
{
int x{ 5 }; // not constexpr
int y{ 6 }; // not constexpr

std::cout << greater(x, y) << " is greater!\n"; // will be evaluated at runtime

return 0;
}

在此示例中,由于参数 xy 不是常量表达式,因此无法在编译时解析该函数。但是,该函数仍将在运行时解析,以非 constexpr int 形式返回预期值。

2. 非必需常量表达式中的 constexpr 函数调用

任何属于非必需常量表达式的 constexpr 函数调用都可以在编译时或运行时进行计算。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

constexpr int getValue(int x)
{
return x;
}

int main()
{
int x { getValue(5) }; // may evaluate at runtime or compile-time

return 0;
}

在上面的示例中,由于 getValue() 是 constexpr,因此调用 getValue(5) 是一个常量表达式。但是,由于变量 x 不是 constexpr,因此它不需要常量表达式初始值设定项。因此,即使我们提供了常量表达式初始化器,编译器也可以自由选择 getValue(5) 是在运行时还是在编译时计算。

3. 诊断所需常量表达式中的 constexpr 函数

编译器不需要确定 constexpr 函数在编译时是否可计算,直到在编译时实际计算该函数。编写一个 constexpr 函数相当容易,该函数可以成功编译以供运行时使用,但在编译时计算时无法编译。示例:

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

int getValue(int x)
{
return x;
}

// This function can be evaluated at runtime
// When evaluated at compile-time, the function will produce a compilation error
// because the call to getValue(x) cannot be resolved at compile-time
constexpr int foo(int x)
{
if (x < 0) return 0; // needed prior to adoption of P2448R1 in C++23 (see note below)
return getValue(x); // call to non-constexpr function here
}

int main()
{
int x { foo(5) }; // okay: will evaluate at runtime
constexpr int y { foo(5) }; // compile error: foo(5) can't evaluate at compile-time

return 0;
}

在上面的例子中,当 foo(5) 被用作非 constexpr 变量 x 的初始化器时,它将在运行时被计算。这工作正常,并返回值 5。但是,当 foo(5) 用作 constexpr 变量 y 的初始化器时,必须在编译时对其进行评估。此时,编译器将确定对 foo(5) 的调用无法在编译时求值,因为 getValue() 不是 constexpr 函数。因此,在编写 constexpr 函数时,请始终显式测试它在编译时计算时是否编译(通过在需要常量表达式的上下文中调用它,例如在 constexpr 变量的初始化中)。

4. constexpr 函数参数不是 constexpr

constexpr 函数的参数不是隐式的 constexpr,也不能声明为 constexpr,因此它们不能在函数内的常量表达式中使用。示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
consteval int goo(int c)    // c is not constexpr, and cannot be used in constant expressions
{
return c;
}

constexpr int foo(int b) // b is not constexpr, and cannot be used in constant expressions
{
constexpr int b2 { b }; // compile error: constexpr variable requires constant expression initializer

return goo(b); // compile error: consteval function call requires constant expression argument
}

int main()
{
constexpr int a { 5 };

std::cout << foo(a); // okay: constant expression a can be used as argument to constexpr function foo()

return 0;
}

在上面的示例中,函数参数 b 不是 constexpr(即使参数 a 是常量表达式)。这意味着 b 不能在需要常量表达式的任何地方使用,例如 constexpr 变量的初始化器(例如 b2)或对 consteval 函数的调用 (goo(b))。constexpr 函数的参数可以声明为 const,在这种情况下,它们被视为运行时常量。

5. constexpr 函数是隐式内联的

在编译时评估 constexpr 函数时,编译器必须能够在此类函数调用之前看到 constexpr 函数的完整定义(以便它可以自行执行评估)。在这种情况下,前向声明是不够的,即使实际的函数定义稍后出现在同一编译单元中。这意味着在多个文件中调用的 constexpr 函数需要将其定义包含在每个翻译单元中 —— 这通常违反了单一定义规则。为了避免此类问题,constexpr 函数是隐式内联的,这使得它们不受单一定义规则的约束。因此, constexpr 函数通常在头文件中定义。

6. 强制在编译时计算 constexpr 函数

最常见的方法是使用返回值来初始化 constexpr 变量。不幸的是,这需要在我们的程序中引入一个新变量,以确保编译时计算,这很丑陋并且降低了代码的可读性。

C++20 引入了关键字 consteval,用于表示函数必须在编译时计算,否则将导致编译错误。此类函数称为立即函数。示例:

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

consteval int greater(int x, int y) // function is now consteval
{
return (x > y ? x : y);
}

int main()
{
constexpr int g { greater(5, 6) }; // ok: will evaluate at compile-time
std::cout << g << '\n';

std::cout << greater(5, 6) << " is greater!\n"; // ok: will evaluate at compile-time

int x{ 5 }; // not constexpr
std::cout << greater(x, 6) << " is greater!\n"; // error: consteval functions must evaluate at compile-time

return 0;
}

在上述示例中,前两次调用 greater() 将在编译时进行计算。对于调用 greater(x, 6),由于无法在编译时进行计算,因此会导致编译错误。consteval 函数的参数不是 constexpr(即使 consteval 函数只能在编译时计算)。这是为了保持一致性。

consteval 函数的缺点在于它们无法在运行时计算,这使得它们相比 constexpr 函数灵活性较差,而constexpr 函数则既可以在编译时计算,也可以在运行时计算。因此,仍然有必要提供一种便捷的方法,强制 constexpr 函数在编译时进行计算(即使返回值的使用场景并不需要常量表达式),这样我们就可以在可能的情况下进行编译时计算,而在无法编译时计算时则进行运行时计算。consteval 函数提供了一种实现此目的的方法,它使用一个简洁的辅助函数:

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>

// Uses abbreviated function template (C++20) and `auto` return type to make this function work with any type of value
// See 'related content' box below for more info (you don't need to know how these work to use this function)
consteval auto compileTimeEval(auto value)
{
return value;
}

constexpr int greater(int x, int y) // function is constexpr
{
return (x > y ? x : y);
}

int main()
{
std::cout << greater(5, 6) << '\n'; // may or may not execute at compile-time
std::cout << compileTimeEval(greater(5, 6)) << '\n'; // will execute at compile-time

int x { 5 };
std::cout << greater(x, 6) << '\n'; // we can still call the constexpr version at runtime if we wish

return 0;
}

这之所以有效,是因为 consteval 函数需要常量表达式作为参数 – 因此,如果我们使用 constexpr 函数的返回值作为 consteval 函数的参数,则必须在编译时评估 constexpr 函数!consteval 函数只是将此参数作为自己的返回值返回,因此调用者仍然可以使用它。

7. constexpr/consteval 函数可以使用非常量局部变量

在 constexpr 或 consteval 函数中,我们可以使用非 constexpr 的局部变量,并且可以更改这些变量的值。示例:

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

consteval int doSomething(int x, int y) // function is consteval
{
x = x + 2; // we can modify the value of non-const function parameters

int z { x + y }; // we can instantiate non-const local variables
if (x > y)
z = z - 1; // and then modify their values

return z;
}

int main()
{
constexpr int g { doSomething(5, 6) };
std::cout << g << '\n';

return 0;
}

在编译时计算此类函数时,编译器实质上将“执行”该函数并返回计算值。

8. constexpr/consteval 函数可以使用函数参数和局部变量作为 constexpr 函数调用中的参数

constexpr 或 consteval 函数可以使用其函数参数(不是 constexpr)甚至局部变量(可能根本不是 const)作为 constexpr 函数调用中的参数。在编译时计算 constexpr 或 consteval 函数时,编译器必须知道所有函数参数和局部变量的值(否则无法在编译时计算它们)。因此,在此特定上下文中,C++ 允许将这些值用作对 constexpr 函数的调用中的参数,并且该 constexpr 函数调用仍可在编译时进行计算。

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

constexpr int goo(int c) // goo() is now constexpr
{
return c;
}

constexpr int foo(int b) // b is not a constant expression within foo()
{
return goo(b); // if foo() is resolved at compile-time, then `goo(b)` can also be resolved at compile-time
}

int main()
{
std::cout << foo(5);

return 0;
}

9. constexpr 函数可以调用非 constexpr 函数

当 constexpr 函数在常量上下文中计算时,可能无法调用非 constexpr 函数(因为那样 constexpr 函数将无法生成编译时常量值),这样做会产生编译错误。允许调用非 constexpr 函数,以便 constexpr 函数可以执行如下操作:

1
2
3
4
5
6
7
8
9
#include <type_traits> // for std::is_constant_evaluated

constexpr int someFunction()
{
if (std::is_constant_evaluated()) // if evaluating in constant context
return someConstexprFcn();
else
return someNonConstexprFcn();
}

参考资料

Learn C++ – Skill up with our free tutorials