Effective Modern C++[实践]->只要有可能使用constexpr,就使用它

  • constexpr变量具备const属性,且必须由编译器已知的值完成初始化。
  • constexpr函数在调用时,若传入的实参值都是编译期已知的,则返回的结果也是constexpr变量,否则返回非constexpr变量
  • 比起非constexpr变量或constexpr函数而言,constexpr函数或constexpr变量可以用在一个作用域更广的语境中
  • constexpr 是对象和函数接口的一部分。

在看此条规约之前,我们先看如下代码,这三个分别是宏、const修饰、constexpr修饰,有何区别,意义何在,本文会一一讲清。

// 预处理
#define MEANING_OF_LIFE 42
// 常量:
const int MeaningOfLife = 42;
// constexpr-函数:
constexpr int MeaningOfLife () {
    
     return 42; }

宏——>避免使用宏

实不相瞒:宏是CC++语言的抽象设施中最生硬的工具,它是披着函数外衣的饥饿的狼,很难驯服,它会我行我素地游走于各处。要避免使用宏。

宏被吹捧为一种文本替换设施,其效果在预处理阶段就产生了,而此时c++的语法和语义规则都还没有起作用。

c++中几乎从不需要用宏。可以用const或者enum定义易于理解的常量,用inline避免函数调用的开销,用template指定函数系列和类型系列,用namespace避免名称冲突。—stroustrup

关于宏的第一规则就是:不要使用它,除非不得不用。几乎每个宏都说明程序设计语言、程序或者程序员存在缺陷。—stroustrup

宏会忽略作用域,忽略类型系统,忽略所有其他的语言特性和规则,而且会劫持它为文件其余部分所定义(#define)的符号。宏看上去很像符号或者函数调用,但实际并非如此。它会根据自己被使用时所处的环境引人注目而且令人惊奇地展开为各种东西。
宏中的错误可能只有在宏展开之后才能被报告出来,而不是在定义时。
例外情况:

  1. 为头(文件)添加保护:在所有头文件中使用带有唯一名称的包含保护符(#include guard防止无意的多次包含
  2. 断言一般只会在调试模式下生成代码(在NDEBUG宏没有被定义时),因此在发行版本中他们是不存在的。
  3. 条件编译中的#ifdefif defined。在条件编译(如与系统有关的部分)中,要避免在代码中到处杂乱地插入#ifdef。相反,应该对代码进行组织,利用宏来驱动一个公共接口的多个实现,然后始终使用该接口。

常量表达式

定义能在编译时求值的表达式。

所谓常量表达式,指的就是由多个(≥1)常量组成的表达式。换句话说,如果表达式中的成员都是常量,那么该表达式就是一个常量表达式。这也意味着,常量表达式一旦确定,其值将无法修改。

int n = 1;
std::array<int, n> a1; // 错误:n 不是常量表达式
const int cn = 2;
std::array<int, cn> a2; // OK:cn 是常量表达式

C++ 程序的执行过程大致要经历编译、链接、运行这 3 个阶段。值得一提的是,常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果;而常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。

#include <array>
#include <iostream>
#include <array>
using namespace std;
void dis_1(const int x) {
    
    
    //error: 'x' is not a constant expression
    array<int, x> myarr{
    
    1, 2, 3, 4, 5};
    cout << myarr[1] << endl;
}
void dis_2() {
    
    
    const int x = 5;
    array<int, x> myarr{
    
    1, 2, 3, 4, 5};
    cout << myarr[1] << endl;
}
int main() {
    
    
    dis_1(5);
    dis_2();
}

const 何用

  1. 修饰变量,说明该变量不可以被改变;
  2. 修饰指针,分为指向常量的指针和指针常量;
  3. 常量引用,经常用于形参类型,即避免了拷贝,又避免了函数对值的修改;
  4. 修饰成员函数,说明该成员函数内不能修改成员变量。
// 类
class A {
    
    
   private:
    const int a;  // 常对象成员,只能在初始化列表赋值

   public:
    // 构造函数
    A() : a(0){
    
    };
    A(int x) : a(x){
    
    };  // 初始化列表

    // const可用于对重载函数的区分
    int getValue();  // 普通成员函数
    int getValue() const;  // 常成员函数,不得修改类中的任何数据成员的值
};

void function() {
    
    
    // 对象
    A b;        // 普通对象,可以调用全部成员函数
    const A a;  // 常对象,只能调用常成员函数、更新常成员变量
    const A* p = &a;  // 常指针
    const A& q = a;   // 常引用

    // 指针
    char greeting[] = "Hello";
    char* p1 = greeting;              // 指针变量,指向字符数组变量
    const char* p2 = greeting;        // 指针变量,指向字符数组常量
    char* const p3 = greeting;        // 常指针,指向字符数组变量
    const char* const p4 = greeting;  // 常指针,指向字符数组常量
}

// 函数
void function1(const int Var);    // 传递过来的参数在函数内不可变
void function2(const char* Var);  // 参数指针所指内容为常量
void function3(char* const Var);  // 参数指针为常指针
void function4(const int& Var);   // 引用参数在函数内为常量

// 函数返回值
const int function5();  // 返回一个常数
const int*
function6();  // 返回一个指向常量的指针变量,使用:const int *p = function6();
int* const
function7();  // 返回一个指向变量的常指针,使用:int* const p = function7();

const是我们的朋友:不变的值更易于理解、跟踪和分析,所以应该尽可能地使用常量代替变量,定义值的时候,应该把const作为默认的选项;常量很安全,在编译时会对其进行检查,而且它与c++的类型系统已浑然一体。不要强制转换const的类型,除非要调用常量不正确的函数。

惯用手法

const double gravity {
    
     9.8 };  // 首选在类型前使用const
int const sidesInSquare {
    
     4 }; // "east const" 风格, ok但是不推荐

const变量必须初始化

const变量在定义时必须初始化,然后不能通过赋值来改变其值:

int main()
{
    
    
    const double gravity; // error: const variables must be initialized
    gravity = 9.9;        // error: const variables can not be changed

    return 0;
}

编译如下:

<source>: In function 'int main()':
<source>:7:18: error: uninitialized 'const gravity' [-fpermissive]
    7 |     const double gravity;  // error: const variables must be initialized
      |                  ^~~~~~~
<source>:8:13: error: assignment of read-only variable 'gravity'
    8 |     gravity = 9.9;         // error: const variables can not be changed

注意,常量变量可以从其他变量(包括非常量变量)初始化:

#include <iostream>

int main() {
    
    
    int age{
    
    };
    age = 11;

    const int constAge{
    
    age};  // 使用非const值初始化const变量

    age = 5;  // ok: age 是非常数的,所以我们可以改变它的值
    // constAge = 6;  // error: constAge 是const,我们不能更改其值

    return 0;
}

传值和返回值

  • 通过值传递时不要使用 const
  • 通过值返回时不要使用 const

内联函数

使用

  • 相当于把内联函数里面的内容写在调用内联函数处;
  • 相当于不用执行进入函数的步骤,直接执行函数体;
  • 相当于宏,却比宏多了类型检查,真正具有函数特性;
  • 不能包含循环、递归、switch 等复杂操作;
  • 在类声明中定义的函数,除了虚函数的其他函数都会自动隐式地当成内联函数。
// 声明1(加 inline,建议使用)
inline int functionName(int first, int secend,...);
// 声明2(不加 inline)
int functionName(int first, int secend,...);
// 定义
inline int functionName(int first, int secend,...) {
    
    /****/};

// 类内定义,隐式内联
class A {
    
    
    int doA() {
    
     return 0; }         // 隐式内联
}
// 类外定义,需要显式内联
class A {
    
    
    int doA();
}
inline int A::doA() {
    
     return 0; }   // 需要显式内联

优缺点

优点

  1. 内联函数同宏函数一样将在被调用处进行代码展开,省去了参数压栈、栈帧开辟与回收,结果返回等,从而提高程序运行速度
  2. 内联函数相比宏函数来说,在代码展开时,会做安全检查或自动类型转换(同普通函数),而宏定义则不会。
  3. 在类中声明同时定义的成员函数,自动转化为内联函数,因此内联函数可以访问类的成员变量,宏定义则不能。
  4. 内联函数在运行时可调试,而宏定义不可以。

缺点

  1. **代码膨胀。**内联是以代码膨胀(复制)为代价,消除函数调用带来的开销。如果执行函数体内代码的时间,相比于函数调用的开销较大,那么效率的收获会很少。另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
  2. inline 函数无法随着函数库升级而升级。inline函数的改变需要重新编译,不像 non-inline 可以直接链接。
  3. 是否内联,程序员不可控。内联函数只是对编译器的建议,是否对函数内联,决定权在于编译器。

虚函数可以内联吗

  1. 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联
  2. 内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
  3. inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
#include <iostream>  
using namespace std;
class Base
{
    
    
public:
	inline virtual void who(){
    
    
		cout << "I am Base\n";
	}
	virtual ~Base() {
    
    }
};
class Derived : public Base{
    
    
public:
	inline void who() {
    
     // 不写inline时隐式内联
		cout << "I am Derived\n";
	}
};

int main(){
    
    
	// 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。 
	Base b;
	b.who();
	// 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。  
	Base *ptr = new Derived();
	ptr->who();
	// 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。
	delete ptr;
	ptr = nullptr;
	system("pause");
	return 0;
}
概念 内联函数
它在编译时评估代码/表达式时删除了函数调用。 它几乎不删除任何函数调用,因为它在运行时对表达式执行操作。
可以在编译时评估变量或函数的值。 无法在编译时评估函数或变量的值。
它并不意味着外部联系 它意味着外部联系。

constexpr - 广义的常量表达式

constexpr机制

  • 提供更一般的常量表达式
  • 允许包含用户定义类型的常量表达式
  • 提供了一种保证在编译时完成初始化的方法

constexpr 说明符声明编译时可以对函数或变量求值。这些变量和函数(给定了合适的函数实参的情况下)即可用于需要编译期常量表达式的地方。

  • 声明对象或非静态成员函数 (C++14 前)时使用 constexpr 说明符则同时蕴含 const
  • 声明函数或静态成员变量 (C++17 起)时使用constexpr说明符则同时蕴含 inline。如果一个函数或函数模板的某个声明拥有constexpr说明符,那么它的所有声明都必须含有该说明符。

示例:

#include <array>
#include <iostream>

using namespace std;

constexpr int ximen = 12; //蕴含const
//若无constexpr则会报错 
//error: redeclaration 'int fun()' differs in 'constexpr' from previous declaration
constexpr int fun();
constexpr auto square(int x) -> int {
    
     return x * x; } //蕴含inline
constexpr auto twice_square(int x) -> int {
    
     return square(x); } //蕴含inline
constexpr int fun() {
    
     return 12;}  
int main() {
    
     return 0; }

编译生成的内容如下:

#include <array>
#include <iostream>

using namespace std;
constexpr const int ximen = 12;
inline constexpr int fun();

inline constexpr int square(int x)
{
    
    
  return x * x;
}
inline constexpr int twice_square(int x)
{
    
    
  return square(x);
}
//若无constexpr则会报错 
//error: redeclaration 'int fun()' differs in 'constexpr' from previous declaration
inline constexpr int fun()
{
    
    
  return 12;
}
int main()
{
    
    
  return 0;
}

修饰普通变量

constexpr 变量必须满足下列要求:

  • 它的类型必须是字面类型 (LiteralType) 。
  • 它必须被立即初始化。
  • 它的初始化包括所有隐式转换、构造函数调用等的全表达式必须是常量表达式 (当满足这两个条件时,可以将引用声明为 constexpr: 被引用的对象由常量表达式初始化,初始化过程中调用的任何隐式转换也是常量表达式)
  • 它必须拥有常量析构 (C++20 起) ,即:
    • 它不是类类型或它的(可能多维的)数组,或
    • 它是类类型或它的(可能多维的)数组,且该类类型拥有 constexpr 析构函数;对于作用仅为销毁该对象的虚设表达式 e,如果该对象与它的非mutable子对象(但不含它的 mutable 子对象)的生存期始于e内,那么 e 是核心常量表达式。

如果 constexpr 变量不是翻译单元局部的,那么它不应被初始化为指向或指代可用于常量表达式的翻译单元局部实体,或拥有指向或指代这种实体的(可能递归的)子对象。这种初始化在模块接口单元(在它的私有模块片段外,如果存在)或模块划分中被禁止,并在任何其他语境中被弃用 (C++20 起)
示例:

#include <math.h>
#include <iostream>

using namespace std;

int main() {
    
    
    constexpr float x = 42.0;
    constexpr float y{
    
    108};
    constexpr float z = exp(5);
    constexpr int i;  // error: uninitialized 'const i' [-fpermissive]

    //可以使用constexpr运行时的值,但不能在编译时使用运行时变量。
    int j = 0;
    constexpr int k = j + 1; //  error: the value of 'j' is not usable in a constant expression
    int kk = x; // ok
    x = 100.00002; //error: assignment of read-only variable 'x'
}

修饰函数

  • 函数必须具有非void返回类型

  • 如果函数或函数模板的任何声明都是通过constexpr指定的,那么它的所有声明都必须包含 constexpr说明符

constexpr int f1();  // OK, function declaration
int f1() {
    
               // Error, the constexpr specifier is missing
  return 55;
}     
  • constexpr不保证编译时求值;它只是保证如果程序员需要或编译器决定这样做以优化,就可以在编译时为常量表达式参数求值。如果用不是常量表达式的参数调用了 constexpr函数或构造函数,那么调用的行为就好像该函数不是 constexprr 函数一样,并且得到的值也不是常量表达式。同样,如果在给定的调用中,constexpr函数返回语句中的表达式不求值为常数表达式,则结果不是常数表达式。
#include <iostream>

using namespace std;

constexpr int min(int x, int y) {
    
     return x < y ? x : y; }

void test(int v) {
    
    
    int m1 = min(-1, 2);            // 可能是编译时评估
    constexpr int m2 = min(-1, 2);  // 编译时评估
    int m3 = min(-1, v);            // 运行时评估
    // constexpr int m4 = min(-1, v);  //error: 'v' is not a constant expression
}

int main() {
    
    
    return 0;
}
        mov     DWORD PTR [rbp-4], -1
        mov     DWORD PTR [rbp-8], -1
        mov     eax, DWORD PTR [rbp-20]
        mov     esi, eax
        mov     edi, -1
        call    min(int, int)
        mov     DWORD PTR [rbp-12], eax
  • constexpr 意为函数必须是一个简单的形式,这样,如果给定了常量表达式参数,就可以在编译时计算它,函数主体可能只包含声明、 null 语句和单个return语句。必须存在参数值,以便在参数替换之后,return 语句中的表达式生成一个常量表达式。
    阶乘示例
#include <iostream>
#include <vector>

using namespace std;

constexpr int fac(int n) {
    
    
    constexpr int max_exp = 2;
    if (0 <= n && n < max_exp) return 1; 
    int x = 1;
    for (int i = 2; i <= n; ++i) x *= i;
    return x;
}
int main() {
    
    
    int ddd = fac(5);
    cout << " ddd = " << ddd << endl;
}

汇编代码部分如下:

main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], 120
//c++11的实例 constexpr 函数必须把一切放在单条 return 语句中
#include <iostream>
constexpr long long factorial(long long n) {
    
    
    return (n == 0) ? 1 : n * factorial(n - 1);
}
int main() {
    
    
    char test[factorial(3)];
    std::cout << factorial(7) << '\n';
}
//c++14的实例 无此要求
#include <iostream>
#include <utility>
constexpr long long factorial(int n) {
    
    
    long long result = 1;
    for (int i = 1; i <= n; ++i) {
    
    
        result *= i;
    }
    return result;
}
constexpr long long factorial1(long long n) {
    
    
    if (n == 0)
        return 1;
    else
        return n * factorial(n - 1);
}
int main() {
    
     std::cout << factorial(9) << "  "  <<factorial1(9) << std::endl; }

Consider

	enum Flags {
    
     good=0, fail=1, bad=2, eof=4 };
	constexpr int operator|(Flags f1, Flags f2) {
    
     return Flags(int(f1)|int(f2)); }
	void f(Flags x)
	{
    
    
		switch (x) {
    
    
		case bad:         /* ... */ break;
		case eof:         /* ... */ break;
		case bad|eof:     /* ... */ break;
		default:          /* ... */ break;
		}
	}
  • 除了能够在编译时计算表达式之外,我们还希望能够要求在编译时计算表达式; 变量定义前的constexpr就是这样做的(并蕴含const) :
	constexpr int x1 = bad|eof;	// ok

	void f(Flags f3)
	{
    
    
		constexpr int x2 = bad|f3;	// error: can't evaluate at compile time
		int x3 = bad|f3;		// ok
	}

通常,我们希望对全局对象或名称空间对象(通常是我们希望放在只读存储中的对象)提供编译时求值保证。

修饰构造函数

要从用户定义的类型构造常量表达式数据值,可以使用constexpr声明构造函数。constexpr 构造函数的函数体只能包含声明和 null 语句,不能像 constexpr 函数那样声明变量或定义类型。必须存在参数值,以便在参数替换之后,使用常量表达式初始化类的成员。这些类型的析构函数必须是微不足道的。

函数体不是 =delete;constexpr 构造函数必须满足下列额外要求:

  • 对于类或结构体的构造函数,每个子对象和每个非变体非静态数据成员必须被初始化。如果类是联合体式的类,那么对于它的每个非空匿名联合体成员,必须恰好有一个变体成员被初始化 (C++20 前)
  • 对于非空联合体的构造函数,恰好有一个非静态数据成员被初始化 (C++20 前)
  • 每个被选用于初始化非静态成员和基类的构造函数必须是 constexpr 构造函数。
    constexpr 修饰类的构造函数时,要求该构造函数的函数体必须为空,且采用初始化列表的方式为各个成员赋值时,必须使用常量表达式。
    除了声明为=delete以外,constexpr构造函数的函数体一般为空,使用初始化列表或者其他的constexpr构造函数初始化所有数据成员。
    一个 constexpr构造函数 是隐式内联的。
struct Point{
    
    
    constexpr Point(int _x, int _y)
        :x(_x),y(_y){
    
    }
    constexpr Point()
        :Point(0,0){
    
    }
    int x;
    int y;
};

constexpr Point pt = {
    
    10, 10};

对于一个带有任何 constexpr构造函数的类型,复制建构子通常也应该定义为 constexpr构造函数,以允许该类型的对象从 constexpr函数中通过值返回。类的任何成员函数,比如复制构造函数、运算符重载等,只要满足 constexpr函数的要求,就可以声明为 constexpr。这允许编译器在编译时复制对象,对它们执行操作,等等。

使用constexpr构造函数的示例如下:

  • 例1
#include <iostream>

using namespace std;

struct BASE {
    
    };

struct B2 {
    
    
    int i;
};

// NL is a non-literal type.
struct NL {
    
    
    virtual ~NL() {
    
    }
};

int i = 11;

struct D1 : public BASE {
    
    
    // OK, BASE的隐式默认构造函数是一个constexpr构造函数。
    constexpr D1() : BASE(), mem(55) {
    
    }

    // OK, BASE的隐式复制构造函数是一个constexpr构造函数。
    constexpr D1(const D1& d) : BASE(d), mem(55) {
    
    }

    // OK, 所有引用类型都是文字类型。
    constexpr D1(NL& n) : BASE(), mem(55) {
    
    }

    // 转换运算符不是constexpr.
    operator int() const {
    
     return 55; }

   private:
    int mem;
};

// 不能是虚继承
//  struct D2 : virtual BASE {
    
    
//    //error, D2 must not have virtual base class.
//    constexpr D2() : BASE(), mem(55) { }

// private:
//   int mem;
// };

struct D3 : B2 {
    
    
    // error, D3 必须不能包含try语句块
    //   constexpr D3(int) try : B2(), mem(55) { } catch(int) { }

    // error: 'constexpr' 构造函数内不能包含任何语句
    //   constexpr D3(char) : B2(), mem(55) { mem = 55; }

    // error, initializer for mem is not a constant expression.
    constexpr D3(double) : B2(), mem(i) {
    
    }

    // error, 隐式转换不是一个 constexpr.
    //   constexpr D3(const D1 &d) : B2(), mem(d) { }

    // error: invalid type for parameter 1 of 'constexpr' function 'constexpr D3::D3(NL)'
    //   constexpr D3(NL) : B2(), mem(55) { }

   private:
    int mem;
};

  • 例2
int main() {
    
    
    constexpr D3 d3instance(100);
    cout << " D3 i " << d3instance.i;

    return 0;
}
	struct Point {
    
    
		int x,y;
		constexpr Point(int xx, int yy) : x(xx), y(yy) {
    
     }
	};
	constexpr Point origo(0,0);
	constexpr int z = origo.x;
	constexpr Point a[] = {
    
    Point(0,0), Point(1,1), Point(2,2) };
	constexpr int x = a[1].x;	// x becomes 1
constexpr int f1();  // OK, function declaration
int f1() {
    
               // Error, the constexpr specifier is missing
  return 55;
}     

修饰模板函数

C++11 语法中,constexpr 可以修饰模板函数,但由于模板中类型的不确定性,因此模板函数实例化后的函数是否符合常量表达式函数的要求也是不确定的
示例1 :

#include <iostream>

using namespace std;

template <typename Type>
constexpr Type max111(Type a, Type b) {
    
    
    return a < b ? b : a;
}

int main() {
    
    
    int maxv = max111(11, 5);
    cout << maxv << endl;
    string maxv1 = max111("12", "22");
    cout << maxv1 << endl;
    return 0;
}

汇编代码如下:
在这里插入图片描述

示例2:
编译时是一样的,但是运行时确是两个不同的函数调用。

#include <iostream>

using namespace std;

template <int i>
auto f() {
    
    
    if constexpr (i == 0)
        return 10;
    else
        return std::string("hello");
}

int main() {
    
    
    cout << f<0>() << endl;  // 10
    cout << f<1>() << endl;  // hello
}

编译输出如下:

#include <iostream>

using namespace std;

template<int i>
auto f()
{
    
    
  if constexpr(i == 0) {
    
    
    return 10;
  } else /* constexpr */ {
    
    
    return std::basic_string<char, std::char_traits<char>, std::allocator<char> >(std::basic_string<char, std::char_traits<char>, std::allocator<char> >("hello", std::allocator<char>()));
  } 
  
}
#ifdef INSIGHTS_USE_TEMPLATE
template<>
int f<0>()
{
    
    
  if constexpr(true) {
    
    
    return 10;
  } 
  
}
#endif
#ifdef INSIGHTS_USE_TEMPLATE
template<>
std::basic_string<char, std::char_traits<char>, std::allocator<char> > f<1>()
{
    
    
  if constexpr(false) {
    
    
  } else /* constexpr */ {
    
    
    return std::basic_string<char, std::char_traits<char>, std::allocator<char> >(std::basic_string<char, std::char_traits<char>, std::allocator<char> >("hello", std::allocator<char>()));
  } 
  
}
#endif
int main()
{
    
    
  std::cout.operator<<(f<0>()).operator<<(std::endl);
  std::operator<<(std::cout, f<1>()).operator<<(std::endl);
  return 0;
}

constexprLambdas (c++17)

说明

constexpr 显式地指定函数调用运算符或任何给定的运算符模板特化是一个 constexpr 函数。当这个说明符不存在时,如果函数调用操作符或任何给定的操作符模板特化恰好满足所有 constexpr 函数要求,那么它将是 constexpr

using F = ret(*)(params);
constexpr operator F() const noexcept;

template<template-params> using fptr_t = /*see below*/;
template<template-params>
constexpr operator fptr_t<template-params>() const noexcept;

只有当lambda表达式的捕获列表为空时,才定义这个用户定义的转换函数。它是闭包对象的一个publicconstexpr(自 C++17 起)非虚的、非显式的、 const noexcept 成员函数。

使用

  1. 当在常量表达式中允许初始化它捕获或引入的每个数据成员时,可以将 lambda 表达式声明为 constexpr 或在常量表达式中使用。
#include <iostream>
#include <numeric>

using namespace std;

constexpr int Increment(int n) {
    
    
    return [n] {
    
     return n + 1; }();
}
int main() {
    
    
    int y = 32;
    auto answer = [&y]() {
    
    
        int x = 10;
        return y + x;
    };
    constexpr int incr = Increment(100);
    cout << "Increment = " << incr; //101
    return 0;
}
  1. 如果 lambda 的结果满足 constexpr 函数的要求,那么它就是隐式的 constexpr
#include <iostream>
#include <vector>
using namespace std;
int main() {
    
    
    std::vector<int> c = {
    
    1, 2, 3, 4, 5, 6, 7};
    auto answer = [](int n) {
    
     return 32 + n; };
    constexpr int response = answer(10);
    cout << " response = " << response << endl; // response = 42
}

汇编代码如下:

        mov     DWORD PTR [rbp-80], 1
        mov     DWORD PTR [rbp-76], 2
        mov     DWORD PTR [rbp-72], 3
        mov     DWORD PTR [rbp-68], 4
        mov     DWORD PTR [rbp-64], 5
        mov     DWORD PTR [rbp-60], 6
        mov     DWORD PTR [rbp-56], 7
        lea     rax, [rbp-80]
        mov     r12, rax
        mov     r13d, 7
        lea     rax, [rbp-37]
        mov     rdi, rax
        call    std::allocator<int>::allocator() [complete object constructor]
        lea     rdx, [rbp-37]
        mov     rsi, r12
        mov     rdi, r13
        mov     rcx, r12
        mov     rbx, r13
        mov     rdi, rbx
        lea     rax, [rbp-112]
        mov     rcx, rdx
        mov     rdx, rdi
        mov     rdi, rax
        call    std::vector<int, std::allocator<int> >::vector(std::initializer_list<int>, std::allocator<int> const&) [complete object constructor]
        lea     rax, [rbp-37]
        mov     rdi, rax
        call    std::allocator<int>::~allocator() [complete object destructor]
        mov     DWORD PTR [rbp-36], 42
  1. 如果一个lambda是隐式或显式的 constexpr,并将其转换为函数指针,那么得到的函数也是 constexpr
#include <iostream>
#include <vector>

using namespace std;
auto Increment = [](int n) {
    
     return n + 1; };

constexpr int (*inc)(int) = Increment;

int main() {
    
    
    constexpr int response = inc(22);
    cout << " response = " << response << endl;
}

汇编代码如下:

Increment::{
    
    lambda(int)#1}::operator()(int) const:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        mov     DWORD PTR [rbp-12], esi
        mov     eax, DWORD PTR [rbp-12]
        add     eax, 1
        pop     rbp
        ret
Increment::{
    
    lambda(int)#1}::_FUN(int):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        mov     esi, eax
        mov     edi, 0
        call    Increment::{
    
    lambda(int)#1}::operator()(int) const
        leave
        ret
Increment:
        .zero   1
.LC0:
        .string " response = "
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], 23
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:_ZSt4cout
        call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
        mov     esi, 23
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
        mov     esi, OFFSET FLAT:_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
        mov     rdi, rax
        call    std::basic_ostream<char, std::char_traits<char> >::operator<<(std::basic_ostream<char, std::char_traits<char> >& (*)(std::basic_ostream<char, std::char_traits<char> >&))
        mov     eax, 0
        leave
        ret

c++11中constexpr示例

const int array_size1 (int x) {
    
    
  return x+1;
}
// Error, constant expression required in array declaration
int array[array_size1(10)];    

constexpr int array_size2 (int x) {
    
     
  return x+1; 
} 
// OK, constexpr functions can be evaluated at compile time
// and used in contexts that require constant expressions. 
int array[array_size2(10)];     

struct S {
    
    
  S() {
    
     }
  constexpr S(int) {
    
     }
  constexpr virtual int f() {
    
      // Error, f must not be virtual.
    return 55;
  }       
};

struct NL {
    
    
  ~NL() {
    
     }  // The user-provided destructor (even if it is trivial) 
             // makes the type a non-literal type.
};

constexpr NL f1() {
    
      // Error, return type of f1 must be a literal type.
  return NL();
}       

constexpr int f2(NL) {
    
      // Error, the parameter type NL is not a literal type.
  return 55; 
}                 

constexpr S f3() {
    
    
  return S();
}

enum {
    
     val = f3() };  // Error, initialization of the return value in f3()
                      // uses a non-constexpr constructor.

constexpr void f4(int x) {
    
      // Error, return type should not be void.
  return;
}

constexpr int f5(int x) {
    
     // Error, function body contains more than
  if (x<0)                // return statement.
    x = -x;
  return x;
}

'C++'一直有常量表达式的概念。这些表达式(如3 + 4)在编译时和运行时总是产生相同的结果。常数表达式是编译器的优化机会,编译器经常在编译时执行常数表达式,并将结果硬编码到程序中。此外,在一些地方,C + + 规范要求使用常量表达式。定义数组需要常量表达式,枚举数值必须是常量表达式。

但是,常量表达式永远不允许包含函数调用或对象构造函数。这样一段简单的代码是无效的:

int get_five() {
    
    return 5;}
int some_value[get_five() + 7]; // Create an array of 12 integers. Ill-formed C++

这在 C++03中无效,因为 get_five() + 7不是常量表达式。C++03编译器无法知道 get_five() 在运行时是否是常量。理论上,这个函数可以影响一个全局变量,调用其他非运行时常量函数等等。
C++11引入了关键字 constexpr,它允许用户保证函数或对象构造函数是编译时常量。上述例子可重写如下:

constexpr int get_five() {
    
    return 5;}
int some_value[get_five() + 7]; // Create an array of 12 integers. Valid C++11

这允许编译器理解并验证 get_five() 是一个编译时常量。

constexprconst 区别

  1. 目的不同。
  • constexpr 主要用于优化,而 const 实际上用于const对象,例如 Pi 的值。 它们都可以应用于成员方法。 将成员方法设为const以确保方法中没有意外更改。
  • 使用 constexpr 的想法是在编译时计算表达式 ,以便在运行代码时节省时间。
    在这里插入图片描述
  • const 只能与非静态成员函数一起使用,而 constexpr 可以与成员函数和非成员函数一起使用,即使是构造函数,但条件是参数和返回类型必须是文字类型。
  1. 功能不同:constexpr并不是const的通用替代品(反之亦然):
  • const 的主要功能是表达不通过接口修改对象的思想(即使对象很可能通过其他接口修改)。声明一个对象常量恰好为编译器提供了极好的优化机会。特别是,如果一个对象被声明为const并且它的地址没有被获取,编译器通常能够在编译时计算它的初始化器(尽管这并不能保证) ,并将该对象保留在其表中,而不是发送到生成的代码中。
  • constexpr 的主要功能是扩展可以在编译时计算的内容的范围,使这种计算类型安全constexpr声明的对象的初始值在编译时计算; 它们基本上是保存在编译器表中的值,只有在需要时才发送到生成的代码中。

应用于对象时的基本区别是:

  • const 将对象声明为常量。这意味着保证一旦初始化,该对象的值不会改变,编译器可以利用这一事实进行优化。它还有助于防止程序员编写代码来修改初始化后不应该修改的对象。
  • constexpr 声明了一个对象,该对象适用于标准语句所称的常量表达式。。但是请注意,constexpr 并不是这样做的唯一方法。
  • const变量和 constexpr 变量之间的主要区别是 const 变量的初始化可以推迟到运行时constexpr 变量必须在编译时初始化。所有的 constexpr 变量都是 const

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

  • const 只能用于非静态成员函数,而不能用于一般函数。它保证成员函数不会修改任何非静态数据成员(可变数据成员除外,无论如何都可以修改)。
  • constexpr既可以用于成员函数,也可以用于非成员函数,以及构造函数。它声明该函数适合用于常量表达式。只有当函数满足某些条件时,编译器才会接受它,最重要的是:
    • 函数体必须是非虚的并且非常简单:除了typedefsstatic assert之外,只允许一个返回语句。对于构造函数,只允许初始化列表、typedefs和静态断言。(= default= delete也是允许的。)
    • c++14中,规则更加宽松,从那时起在constexpr函数中允许的:asm声明,goto语句,带有casedefault以外的标签的语句,try-block,非文字类型变量的定义,静态或线程存储持续时间变量的定义,不执行初始化的变量的定义。
    • 参数和返回类型必须是文字类型(即,通常来说,非常简单的类型,通常是标量或聚合)

const可以由任何函数初始化,但 constexpr由非 constexpr(未用 constexpr 或非constexpr表达式标记的函数)初始化将生成编译器错误。

const int function5(){
    
     return 100;} // 返回一个常数
constexpr int function8(){
    
    return 100;}
int main(){
    
    
    constexpr int temp5 = function5();// error: call to non-'constexpr' function 'const int function5()'
    constexpr int temp6 = funtion8(); //ok
    const int temp7 = function8();    //ok
    return 0;
}
#include <iostream>

template <typename T>
void f(T t) {
    
    
    static_assert(t == 1, "");
}
constexpr int one = 1;
int main() {
    
    
    f(one);
    return 0;
}

gcc编译输出如下:

<source>: In instantiation of 'void f(T) [with T = int]':
<source>:10:6:   required from here
<source>:5:21: error: non-constant condition for static assertion
    5 |     static_assert(t == 1, "");
      |                   ~~^~~~
<source>:5:21: error: 't' is not a constant expression

f的主体内部,t 不是一个常量表达式,因此它不能用作 static _ assert 的操作数(进行编译时断言检查的static_assert需要一个constexpr的常量bool表达式)。原因是编译器根本无法生成这样的函数

template <typename T>
void f1(constexpr T t) {
    
    
  static_assert(t == 1, "");
}
int main() {
    
    
		constexpr int one = 1;
    f1(one);
    return 0;
}

gcc编译错误输出如下:

<source>:4:9: error: a parameter cannot be declared 'constexpr'
    4 | void f1(constexpr T t) {
    
    
      |         ^~~~~~~~~
<source>: In function 'int main()':
<source>:9:8: error: 'one' was not declared in this scope
    9 |     f1(one);
      |        ^~~

实参不是常量表达式这一事实意味着我们不能将其作为非类型模板形参、作为数组绑定形参、在static_assert或其他需要常量表达式的对象中使用。
为了通过参数传递来保持constexpr特性,必须将constexpr值编码为一个类型,然后将该类型的一个不一定是constexpr的对象传递给函数。该函数必须是模板,然后可以访问编码在该类型中的constexpr值。

constexpr int sqrt(int i) {
    
    
  if (i < 0) throw "i should be non-negative";
  return 111;
}
constexpr int two = sqrt(4); // ok: did not attempt to throw
constexpr int error = sqrt(-4); // error: can't throw in a constant expression

gcc编译输出如下:

<source>:14:27:   in 'constexpr' expansion of 'sqrt(-4)'
<source>:10:14: error: expression '<throw-expression>' is not a constant expression
   10 |   if (i < 0) throw "i should be non-negative";
#include <iostream>

template <typename T>
constexpr int f(T& n, bool touch_n) {
    
    
    if (touch_n) n + 1;
    return 1;
}
int main() {
    
    
    int n = 0;
    constexpr int i = f(n, false);  // ok
    constexpr int j = f(n, true);   // error
    return 0;
}

gccc++11标准中编译输出如下:

<source>: In function 'int main()':
<source>:11:24: error: 'constexpr int f(T&, bool) [with T = int]' called in a constant expression
   11 |     constexpr int i = f(n, false);  // ok
      |                       ~^~~~~~~~~~
<source>:4:15: note: 'constexpr int f(T&, bool) [with T = int]' is not usable as a 'constexpr' function because:
    4 | constexpr int f(T& n, bool touch_n) {
    
    
      |               ^
<source>:12:24: error: 'constexpr int f(T&, bool) [with T = int]' called in a constant expression
   12 |     constexpr int j = f(n, true);   // error
      |                       ~^~~~~~~~~
#include <iostream>
template <typename T>
constexpr int f(T n) {
    
    
    return 1;
}
int main() {
    
    
    int n = 0;
    constexpr int i = f(n);
    return 0;
}

与我们初始场景的唯一区别是,f现在通过值而不是引用接受参数。然而,这使世界大不相同。实际上,我们现在要求编译器复制n,并将这个拷贝传递给f。然而,n不是constexpr,所以它的值只有在运行时才知道。编译器如何复制(在编译时)一个变量的值只有在运行时才知道?当然不能。

<source>: In function 'int main()':
<source>:10:26: error: the value of 'n' is not usable in a constant expression
   10 |     constexpr int i = f(n);
      |                          ^
<source>:9:9: note: 'int n' is not const
    9 |     int n = 0;
      |      

后记

  • 不要尝试让所有的函数声明位constexpr。大多数计算最好在运行时完成。
  • 任何可能最终依赖于高级运行时配置或业务逻辑的API都不应该使用 constexpr。编译器无法对这种定制进行评估,任何依赖于该 API constexpr函数都必须重构或删除 constexpr
  • 如果在需要常量的地方调用非 constexpr函数,编译器会出错。

参考

[1] 常量表达式
[2] What’s the difference between constexpr and const?
[3] Appendix I: Advanced constexpr
[4] When to Use const vs constexpr in C++
[5] constexpr 说明符(C++11 起)
[6] CppCoreGuidelines F.4
[7] constexpr-cpp.md

猜你喜欢

转载自blog.csdn.net/MMTS_yang/article/details/126344277