constexpr和const之间的区别


593

constexpr和之间有什么区别const

  • 什么时候只能使用其中之一?
  • 什么时候可以同时使用两者?应该如何选择?

71
constexpr创建一个编译时常量;const仅仅意味着价值是无法改变的。
0x499602D2


也许是这篇文章从boost/hana图书馆可以微启的一些constexpr问题,在这里你可以使用constexpr和,你不能:boost.org/doc/libs/1_69_0/libs/hana/doc/html/...
安德里

@ 0x499602D2“ 仅表示无法更改值 ”对于使用文字量初始化的标量,无法更改的也是编译时间常数。
curiousguy

@curiousguy是的,我的评论太过简单了。诚然,我也是新手constexpr:)
0x499602D2

Answers:


587

基本含义和语法

这两个关键字都可以在对象和函数的声明中使用。应用于对象的基本区别是:

  • const将一个对象声明为常量。这意味着保证,一旦初始化,该对象的值就不会改变,并且编译器可以利用这一事实进行优化。它还有助于防止程序员编写代码来修改在初始化后不希望修改的对象。

  • constexpr声明一个适合在标准调用常量表达式中使用的对象。但是请注意,这constexpr不是唯一的方法。

当应用于函数时,基本区别是:

  • const只能用于非静态成员函数,不能用于一般函数。它保证成员函数不会修改任何非静态数据成员。

  • constexpr可以与成员函数和非成员函数以及构造函数一起使用。它声明该函数适合在常量表达式中使用。仅当函数满足某些条件(7.1.5 / 3,4),最重要的是(†)时,编译器才会接受它:

    • 函数体必须是非虚拟的并且非常简单:除了typedef和static断言,只有一个 return允许使用语句。对于构造函数,仅允许使用初始化列表,typedef和静态断言。(= default并且= delete也被允许。)
    • 从C ++ 14开始,规则更加宽松,此后允许在constexpr函数中使用以下内容:asm声明,goto语句,带有标签而不是标签的语句casedefault,尝试块,非文字类型的变量的定义,定义静态或线程存储持续时间的变量,定义不执行初始化的变量。
    • 参数和返回类型必须是文字类型(即,通常来说,非常简单的类型,通常是标量或集合)

常数表达式

如上所述,constexpr声明两个对象以及适合在常量表达式中使用的函数。常量表达式不仅仅是常量:

  • 它可以在需要编译时评估的地方使用,例如模板参数和数组大小说明符:

    template<int N>
    class fixed_size_list
    { /*...*/ };
    
    fixed_size_list<X> mylist;  // X must be an integer constant expression
    
    int numbers[X];  // X must be an integer constant expression
  • 但请注意:

    • 声明为as constexpr并不一定保证会在编译时对其进行评估。它可以用于此目的,但也可以在运行时评估的其他地方使用。

    • 一个对象可以是适合于常量表达式使用没有被宣布constexpr。例:

      int main()
      {
        const int N = 3;
        int numbers[N] = {1, 2, 3};  // N is constant expression
      }

    这是可能的,因为N在声明时使用文字常量进行常量初始化并满足常量表达式的条件,即使未声明常量也是如此constexpr

那我constexpr什么时候真正需要使用呢?

  • 一个对象N上面可以用作常量表达式被声明constexpr。对于以下所有对象都是如此:

    • const
    • 积分或枚举类型
    • 在声明时使用本身就是常量表达式的表达式进行初始化

    [这是由于第5.19 / 2条所致:常量表达式不得包含涉及“从左值到右值的修改,除非[...]整数或枚举类型的[...]的glvalue”的子表达式”感谢Richard Smith纠正了早些时候声称对所有文字类型都是如此。]

  • 为了使函数适合在常量表达式中使用,必须明确声明它constexpr;仅满足常数表达式函数的标准是不够的。例:

    template<int N>
    class list
    { };
    
    constexpr int sqr1(int arg)
    { return arg * arg; }
    
    int sqr2(int arg)
    { return arg * arg; }
    
    int main()
    {
      const int X = 2;
      list<sqr1(X)> mylist1;  // OK: sqr1 is constexpr
      list<sqr2(X)> mylist2;  // wrong: sqr2 is not constexpr
    }

我什么时候可以/我应该同时使用,constconstexpr 在一起呢?

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被声明为地址常量表达式,即本身就是常量表达式的指针。(当通过将地址运算符应用于静态/全局常量表达式来生成地址时,这是可能的。)在此,constexprconst都是必需的:constexpr始终引用要声明的表达式(here NP),而const引用int(它声明指针- to-const)。删除const会导致表达式非法(因为(a)指向非常量对象的指针不能是常量表达式,而(b)&N实际上是指向常量的指针)。

B.在成员函数声明中。在C ++ 11中,constexpr暗含const,而在C ++ 14和C ++ 17中则并非如此。在C ++ 11下声明为的成员函数为

constexpr void f();

需要声明为

constexpr void f() const;

在C ++ 14下为了仍然可以用作const函数。


3
IMO“不一定在编译时进行评估”的帮助要比认为它们是“在编译时进行评估”的帮助小。常量表达式的约束意味着编译器对其进行评估相对容易。如果不满足这些约束,则编译器必须进行投诉。由于没有副作用,因此您永远无法分辨出编译器是否对其进行了“评估”。
aschepler

10
@aschepler好的。我的主要观点是,如果您constexpr在非常量表达式上调用函数,例如普通变量,则这是完全合法的,并且该函数将像其他函数一样使用。它不会在编译时评估(因为它不能)。也许您认为这很明显-但如果我声明声明为as的函数constexpr将始终在编译时求值,则可能会以错误的方式解释它。
jogojapan

5
是的,我说的是constexpr对象,而不是功能。我喜欢将constexpr对象视为强制对值进行编译时评估,而constexpr将函数视为允许在编译时或运行时适当地评估函数。
aschepler

2
更正:'const'仅是您不能更改变量值的限制;它不能保证价值不会改变(即被他人)。这是一个write属性,而不是read属性。
贾里德·格拉布

3
这句话:它保证成员函数不会修改任何非静态数据成员。遗漏了一个重要的细节。标记为mutableconst成员也可以通过成员函数进行修改。
全方位

119

const适用于变量,并防止它们在您的代码中被修改

constexpr告诉编译器该表达式产生一个编译时间常数值,因此可以在数组长度,赋值给const变量等地方使用。Oli提供的链接提供了很多出色的示例。

基本上,它们总共是2个不同的概念,可以(并且应该)一起使用。



64

总览

  • const保证程序不会更改对象的value。但是,const不能保证对象将进行哪种类型的初始化。

    考虑:

    const int mx = numeric_limits<int>::max();  // OK: runtime initialization

    该函数max()仅返回文字值。但是,由于初始化程序是函数调用,因此需要mx进行运行时初始化。因此,不能将其用作常量表达式

    int arr[mx];  // error: “constant expression required”
  • constexpr是一个新的C ++ 11关键字,使您无需再创建宏和硬编码的文字。它也保证在某些条件下物体能够经受静态初始化。它控制表达式的评估时间。通过对其表达式进行编译时评估constexpr可以使您在依赖编译时常量的任何代码中定义对时间紧迫的应用程序,系统编程,模板以及通常来说至关重要的真正常量表达式

常数表达函数

常数表达式函数是声明的函数constexpr。它的主体必须是非虚拟的,并且除了typedef和static断言之外,只能由单个return语句组成。它的参数和返回值必须具有文字类型。可以将其与非常量表达式参数一起使用,但是完成后的结果将不是常量表达式。

常量表达式函数旨在替换 硬编码文字,而不会牺牲性能或类型安全性。

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。必须使用常量表达式或带有常量表达式参数的常量表达式构造函数构造的右值对其进行初始化。

常量表达式对象的行为就像声明它一样 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。它可以有一个成员初始化列表,但是除了typedef和static断言之外,它的主体必须为空。它的参数必须具有文字类型。

常量表达式构造函数允许编译器在编译时初始化对象,前提是构造函数的参数均为常量表达式。

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)的《有效的现代C ++》一书中的技巧constexpr

  • constexpr 对象是常量,并使用编译期间已知的值进行初始化;
  • constexpr 当使用在编译期间已知其值的参数调用函数时,函数会产生编译时结果;
  • constexpr与非constexpr对象和功能相比,对象和功能可以在更广泛的上下文中使用;
  • constexpr 是对象或函数接口的一部分。

资料来源: 使用constexpr改善C ++的安全性,性能和封装


感谢提供了展示不同情况的出色示例代码。就像其他一些解释一样,我发现看到实际的代码更加有用和易于理解。这确实有助于巩固我对正在发生的事情的理解。
RTHarston,

35

根据Bjarne Stroustrup撰写的“ The C ++ Programming Language 4th Editon”一书
const:大致意思是“我保证不会更改此值”(第7.5节)。这主要用于指定接口,以便可以将数据传递给函数而不必担心会被修改。
编译器将执行const作出的承诺。
constexpr:大致意思是“在编译时求值”(第10.4节)。这主要用于指定常量,以允许
例如:

const 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.4square(var); // error : var is not a constant expression
const double max3 = 1.4square(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 xx; }


要成为constexpr,函数必须相当简单:仅是一个计算值的返回语句。constexpr函数可用于非恒定参数,但这样做的结果不是恒定表达式。我们允许在不需要常量表达式的上下文中使用非常量表达式参数调用constexpr函数,因此我们不必两次定义本质上相同的函数:一次用于常量表达式,一次用于变量。
在某些地方,语言规则(例如,数组边界(第2.2.5节,第7.3节),大小写标签(第2.2.4节,第9.4.2节),一些模板参数(第25.2节)和使用constexpr声明的常量)。在其他情况下,编译时评估对于性能很重要。与性能问题无关,(具有不变状态的对象的)不变性的概念是一个重要的设计问题(第10.4节)。


仍然存在性能问题。似乎constexpr函数(如果在运行时求值)可能比非constexpr版本的函数慢。另外,如果我们有一个恒定的值,我们应该选择“ const”还是“ constexpr”?(更多的样式问题生成的程序集看起来相同)
CoffeDeveloper 2014年

31

二者constconstexpr可以应用到变量和函数。尽管它们彼此相似,但实际上它们是非常不同的概念。

双方constconstexpr意味着他们的价值不能被他们的初始化后改变。因此,例如:

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总是定义可以指定数组大小的编译时间常数。

双方constconstexpr可以应用到的功能了。甲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.
   }
};

A 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也可应用于所述成员函数(方法),操作者和甚至构造函数。例如。

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.

同样,在C中,constexpr int存在但它是拼写的const int
curiousguy

8

正如@ 0x499602d2已经指出的那样,const仅确保在初始化后不能更改值,而as constexpr(在C ++ 11中引入)保证变量是编译时间常数。
考虑以下示例(来自LearnCpp.com):

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

5

const int var可以在运行时将A 动态设置为一个值,一旦将其设置为该值,就无法再对其进行更改。

A 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
}

上面的代码片段编译良好,我已经注释掉了导致其出错的代码。

这里要注意的关键概念是compile timerun time。C ++中引入了新的创新,旨在** know **在编译时尽可能多地执行某些操作,以提高运行时的性能。


3

我认为,没有任何答案能确切说明它有什么副作用,或者实际上是什么副作用。

constexprconst当用文字或表达式初始化时,在命名空间/文件范围内是相同的;但是带有函数,const可以由任何函数constexpr初始化,但是由非constexpr初始化(未用constexpr或非constexpr表达式标记的函数)会生成编译器错误。两者constexprconst都是变量的隐式内部链接(实际上,如果编译-O1和更强大的变量,它们将无法生存到链接阶段,并且static不会强制编译器在constconstexpr处于时发出内部(本地)链接器符号-O1或更强;只有当您获取变量的地址时,它才会这样做,const并且constexpr它将是一个内部符号,除非用externie 表示extern constexpr/const int i = 3;需要使用)。上的功能,constexpr使功能永久不会到达连接阶段(无论externinline在定义或-O0或-Ofast),而const从来不,并且staticinline只对-O1和上述这种效果。当const/ constexpr变量由constexpr函数初始化时,总是使用任何优化标志来优化负载,但是如果函数仅是staticinline,或者变量不是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

constexprat块作用域的行为相同,如果由非constexpr函数初始化,则会产生错误;该值也将立即替换。

最后,其主要目的类似于C的内联函数,但仅在该函数用于初始化文件作用域变量时才有效(该函数不能在C上执行,但可以在C ++上执行,因为它允许对文件范围进行动态初始化。作用域变量),除非该函数也无法将全局/局部符号导出到链接器,即使使用extern/static,也可以inline在C上使用;无需constexpr使用C和C ++,只需使用-O1优化就可以内联块范围变量分配函数。


链接器上的好点。通常使用constexpr会更安全,因为它会减少符号泄漏?
尼尔·麦吉尔

1
@NeilMcGill并不是真的,因为如果使用-O1或更高版本进行编译,则内联和静态将导致编译器不发出用于乘法的本地符号。Constexpr是唯一一个优化val负载的工具,但不同于将static或inline放在函数之前。我也忘记了其他东西。Constexpr是唯一不会在-O0,静态和内联函数上发出该函数符号的关键字
Lewis Kelsey

1

首先,两者都是c ++中的限定符。声明为const的变量必须进行初始化,以后不能更改。因此,通常声明为const的变量甚至在编译前都将具有值。

但是,对于constexpr来说有点不同。

对于constexpr,您可以提供一个可以在程序编译期间求值的表达式。

显然,声明为constexper的变量将来不能像const一样进行更改。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.