当前位置: 首页 > news >正文

【C++编程基础-关键字】:constexpr和const

引言:constexpr 是什么

在 C++ 编程的领域中,随着 C++11 标准的推出,一系列强大而实用的新特性如璀璨星辰般照亮了开发者的道路,其中constexpr关键字便是一颗耀眼的明星。constexpr,全称为 “constant expression”,即常量表达式,它的出现为 C++ 语言带来了编译期计算的强大能力,使得程序在编译阶段就能完成一些原本需要在运行时进行的计算,极大地提升了程序的性能和效率 。

从本质上讲,constexpr用于声明那些在编译时就能确定其值的常量或函数。这意味着,当我们使用constexpr修饰一个变量时,该变量的值必须是一个常量表达式,编译器会在编译阶段就计算出它的值并将其视为一个常量;当constexpr用于修饰函数时,这个函数必须满足一定的条件,以便编译器能够在编译时对其进行求值,从而可以在需要常量表达式的地方使用这个函数调用 。例如,在定义数组大小时,如果使用普通变量,编译器会报错,因为数组大小必须是常量表达式;但如果使用constexpr修饰的常量,就可以顺利通过编译,这充分体现了constexpr在编译期求值的特性。在后续的内容中,我们将深入探究constexpr在变量、函数以及类构造函数等方面的具体应用,揭开它神秘而强大的面纱。

一、constexpr 基础用法

(一)定义编译期常量

在 C++ 中,constexpr用于定义编译期常量,这些常量的值在编译阶段就已经确定,并且不能被修改。这与普通的const常量有所不同,虽然const常量也表示其值不会改变,但const常量不一定能在编译期求值 。例如:

const int a = getValue(); // 这里的a虽然是const常量,但值在运行时才确定

constexpr int b = 10; // b是constexpr常量,值在编译期确定

上述代码中,getValue函数返回的值在运行时才能得到,所以const修饰的a的值在运行时才确定;而constexpr修饰的b,由于初始值是编译期常量10,所以b的值在编译期就确定了 。在实际应用中,constexpr常量常用于需要常量表达式的场景,比如定义数组的大小:

constexpr int arraySize = 5;

int numbers[arraySize]; // 合法,arraySize是编译期常量

如果将arraySize定义为普通的const变量,且其值在运行时确定,那么在定义数组时就会报错,因为数组大小必须是常量表达式 。

(二)constexpr 函数

constexpr函数是指能用于常量表达式的函数,它的返回值在编译期就可以确定 。要成为constexpr函数,必须满足一些条件:

  1. 返回值类型和参数类型:返回类型和所有形参的类型必须是字面类型(literal type),例如基本数据类型(如int、double等)、枚举类型、指针类型以及满足特定条件的自定义类类型 。
  2. 函数体结构:在 C++11 标准中,函数体只能包含一条return语句;从 C++14 开始,放宽了限制,允许函数体包含循环、局部变量等更多复杂结构,但这些操作也必须能在编译期完成 。
  3. 不能有副作用:函数中不能包含修改全局变量、进行 I/O 操作等可能产生副作用的代码 。

下面是一个简单的constexpr函数示例,用于计算两个整数的和:

constexpr int add(int a, int b) {

return a + b;

}

constexpr int result = add(3, 5); // 在编译期计算结果,result的值为8

在这个例子中,add函数被声明为constexpr函数,当使用常量表达式作为参数调用它时,如add(3, 5),编译器会在编译期计算出结果,并将其赋值给result 。这在一些需要在编译期进行计算的场景中非常有用,比如计算数组的初始化值、模板参数的计算等 。再比如,计算一个数的平方:

constexpr int square(int num) {

return num * num;

}

constexpr int squareResult = square(4); // 编译期计算出squareResult的值为16

通过constexpr函数,我们可以将一些简单的计算从运行时提前到编译期,提高程序的运行效率,同时也增强了代码的可读性和可维护性 。

(三)constexpr构造函数

构造函数不能说const,但字面值常量类的构造函数可以是constexpr。

constexpr构造函数必须有一个空的函数体,即所有成员变量的初始化都放到初始化列表中。对象调用的成员函数必须使用constexpr。

  1. 字面值常量类必须至少提供一个constexpr构造函数;
  2. constexpr构造函数可以声明成=default的形式、或者删除函数的形式。否则,constexpr构造函数必须符合构造函数的要求又符合constexpr函数的要求(拥有的唯一可执行语句就是返回语句);
  3. constexpr构造函数必须初始化所有的数据成员;
  4. constexpr构造函数用于生成constexpr对象;
class Debug {
private:
    bool hw;
    bool io;
    bool other;
public:
    constexpr Debug(bool b = true) :hw(b), io(b), other(b) {}
    constexpr Debug(bool h, bool i, bool o) : hw(h), io(i), other(o) {}
    constexpr bool any() { return hw || io || other; }
    void set_io(bool b) { io = b; }
    void set_hw(bool b) { hw = b; }
    void set_other(bool b) { other = b; }
};
 
int main()
{
    constexpr Debug io_sub(false, true, false);  //调试IO
    constexpr Debug prod(false);  //无调试
    if (io_sub.any())  //等价于if(true)
        cerr << "print appropriate error messages;" << endl;
    if(prod.any()) //等价于if(false)
        cerr << "print an error message;" << endl;
    return 0;
}

二、constexpr 与 const 的区别

在 C++ 中,const和constexpr虽然都与常量相关,但它们之间存在着一些重要的区别,这些区别体现在初始化时机、常量性质以及应用场景等多个方面 。

(一)初始化时机

const变量的初始化可以推迟到运行时。这意味着const变量的值既可以在编译时通过常量表达式进行初始化,也可以在运行时通过函数返回值或其他运行时才确定的表达式来初始化 。例如:

const int a = getValue(); // 假设getValue是一个返回int值的函数,a在运行时初始化

const int b = 10; // b在编译时初始化,10是常量表达式

在上述代码中,a的值取决于getValue函数的返回结果,而这个结果在运行时才能确定;b则因为初始值是常量表达式10,所以在编译时就完成了初始化 。

相比之下,constexpr变量必须在编译时进行初始化,其初始值必须是一个常量表达式。编译器会在编译阶段对constexpr变量的初始化表达式进行求值,如果表达式不是常量表达式,编译器会报错 。例如:

constexpr int c = 10; // 正确,10是常量表达式

int x = 5;

//constexpr int d = x; // 错误,x不是常量表达式,不能用于初始化constexpr变量

这里的c是一个合法的constexpr变量,因为它的初始值10是常量表达式;而尝试用变量x初始化constexpr变量d是错误的,因为x的值在运行时才确定,不是常量表达式 。

(二)常量性质

const主要用于表达 “对接口的写权限控制”,它并未严格区分编译期常量和运行期常量,仅仅保证在运行时该变量的值不会被直接修改 。虽然在很多情况下,const变量的值在初始化后就不会再改变,但从本质上讲,它没有对变量在编译期求值做出严格要求 。例如:

int value = 20;

const int num = value;

// 这里num是const变量,虽然值在运行时确定,但后续不能通过num直接修改其值

在这个例子中,num被声明为const变量,它的值在运行时由value确定,之后不能通过num来修改这个值,但这并不意味着num是编译期常量 。

constexpr则明确限定变量为编译期常量,其值在编译阶段就已经完全确定,并且在整个程序运行过程中不会发生变化 。这使得constexpr变量可以用于一些对编译期常量有严格要求的场景,比如定义数组大小、作为模板参数等 。例如:

constexpr int arraySize = 5;

int numbers[arraySize]; // 合法,arraySize是编译期常量

由于arraySize是constexpr变量,其值在编译时就确定为5,所以可以用它来定义数组的大小 。如果arraySize不是编译期常量,编译器会报错,因为数组大小必须是常量表达式 。

(三)应用场景差异

const的应用场景较为广泛,常用于表示运行时不可修改的值 。例如,在函数参数传递中,使用const修饰参数可以防止函数内部对参数进行意外修改,保护函数外部传入的数据 。在定义指针或引用时,const可以修饰指针本身或指针所指向的值,或者同时修饰两者,以控制对指针和所指对象的修改权限 。例如:

void func(const int& param) {

// 这里不能通过param修改外部传入的值

}

const int* ptr = &a; // 指向常量的指针,不能通过ptr修改所指的值

int* const cptr = &a; // 常量指针,不能修改指针本身的指向

在上述代码中,func函数的参数param被声明为const引用,函数内部无法通过param修改外部传入的值;ptr是指向常量的指针,不能通过ptr修改所指的a的值;cptr是常量指针,不能修改cptr本身的指向 。

constexpr则主要应用于需要编译期常量的场景 。例如,在定义数组大小时,数组的大小必须是常量表达式,此时使用constexpr变量可以满足这一要求 。在模板编程中,模板参数也常常需要是编译期常量,constexpr可以用于修饰模板参数,使得模板在编译期能够根据常量参数进行实例化 。此外,constexpr函数可以在编译期进行计算,这在一些需要在编译期完成复杂计算的场景中非常有用,比如计算数学常量、生成查找表等 。例如:

constexpr int factorial(int n) {

return n <= 1? 1 : n * factorial(n - 1);

}

constexpr int result = factorial(5); // 在编译期计算5的阶乘

这里的factorial函数被声明为constexpr函数,当使用常量表达式5作为参数调用它时,编译器会在编译期计算出5的阶乘,并将结果赋值给result 。

三、constexpr 的进阶应用

(一)在类中的使用

在类中,constexpr有着独特而强大的应用,主要体现在修饰构造函数和成员函数上 。当constexpr修饰类的构造函数时,这个构造函数必须满足特定的条件:

  • 函数体结构:函数体通常为空,采用初始化列表的方式为各个成员变量赋值 。这是因为constexpr构造函数的目的是在编译时创建对象,所以其初始化操作必须能在编译期完成 。
  • 初始化表达式:为成员变量赋值的表达式必须是常量表达式 。只有这样,编译器才能在编译阶段确定对象的初始状态 。

下面以一个简单的Point类为例,展示constexpr在类中的应用:

class Point {

public:

constexpr Point(double xVal, double yVal) : x(xVal), y(yVal) {}

constexpr double getX() const { return x; }

constexpr double getY() const { return y; }

private:

double x, y;

};

constexpr Point origin(0, 0);

在这个例子中,Point类的构造函数被声明为constexpr,它使用初始化列表为x和y成员变量赋值,并且赋值表达式0是常量表达式 。因此,我们可以在编译时创建Point类的对象,如constexpr Point origin(0, 0); 。这样创建的origin对象在编译期就已经确定了其坐标值,并且可以在需要常量表达式的地方使用,例如作为模板参数传递给模板函数或模板类 。

constexpr修饰的成员函数也必须满足一定条件,类似于constexpr函数的要求,即返回值类型和参数类型必须是字面类型,函数体中的操作必须能在编译时完成,且不能有副作用 。constexpr成员函数使得我们可以在编译时调用类的成员函数来获取常量值,进一步增强了类在编译期的计算能力和使用的灵活性 。

(二)constexpr if(C++17)

C++17 引入的constexpr if特性为编译期条件判断带来了极大的便利 。它允许在编译时根据条件进行分支,使得代码能够根据不同的编译期条件生成不同的代码,而不是在运行时进行条件判断 。这在模板编程中尤为有用,可以避免不必要的模板实例化,提高编译效率 。

constexpr if的语法形式为if constexpr (condition) { /* 条件为真时的代码 */ } else { /* 条件为假时的代码 */ },其中condition必须是一个在编译时可以求值的常量表达式,其结果为true或false 。如果condition为true,则编译器会选择if分支的代码进行编译,忽略else分支(如果存在);反之,如果condition为false,则编译器会选择else分支的代码进行编译,忽略if分支 。

下面结合一个模板编程的示例来展示constexpr if的用法和优势:

#include <iostream>

#include <type_traits>

template <typename T>

void printTypeInfo(T value) {

if constexpr (std::is_integral_v<T>) {

std::cout << "The value is an integral type: " << value << std::endl;

} else if constexpr (std::is_floating_point_v<T>) {

std::cout << "The value is a floating - point type: " << value << std::endl;

} else {

std::cout << "The type is not an integral or floating - point type." << std::endl;

}

}

int main() {

printTypeInfo(10);

printTypeInfo(3.14);

printTypeInfo("Hello");

return 0;

}

在上述代码中,printTypeInfo是一个模板函数,它使用constexpr if根据模板参数T的类型进行不同的操作 。std::is_integral_v<T>和std::is_floating_point_v<T>是 C++ 标准库中的类型特征模板,用于判断T是否为整数类型或浮点数类型 。当调用printTypeInfo(10)时,T被推导为int,std::is_integral_v<T>为true,编译器会选择if分支的代码进行编译,输出The value is an integral type: 10;当调用printTypeInfo(3.14)时,T被推导为double,std::is_floating_point_v<T>为true,编译器会选择else if分支的代码进行编译;当调用printTypeInfo("Hello")时,T被推导为const char*,两个条件都为false,编译器会选择else分支的代码进行编译 。通过constexpr if,我们可以在编译时根据不同的类型条件生成不同的代码,避免了运行时的条件判断开销,同时也增强了代码的可读性和可维护性 。

(三)constexpr lambda 表达式(C++17)

C++17 引入的constexpr lambda表达式允许在编译时计算 lambda 表达式的结果,这为编译期计算带来了更多的灵活性和便利性 。传统的 lambda 表达式是在运行时求值的,而constexpr lambda表达式则突破了这一限制,使得一些计算可以在编译阶段完成 。

constexpr lambda表达式的语法与普通 lambda 表达式类似,只是在前面加上了constexpr关键字 。例如:

constexpr auto square = [](int n) { return n * n; };

constexpr int result = square(5);

在这个例子中,square是一个constexpr lambda表达式,它接受一个整数参数n,返回n的平方 。由于它被声明为constexpr,当使用常量表达式5作为参数调用它时,编译器会在编译期计算出结果,并将其赋值给result 。

下面再通过一个更复杂的示例来展示constexpr lambda表达式的用法和效果:

#include <iostream>

constexpr auto add = [](int a, int b) { return a + b; };

constexpr auto multiply = [](int a, int b) { return a * b; };

template <typename Func, int a, int b>

constexpr int applyFunc() {

return Func(a, b);

}

int main() {

constexpr int sum = applyFunc<decltype(add), 3, 5>();

constexpr int product = applyFunc<decltype(multiply), 3, 5>();

std::cout << "Sum: " << sum << std::endl;

std::cout << "Product: " << product << std::endl;

return 0;

}

在上述代码中,定义了两个constexpr lambda表达式add和multiply,分别用于计算两个整数的和与积 。applyFunc是一个模板函数,它接受一个函数类型Func和两个整数模板参数a和b,并在编译时调用Func函数对a和b进行操作 。通过applyFunc模板函数,我们可以在编译时应用不同的constexpr lambda表达式进行计算,如applyFunc<decltype(add), 3, 5>()在编译期计算3 + 5的结果,applyFunc<decltype(multiply), 3, 5>()在编译期计算3 * 5的结果 。这种方式使得我们可以在编译期灵活地组合和使用不同的计算逻辑,提高了代码的编译期计算能力和灵活性 。

(四)constexpr 的动态分配(C++20)

在 C++20 之前,constexpr中是不允许使用动态内存分配的,因为动态内存分配通常涉及到运行时的系统调用和资源管理,这与constexpr在编译时求值的特性相冲突 。然而,C++20 对constexpr进行了重大扩展,允许在constexpr中使用动态内存分配,这为编译期的复杂数据结构构建和算法实现提供了更大的可能性 。

C++20 允许在constexpr函数中使用new和delete操作符来进行动态内存分配和释放 。这意味着我们可以在编译时创建和管理动态数组、链表等动态数据结构 。例如,下面的代码展示了如何在constexpr函数中创建一个动态数组:

#include <iostream>

constexpr int* createArray(int size) {

int* arr = new int[size];

for (int i = 0; i < size; ++i) {

arr[i] = i * i;

}

return arr;

}

int main() {

constexpr int* array = createArray(5);

for (int i = 0; i < 5; ++i) {

std::cout << array[i] << " ";

}

delete[] array;

return 0;

}

在这个例子中,createArray函数被声明为constexpr,它在编译时使用new创建一个大小为size的动态数组,并对数组元素进行初始化 。main函数中调用createArray(5),在编译期创建一个包含 5 个元素的动态数组,每个元素的值为其索引的平方 。最后,通过delete[]释放动态分配的内存 。

这种在constexpr中使用动态内存分配的特性,使得我们能够在编译期构建更复杂的数据结构,实现更强大的编译期算法 。例如,在编译期生成查找表、构建复杂的数学模型等场景中,动态内存分配可以提供更大的灵活性和扩展性 。同时,也需要注意在constexpr中进行动态内存分配时,要确保内存的正确管理,避免内存泄漏和悬空指针等问题 。

四、使用 constexpr 的注意事项和优化建议

(一)注意事项

在使用constexpr时,有一些关键的注意事项需要牢记 。首先,constexpr函数的参数和返回值类型必须是字面值类型(literal type) 。字面值类型包括基本数据类型(如int、double、char等)、枚举类型、指针类型以及满足特定条件的自定义类类型 。如果使用了非字面值类型作为参数或返回值,编译器会报错 。例如:

class NonLiteralType {

public:

NonLiteralType() = default;

// 这里没有满足字面值类型的条件,如没有constexpr构造函数等

};

// 错误,NonLiteralType不是字面值类型

constexpr NonLiteralType createNonLiteral() {

return NonLiteralType();

}

在上述代码中,NonLiteralType类没有满足字面值类型的要求,因此createNonLiteral函数的声明是错误的,因为它返回了非字面值类型 。

其次,constexpr函数的函数体必须满足一定的规则 。在 C++11 中,函数体只能包含一条return语句;从 C++14 开始,虽然允许函数体包含循环、局部变量等更复杂的结构,但这些操作也必须能在编译时完成 。例如,在 C++11 中,下面的代码是错误的:

// C++11中错误,函数体不能包含多条语句

constexpr int add(int a, int b) {

int sum = a + b;

return sum;

}

在 C++14 及以后,上述代码是合法的,因为它符合 C++14 对constexpr函数体的扩展要求 。但如果在函数体中包含了运行时才能执行的操作,如动态内存分配(在 C++20 之前)、抛出异常等,仍然会导致编译错误 。例如:

// 错误,C++20之前constexpr函数中不能使用动态内存分配

constexpr int* createArray() {

return new int[5];

}

// 错误,constexpr函数中不能抛出异常

constexpr int divide(int a, int b) {

if (b == 0) {

throw std::runtime_error("Division by zero");

}

return a / b;

}

此外,在使用constexpr变量时,其初始化表达式必须是常量表达式 。如果使用了非常量表达式进行初始化,编译器会报错 。例如:

int value = 10;

// 错误,value不是常量表达式,不能用于初始化constexpr变量

constexpr int num = value;

(二)优化建议

为了充分发挥constexpr的优势,在编程过程中有一些优化建议可供参考 。首先,应尽可能在合适的场景中使用constexpr 。例如,在定义数组大小时,使用constexpr变量可以确保数组大小在编译时确定,提高程序的效率和安全性 。如:

constexpr int arraySize = 10;

int numbers[arraySize];

在模板编程中,constexpr也非常有用 。可以将constexpr用于模板参数,使得模板在编译期能够根据常量参数进行实例化,避免不必要的运行时计算 。例如:

template <constexpr int N>

void printValue() {

std::cout << "Value: " << N << std::endl;

}

printValue<5>();

其次,利用constexpr的编译期计算特性,可以将一些简单的计算从运行时提前到编译期 。例如,定义一个计算平方的constexpr函数:

constexpr int square(int num) {

return num * num;

}

constexpr int result = square(4); // 在编译期计算出结果

这样,在程序运行时,result的值已经在编译期确定,避免了运行时的计算开销 。同时,对于一些复杂的计算,如果可以通过constexpr在编译期完成,也应尽量将其实现为constexpr函数或表达式 。例如,计算斐波那契数列的前n项:

constexpr int fibonacci(int n) {

return n <= 1? n : fibonacci(n - 1) + fibonacci(n - 2);

}

constexpr int fib5 = fibonacci(5); // 在编译期计算出5的斐波那契数

最后,需要注意的是,虽然constexpr可以带来性能提升,但也不要过度使用 。在一些情况下,复杂的编译期计算可能会导致编译时间变长,影响开发效率 。因此,在使用constexpr时,需要根据具体的需求和场景进行权衡,确保在提升性能的同时,不会对开发过程造成过大的负担 。

五、总结与展望

constexpr关键字作为 C++ 语言进化历程中的重要成果,为开发者带来了前所未有的编译期计算能力 。它在定义编译期常量、函数以及在类中的应用等方面展现出独特的优势,使得程序能够在编译阶段就完成一些复杂的计算,从而显著提升运行时的效率 。与传统的const相比,constexpr对常量的定义和求值时机有着更严格的要求,确保了常量在编译期的确定性和不可变性,为程序的稳定性和安全性提供了有力保障 。

在进阶应用中,constexpr不断拓展边界,从 C++17 引入的constexpr if和constexpr lambda表达式,到 C++20 允许的动态分配,每一次标准的演进都为constexpr带来了新的活力和可能性 。constexpr if使得编译期条件判断成为现实,有效避免了不必要的模板实例化;constexpr lambda表达式则为编译期计算注入了更多的灵活性;而 C++20 对动态分配的支持,更是开启了在编译期构建复杂数据结构和实现强大算法的大门 。

展望未来,随着 C++ 标准的持续发展,constexpr有望在更多领域发挥关键作用 。一方面,它可能会在标准库中得到更广泛的应用,进一步提升标准库函数和数据结构的性能和类型安全性 。例如,在一些算法库函数中,利用constexpr可以在编译期完成更多的计算和预处理工作,减少运行时的开销 。另一方面,随着硬件性能的不断提升和软件开发对效率的追求,constexpr在高性能计算、嵌入式系统开发等领域的应用前景将更加广阔 。在嵌入式系统中,资源往往十分有限,constexpr能够在编译期完成大量计算,减少运行时的资源占用,从而满足嵌入式系统对高效性和稳定性的严格要求 。同时,我们也期待constexpr在语法和功能上能够进一步优化和扩展,为开发者提供更加简洁、强大的编程体验 。

http://www.dtcms.com/a/122436.html

相关文章:

  • Vue3服务端渲染实战:Nuxt3深度解析与高性能SSR架构设计
  • vLLM实战:多机多卡大模型分布式推理部署全流程指南
  • 深入探究Python的re模块及其在爬虫中的应用
  • 界面控件DevExpress WPF v25.1新功能预览 - 数据网格、报表性能增强
  • [特殊字符] Hyperlane:Rust 高性能 HTTP 服务器库,开启 Web 服务新纪元!
  • ARM裸机全集学习笔记【链接来源:向阳而生,逆风翻盘】
  • 智能家居设备
  • Ansible(5)——编写 Playbook
  • SpringMVC的请求-文件上传
  • 如何利用 Java 爬虫获取京东商品详情信息
  • scala总结与spark安装
  • 游戏引擎学习第213天
  • 【scikit-learn基础】--『预处理』之 正则化
  • JetBrains Terminal 又发布新架构,Android Studio 将再次迎来新终端
  • 21 天 Python 计划:MySQL中DML与权限管理
  • Java基础 4.9
  • 如何生成一个requestid
  • 地图服务热点追踪:创新赋能,领航出行与生活
  • Windows 下 Rust 安装全攻略(无需 Visual Studio)
  • 【力扣hot100题】(078)跳跃游戏Ⅱ
  • 用 npm list -g --depth=0 探索全局包的秘密 ✨
  • MySQL中使用索引一定有效吗?如何排查索引效果?
  • uniapp uni-collapse动态切换数据时高度不能自适应
  • 旅行世界宠物养殖合成游戏源码
  • SQL开发的智能助手:通义灵码在IntelliJ IDEA中的应用
  • 银河麒麟V10 Ollama+ShellGPT打造Shell AI助手——筑梦之路
  • 蓝桥杯 B3619 10 进制转 x 进制
  • 4.7学习总结 可变参数+集合工具类Collections+不可变集合
  • 分析一下HashMap内部是怎么实现的
  • JavaScript Date(日期)