constexpr
和 const
有什么区别?
- 我什么时候可以只使用其中一个?
- 我什么时候可以同时使用它们,如何选择?
这两个关键字都可以用于对象和函数的声明。当应用于对象时,它们的基本区别如下:
const
将对象声明为常量。这意味着一旦初始化,该对象的值将不会改变,并且编译器可以利用这一事实进行优化。它还有助于防止程序员在初始化后修改不应被修改的对象。
constexpr
将对象声明为适用于标准所称的常量表达式。但请注意,constexpr
并不是唯一的方法。
当应用于函数时,基本区别如下:
const
只能用于非静态成员函数,而不是一般的函数。它保证了成员函数不会修改任何非静态数据成员(除了可变数据成员,它可以被任意修改)。
constexpr
可以用于成员函数和非成员函数,以及构造函数。它声明该函数适用于常量表达式。只有当函数满足某些条件(7.1.5/3,4)时,编译器才会接受它,其中最重要的是 (†):
return
语句。对于构造函数,只允许初始化列表、typedefs和static assert。(= default
和= delete
也是允许的。)如上所述,constexpr
声明对象和函数都适用于常量表达式。常量表达式不仅仅是常量:
它可以在需要编译时评估的地方使用,例如模板参数和数组大小指定:
template<int N>
class fixed_size_list
{ /*...*/ };
fixed_size_list<X> mylist; // X 必须是一个整数常量表达式
int numbers[X]; // X 必须是一个整数常量表达式
但请注意:
将某物声明为constexpr
并不一定保证它会在编译时进行评估。它可以用于这样的情况,但也可以用于其他在运行时进行评估的地方。
一个对象在没有声明为constexpr
的情况下可能适用于常量表达式。例如:
int main()
{
const int N = 3;
int numbers[N] = {1, 2, 3}; // N 是常量表达式
}
这是可能的,因为N
是常量,并且在声明时用字面值初始化,满足常量表达式的条件,即使它没有声明为constexpr
。
那么我什么时候需要使用constexpr
呢?
N
这样的对象可以在没有声明constexpr
的情况下用作常量表达式。对于以下所有满足条件的对象都是如此:
const
且[这是由于§5.19/2:常量表达式不能包含涉及“左值到右值修改”的子表达式,除非……是整数或枚举类型的glvalue。感谢Richard Smith纠正了我之前关于这适用于所有字面类型的说法。]
要使一个函数适用于常量表达式,它必须明确声明为`constexpr`;仅满足常量表达式函数的条件是不够的。例如:何时可以/应该同时使用const
和constexpr
?
A. 在对象声明中。当这两个关键字都指向要声明的同一对象时,这是不必要的。constexpr
隐含了const
。
constexpr const int N = 5;
就是一样的
constexpr int N = 5;
static constexpr int N = 3;
int main()
{
constexpr const int *NP = &N;
}
NP
被声明为一个地址常量表达式,即一个指针本身就是一个常量表达式。(当地址是通过将地址运算符应用于静态/全局常量表达式生成时,这是可能的。)在这里,constexpr
和const
都是必需的:constexpr
始终引用被声明的表达式(这里是NP
),而const
引用int
(它声明了一个指向常量的指针)。移除const
将使表达式非法(因为(a)指向非常量对象的指针不能是常量表达式,(b)&N
实际上是一个指向常量的指针)。
B. 在成员函数声明中。在C++11中,constexpr
意味着const
,而在C++14和C++17中不是这样。在C++11下声明的成员函数为constexpr void f();
需要声明为
constexpr void f() const;
constexpr
函数,比如普通变量,这是完全合法的,并且该函数将像任何其他函数一样使用。它不会在编译时进行求值(因为它不能)。也许你认为这很明显--但如果我声明一个constexpr
函数将始终在编译时求值,这可能会被错误地理解。 - jogojapanconstexpr
对象,而不是函数。将constexpr
用于对象时,我喜欢认为它会强制在编译时计算值;而将constexpr
用于函数时,它允许根据需要在编译时或运行时计算函数。 - ascheplerconst
应用于 变量,并且防止它们在代码中被修改。
constexpr
声明此表达式的结果为编译时常量值,因此可以在数组长度、赋值给const
变量等地方使用。Oli提供的链接中有很多优秀的示例。
基本上,它们是完全不同的2个概念,并且可以(并且应该)一起使用。
签名,其中两者都适用于函数,而在
constexpr const T f(x)(即array::get签名)中,
const是返回类型的一部分,而不是函数属性(不确定标准术语的名称)。尽管这个答案没有承认在成员函数上使用
const`。 - tedconst
并不能防止变量被修改,它只是防止程序员在没有明确表示想要这样做的情况下(通过强制类型转换)修改变量。 - einpoklumconst
保证程序不会改变对象的值。然而,const
不保证对象经历哪种类型的初始化。
考虑以下例子:
const int mx = numeric_limits<int>::max(); // OK: runtime initialization
max()
函数仅返回一个字面值。然而,由于初始化程序是函数调用,mx
会在运行时进行初始化。因此,您不能将其用作常量表达式:
函数max()
只是返回一个字面值。但是,由于初始化器是函数调用,mx
会在运行时初始化。因此,您无法将其用作常数表达式:
int arr[mx]; // error: “constant expression required”
constexpr
是C++11中的一个新关键字,它可以替代宏和硬编码文字。在某些条件下,它还可以保证对象进行静态初始化。它控制表达式的评估时间。通过强制对其表达式进行编译时求值,constexpr
允许您定义真正的常量表达式,这对于时间关键型应用程序、系统编程、模板以及通常依赖于编译时常量的任何代码都是至关重要的。
一个常量表达式函数是一个声明为constexpr
的函数。它的主体必须是非虚拟的并且只由一个返回语句组成(除了typedef和static assert)。它的参数和返回值必须具有文字类型。它可以与非常量表达式参数一起使用,但是这样做的结果不是一个常量表达式。
常量表达式函数旨在替代宏和硬编码文字,而不会牺牲性能或类型安全性。
constexpr int max() { return INT_MAX; } // OK
constexpr long long_max() { return 2147483647; } // OK
constexpr bool get_val()
{
bool res = false;
return res;
} // error: body is not just a return statement
constexpr int square(int x)
{ return x * x; } // OK: compile-time evaluation only if x is a constant expression
const int res = square(5); // OK: compile-time evaluation of square(5)
int y = getval();
int n = square(y); // OK: runtime evaluation of square(y)
常量表达式对象 是使用 constexpr
声明的对象。它必须用常量表达式或由常量表达式构造函数用常量表达式参数构造的 rvalue 进行初始化。
常量表达式对象的行为就像被声明为 const
一样,除了必须在使用前进行初始化,且其初始化程序必须是一个常量表达式。因此,常量表达式对象始终可以作为另一个常量表达式的一部分使用。
struct S
{
constexpr int two(); // constant-expression function
private:
static constexpr int sz; // constant-expression object
};
constexpr int S::sz = 256;
enum DataPacket
{
Small = S::two(), // error: S::two() called before it was defined
Big = 1024
};
constexpr int S::two() { return sz*2; }
constexpr S s;
int arr[s.two()]; // OK: s.two() called after its definition
一个常量表达式构造函数是一个声明为constexpr
的构造函数。它可以有成员初始化列表,但其函数体必须为空,除了typedefs
和static_asserts
之外。它的参数必须具有字面类型。
常量表达式构造函数允许编译器在编译时初始化对象,前提是构造函数的参数都是常量表达式。
struct complex
{
// constant-expression constructor
constexpr complex(double r, double i) : re(r), im(i) { } // OK: empty body
// constant-expression functions
constexpr double real() { return re; }
constexpr double imag() { return im; }
private:
double re;
double im;
};
constexpr complex COMP(0.0, 1.0); // creates a literal complex
double x = 1.0;
constexpr complex cx1(x, 0); // error: x is not a constant expression
const complex cx2(x, 1); // OK: runtime initialization
constexpr double xx = COMP.real(); // OK: compile-time initialization
constexpr double imaglval = COMP.imag(); // OK: compile-time initialization
complex cx3(2, 4.6); // OK: runtime initialization
Scott Meyers在他的书《Effective Modern C++》中给出了有关constexpr
的提示:
constexpr
对象是常量,并使用编译时已知的值进行初始化;constexpr
函数产生编译时结果;constexpr
对象和函数可以在比非constexpr
对象和函数更广泛的上下文中使用;constexpr
是对象或函数界面的一部分。来源:Using constexpr to Improve Security, Performance and Encapsulation in C++。
numeric_limits<int>::max()
是一个常量表达式。(因为它是constexpr
) - apple appleconst
和 constexpr
都可以应用于变量和函数。尽管它们非常相似,但实际上它们是非常不同的概念。
const
和 constexpr
都意味着它们的值在初始化后不能被更改。例如:
const int x1=10;
constexpr int x2=10;
x1=20; // ERROR. Variable 'x1' can't be changed.
x2=20; // ERROR. Variable 'x2' can't be changed.
const
和constexpr
的主要区别在于它们初始化值(评估)的时间。虽然const
变量的值可以在编译时和运行时评估,但constexpr
始终在编译时评估。例如:
int temp=rand(); // temp is generated by the the random generator at runtime.
const int x1=10; // OK - known at compile time.
const int x2=temp; // OK - known only at runtime.
constexpr int x3=10; // OK - known at compile time.
constexpr int x4=temp; // ERROR. Compiler can't figure out the value of 'temp' variable at compile time so `constexpr` can't be applied here.
了解值是在编译时还是运行时已知的关键优势在于,可以在需要编译时常量的任何地方使用编译时常量。例如,C ++不允许您使用可变长度指定C数组。
int temp=rand(); // temp is generated by the the random generator at runtime.
int array1[10]; // OK.
int array2[temp]; // ERROR.
const int size1=10; // OK - value known at compile time.
const int size2=temp; // OK - value known only at runtime.
constexpr int size3=10; // OK - value known at compile time.
int array3[size1]; // OK - size is known at compile time.
int array4[size2]; // ERROR - size is known only at runtime time.
int array5[size3]; // OK - size is known at compile time.
因此,const
变量既可以定义编译时常量,例如size1
,用于指定数组大小,也可以定义仅在运行时已知且不能用于定义数组大小的运行时常量,例如size2
。另一方面,constexpr
总是定义可以指定数组大小的编译时常量。
const
和constexpr
也可以应用于函数。一个const
函数必须是一个成员函数(方法、运算符),其中应用const
关键字意味着该方法无法更改其成员(非静态)字段的值。例如。
class test
{
int x;
void function1()
{
x=100; // OK.
}
void function2() const
{
x=100; // ERROR. The const methods can't change the values of object fields.
}
};
constexpr
是一个不同的概念。它将一个函数(成员或非成员)标记为可以在编译时评估的函数,如果编译时常量作为其参数传递。例如,您可以编写以下代码。
constexpr int func_constexpr(int X, int Y)
{
return(X*Y);
}
int func(int X, int Y)
{
return(X*Y);
}
int array1[func_constexpr(10,20)]; // OK - func_constexpr() can be evaluated at compile time.
int array2[func(10,20)]; // ERROR - func() is not a constexpr function.
int array3[func_constexpr(10,rand())]; // ERROR - even though func_constexpr() is the 'constexpr' function, the expression 'constexpr(10,rand())' can't be evaluated at compile time.
constexpr
函数是常规的C++函数,即使传递非常量参数也可以调用。但在这种情况下,您会得到非constexpr
值。int value1=func_constexpr(10,rand()); // OK. value1 is non-constexpr value that is evaluated in runtime.
constexpr int value2=func_constexpr(10,rand()); // ERROR. value2 is constexpr and the expression func_constexpr(10,rand()) can't be evaluated at compile time.
constexpr
可以应用于成员函数 (methods),操作符,甚至构造函数。例如:
class test2
{
static constexpr int function(int value)
{
return(value+1);
}
void f()
{
int x[function(10)];
}
};
class test3
{
public:
int value;
// constexpr const method - can't chanage the values of object fields and can be evaluated at compile time.
constexpr int getvalue() const
{
return(value);
}
constexpr test3(int Value)
: value(Value)
{
}
};
constexpr test3 x(100); // OK. Constructor is constexpr.
int array[x.getvalue()]; // OK. x.getvalue() is constexpr and can be evaluated at compile time.
constexpr int
,但它的拼写为const int
。 - curiousguyconst int dmv = 17; // dmv is a named constant
int var = 17; // var is not a constant
constexpr double max1 = 1.4*square(dmv); // OK if square(17) is a constant expression
constexpr double max2 = 1.4∗square(var); // error : var is not a constant expression
const double max3 = 1.4∗square(var); //OK, may be evaluated at run time
double sum(const vector<double>&); // sum will not modify its argument (§2.2.5)
vector<double> v {1.2, 3.4, 4.5}; // v is not a constant
const double s1 = sum(v); // OK: evaluated at run time
constexpr double s2 = sum(v); // error : sum(v) not constant expression
要使一个函数在编译器评估的表达式中可用,也就是在常量表达式中可用,它必须被定义为constexpr。
例如:
constexpr double square(double x) { return x∗x; }
为了成为constexpr函数,一个函数必须相当简单:只有一个返回语句计算一个值。constexpr函数可以用于非常量参数,但是当这样做时,结果不是常量表达式。我们允许在不需要常量表达式的情况下以非常量表达式参数调用constexpr函数,这样我们就不需要基本上定义相同的函数两次:一次用于常量表达式,一次用于变量。
在一些地方,语言规则要求使用常量表达式(例如,数组边界(§2.2.5、§7.3)、case标签(§2.2.4、§9.4.2)、某些模板参数(§25.2)和使用constexpr声明的常量)。在其他情况下,编译时评估对性能非常重要。独立于性能问题之外,不可变性(具有不可改变状态的对象)的概念是一个重要的设计关注点(§10.4)。
const int var
可以在运行时动态设置为一个值,一旦被设置为该值,就不能再改变。
constexpr int var
不能在运行时动态设置,而是在编译时设置。一旦被设置为该值,就不能再改变。
这里有一个实例:
int main(int argc, char*argv[]) {
const int p = argc;
// p = 69; // cannot change p because it is a const
// constexpr int q = argc; // cannot be, bcoz argc cannot be computed at compile time
constexpr int r = 2^3; // this works!
// r = 42; // same as const too, it cannot be changed
}
以上代码片段可以正常编译,我已将导致错误的部分注释掉。
这里需要注意的关键概念是“编译时”和“运行时”的概念。C++引入了新的创新,旨在尽可能多地在编译时“知道”某些事情,以提高运行时的性能。
任何不涉及上述两个关键概念的解释都是妄想。
const
只保证一个值在初始化后不能被改变,而 constexpr
(在 C++11 中引入)则保证该变量是一个编译时常量。cout << "Enter your age: ";
int age;
cin >> age;
const int myAge{age}; // works
constexpr int someAge{age}; // error: age can only be resolved at runtime
constexpr
和 const
是相同的;但对于函数,const
可以由任何函数初始化,但是非 constexpr 的函数(没有标记 constexpr 或非 constexpr 表达式)初始化的 constexpr
会生成编译器错误。对于变量来说,constexpr
和 const
都是隐式的内部链接(实际上,在编译 -O1 以上时,它们不会存活到链接阶段;而 static
并不会强制编译器为 const
或 constexpr
发出一个内部(本地)链接符号,除非你取变量的地址。只有在使用 extern
表达时,const
和 constexpr
才会成为一个内部符号,例如:extern constexpr/const int i = 3;
)。对于函数来说,constexpr
使函数永远无法到达链接阶段(无论定义中是否带有 extern
或 inline
,以及是否使用 -O0 或 -Ofast),而 const
则永远不会,而 static
和 inline
只在 -O1 以上才具有这种效果。当使用 constexpr
函数初始化 const
/constexpr
变量时,无论使用任何优化标志都会将负载优化掉,但如果函数只是 static
或 inline
,或者变量不是 const
/constexpr
,则永远不会被优化。
标准编译(-O0)
#include<iostream>
constexpr int multiply (int x, int y)
{
return x * y;
}
extern const int val = multiply(10,10);
int main () {
std::cout << val;
}
编译成
val:
.long 100 //extra external definition supplied due to extern
main:
push rbp
mov rbp, rsp
mov esi, 100 //substituted in as an immediate
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov eax, 0
pop rbp
ret
__static_initialization_and_destruction_0(int, int):
.
.
.
然而
#include<iostream>
const int multiply (int x, int y)
{
return x * y;
}
const int val = multiply(10,10); //constexpr is an error
int main () {
std::cout << val;
}
编译为
multiply(int, int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
mov DWORD PTR [rbp-8], esi
mov eax, DWORD PTR [rbp-4]
imul eax, DWORD PTR [rbp-8]
pop rbp
ret
main:
push rbp
mov rbp, rsp
mov eax, DWORD PTR val[rip]
mov esi, eax
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov eax, 0
pop rbp
ret
__static_initialization_and_destruction_0(int, int):
.
.
.
mov esi, 10
mov edi, 10
call multiply(int, int)
mov DWORD PTR val[rip], eax
显然,使用 constexpr
会导致 const/constexpr
文件作用域变量的初始化在编译时发生,并且不会产生全局符号;而不使用它会导致初始化在运行时的 main
函数之前发生。
使用 -Ofast 编译
即使使用 -Ofast 编译选项也无法优化掉这个加载操作!https://godbolt.org/z/r-mhif,因此您需要使用constexpr
。
constexpr
函数也可以从其他constexpr
函数中调用,以达到相同的结果。在函数上使用constexpr
还可以防止在函数中进行不能在编译时完成的任何操作,例如对std::cout
的<<
操作。
在块作用域中使用constexpr
会产生相同的错误,如果由非constexpr
函数初始化,则该值也会立即替换。
最终,constexpr
的主要目的类似于C语言的内联函数,但仅在将函数用于初始化文件作用域变量时才有效(因为C语言不允许使用函数初始化文件作用域变量,但C++可以允许动态初始化文件作用域变量),即使使用extern/static
,该函数也无法导出全局/局部符号给链接器,并且在C和C++中,块作用域变量分配函数可以仅使用-O1优化而不需要constexpr
来进行内联。
在C++中,如果一个const对象使用常量表达式初始化,那么我们可以在任何需要常量表达式的地方使用该const对象。
const int x = 10;
int a[x] = {0};
constexpr auto x = 10;
struct Data { // We can make a bit field element of struct.
int a:x;
};
constexpr const int* p;
通常情况下,函数的返回值在运行时获得。但是,当满足某些条件时,对constexpr函数的调用将在编译时作为常量获得。
注意: 如果在函数调用中发送给参数变量或者所有参数变量(如果有多个参数),则如果是C.E,则函数的返回值将在编译时计算。!!!
constexpr int square (int a){
return a*a;
}
constexpr int a = 3;
constexpr int b = 5;
int arr[square(a*b+20)] = {0}; //This expression is equal to int arr[35] = {0};
class Myclass {
const static int sx = 15; // OK
constexpr static int sy = 15; // OK
const static double sd = 1.5; // ERROR
constexpr static double sd = 1.5; // OK
};
首先,两者都是C++中的限定符。
声明为const的变量必须被初始化,并且将来不能更改。因此,通常在编译之前就会有一个值作为const变量的值。
但是,对于constexpr,情况有所不同。
对于constexpr,您可以提供一个表达式,在程序编译期间可以进行评估。
显然,声明为constexpr的变量就像const一样不能在将来更改。
constexpr
创建编译时常量;const
仅表示该值不能被更改。 - David Gboost/hana
库中的这篇文章可以阐明一些有关constexpr
的问题,它会告诉你何时可以使用constexpr
以及何时不能使用:https://www.boost.org/doc/libs/1_69_0/libs/hana/doc/html/index.html#tutorial-appendix-constexpr - Andryconstexpr
也很陌生 :) - David Gconst
的定义有点过载,尤其是在 C++ 中服务于不同的目的。如果当时有constexpr
,const
可能会更单一用途。也许吧。谁知道呢?我们没有时间机器去旅行到另一个宇宙看看。 - curiousguy