C++之模板元编程(前置知识 constexpr)

一、顶层const和底层const

在C语言我们也遇见过这样的知识(毕竟常考嘛,算是老朋友了)

顶层const :修饰变量本身 ,变量不能被修改

底层const :修饰指向 / 引用的内容 ,内容不能被修改

大多数对象被const修饰都叫顶层const,指针被const修饰时,*左边的const叫底层const,*右边的const叫做顶层const。

注意:const修饰引用时,这个const是底层const

例子:

cpp 复制代码
int main()
{
    int i = 0;
    int* const p1 = &i;  // 顶层const 
    const int ci = 42;   // 顶层const 
    const int* p2 = &ci; // 底层const 
    const int& r = ci;   // 底层const 
    return 0;
}

二、constexpr

2.1 constexpr介绍

constexpr 是 C++11 引入、C++14/17/20 大幅增强关键字 ,全称 constant expression(常量表达式)

constexpr 用于指定常量表达式 。它允许在编译时计算表达式的值,从而提高运行时性能并增强类型安全性。(常量表达式是指值不会改变并且在编译过程中就能得到计算结果的表达式,字面值、常量表达式初始化的const对象都是常量表达式,要注意变量初始化的const对象不是常量表达式,因为变量初始化的const对象要在运行时才确定)

cpp 复制代码
int main()
{
    int x = 5;
    //const int i = x; // 错误:用变量初始化的表达式,不是常量表达式,会导致语句:constexpr int j = i报错
    const int i = 1;
    constexpr int j = i;
    return 0;
}

2.2 constexpr可以修饰的对象

之前说过constexpr 允许编译时计算表达式的值,可以提高我们的代码运行的性能

那么constexpr可以作用在哪些上呢?

2.2.1 修饰变量

constexpr可以修饰变量,但是其修饰的变量的等号右边必须是常量表达式,而且修饰constexpr修饰变量相当于顶层const

比如 constexpr int value = 常量表达式

例子:

cpp 复制代码
constexpr int c = 3;
int main()
{
    int x = 5;
    //const int i = x; // 错误:用变量初始化的表达式,不是常量表达式,会导致语句:constexpr int j = i报错
    const int i = 1;
    constexpr int j = i;



    constexpr const int* p3 = &c;
    return 0;
}

注意:constexpr可以修饰指针,constexpr修饰的指针是顶层const,也就是指针本身

而且constexpr要修饰指针的话,等号右边的地址不能是局部变量,必须是全局变量或者static或nullptr类型,因为之前我的一篇博客说过全局变量、静态变量存储在内存分布中的全局区/静态区,但是局部变量在栈区,而且栈区是运行时系统动态分配 的:每次运行程序、每次函数调用,栈地址都可能变,所以编译器在编译阶段完全不知道它的地址,但是全局区和静态区不同,其在编译时就固定好了地址。

2.2.2 修饰函数

特点

  • constexpr修饰函数func的时候,我们调用该函数,不一定非得是编译时完成才行,运行时也可以完成
  • constexpr修饰普通函数,要求函数声明的参数和返回值都是字面值类型(整形、浮点型、指针、引用等),不能有动态内存。函数返回值类型不能是空。要求函数体中,只包含⼀条return返回语句(意味着不能递归),不能定义局部变量,循环条件判断等控制流,并且返回值必须是常量表达式。
  • constexpr修饰构造函数,constexpr不能修饰自定义类型,但是用constexpr修饰类的构造函数后可以就可以。该类的所有成员变量必须是字面类型(literaltype),constexpr构造函数必须在初始化列表初始化所有成员变量,构造对象实参必须使用常量表达式,函数体必须为空。
cpp 复制代码
struct Point {
    int x, y;   //成员变量必须是字面类型
    constexpr Point(int a, int b) : x(a), y(b)   //成员变量只能用初始化列表
    {
        // 构造函数体内必须为空
    }
};
  • constexpr修饰成员函数,constexpr成员函数自动成为const成员函数,这意味着它们不能修改对象的成员变量,其他要求跟普通函数⼀样。成员函数必须同时加**const,只能**return 成员,不能有复杂逻辑。另外constexpr成员函数不能是虚函数(因为虚函数有虚基表,外部调用的时候得在运行时查找对应基类或子类的函数)。
cpp 复制代码
struct Point {
    int x, y;
    constexpr Point(int a, int b) : x(a), y(b) {}
    constexpr int getX() const { return x; }    //必须加const
};
  • constexpr 修饰模板函数,由于模板中类型的不确定性,因此模板函数实例化后的函数是否符合常量表达式函数的要求也是不确定的。C++11标准规定,如果constexpr修饰的模板函数实例 化结果不满足常量表达式函数的要求,则constexpr会被自动忽略,即该函数就等同于⼀个普通函数。

2.3 样例

cpp 复制代码
#include<iostream>
using namespace std;
constexpr int size()
{
	return 10;
}
constexpr int func(int x)
{
	return 10 + x;
}
int main()
{
	// 编译时,N会被直接替换为10,constexpr函数默认就是inline
	constexpr int N1 = size();
	int arr1[N1];
	// func传10时,func函数返回值是常量表达式,所以N2是常量表达式
	constexpr int N2 = func(10);
	int arr2[N2];
	int i = 10;

	int N4 = func(i);//运行时计算
	//constexpr修饰的函数可以有⼀些其他语句,但是这些语句运⾏时可以不执⾏任何操作就可以

	// 如类型别名、空语句、using声明等

	return 0;
}

三、constexpr随着cpp版本的演进

以上constexpr的使用都是基于Cpp11的,下面将介绍一下cpp14、17、20中constexpr的变化

3.1 Cpp14中constexpr的变化

C++14最显著的改进是大幅放宽了对constexpr函数的限制,使其语法和功能更接近普通函数。

函数限制的全面放宽:

• 局部变量:允许声明和初始化局部变量(只要在constexpr上下文中使用)

• 控制流语句:支持if条件分支、for/while循环、switch语句等

• 多return语句:函数体不再限于单⼀return语句

• 支持更复杂的返回类型:如void返回,自定义类、STL容器(std::array)、其他符合constexpr要求的复合类型

例1:

cpp 复制代码
// C++14允许的constexpr函数⽰例

constexpr int factorial(int n) {
	int res = 1;
	// 允许局部变量

	for (int i = 2; i <= n; ++i) {  // 允许循环
			res *= i;
	}
	return res;// 单⼀return
}
constexpr size_t stringLength(const char* str) {
	size_t len = 0;
	while (str[len] != '\0')
		++len;
	return len;
}
constexpr int func(int n)
{
	return n <= 1 ? 1 : n * factorial(n - 1);//允许递归,也就是多个return
}

constexpr int fact7 = factorial(7);  // 编译时计算
constexpr size_t len = stringLength("Hello");  // 编译期计算:5
constexpr int fcun5 = func(5);  // 编译时计算

例2:支持更复杂的返回类型

cpp 复制代码
#include<iostream>
#include<vector>
#include<array>
using namespace std;

struct Point {
	constexpr Point(double x, double y) : x(x), y(y) {}
	double x, y;
};
constexpr Point midpoint(Point a, Point b) {
	return Point((a.x + b.x) / 2, (a.y + b.y) / 2);
}
constexpr std::array<int, 5> createArray() {
	std::array<int, 5> arr{};
	for (size_t i = 0; i < arr.size(); ++i) {
		arr[i] = i * i;
	}
	return arr;
}
constexpr int fibonacci(int n) {
	return (n <= 1) ? n : (fibonacci(n - 1) + fibonacci(n - 2));
}
int main()
{
	Point p1 = midpoint({ 1.1,1.1 }, { 2.2,2.2 });
	constexpr Point p2 = midpoint({ 1.1,1.1 }, { 2.2,2.2 });
	constexpr std::array<int, 5> a1 = createArray();
	constexpr int fibArray[] = {
	fibonacci(0), fibonacci(1), fibonacci(2), fibonacci(3),
	fibonacci(4), fibonacci(5), fibonacci(6), fibonacci(7)
	};
	return 0;
}

3.2 Cpp17中constexpr的变化

C++17对constexpr进行了重大扩展,使其能力大幅提升,进⼀步模糊了编译时和运行时的界限。

  • 引入if constexpr语法,if constexpr 是 C++17引入的⼀种条件编译语句,它允许在编译时根据常量表达式的结果决定编译哪部分代码,未选择的分支代码不会编译成指令,直接丢弃。
  • constexpr的lambda表达式,lambda表达式可标记为constexpr,捕获的对象必须是编译期常量,函数体需满足constexpr函数要求

例1:if constexpr语法的使用

cpp 复制代码
#include<type_traits>

template<class T>
auto get_value(T t)
{
	if constexpr (std::is_integral_v<T>)
	{
		return 1;
	}
	else return 1.0;
}



int main()
{
	get_value(1);
}

由于auto是自动推导返回值类型(Cpp14引入的),但是我们在编译时如果用if else,return返回的类型不一致,一个是int,一个是浮点数,所以我们编译时会报错,但是我们把if替换成if constexpr的话,我们会在编译时确定具体的分支,比如get_value(1),1是int类型,从而编译时else的代码不会编译进去从而导致return只有一个返回值,编译通过(如果是普通的if else 两个分支都要实例化、都编译)。

例2:constexpr的lambda表达式的使用

cpp 复制代码
int main()
{
	constexpr int n = 10;
	int y = 0;
	constexpr auto square = [n](int x) constexpr { return x * x * n; };
	constexpr int result = square(5);  // 编译期计算:250
	return 0;
}

3.3 Cpp20中constexpr的变化

C++20标准对constexpr关键字进行了革命性的增强,将编译期计算能力提升到了前所未有的高度。这些改进不仅大幅扩展了constexpr的应用范围,还使其成为现代C++元编程和性能优化的核心⼯具。

  • constexpr 支持 new /delete(动态内存)
  • constexpr 支持 std::string /std::vector
  • constexpr 支持 虚函数(virtual)
  • constexpr 支持 堆、try/catch 异常
  • constexpr 析构函数

例1:

cpp 复制代码
constexpr int func() {
    int* p = new int(10);  // ✅ C++20 允许
    int val = *p;
    delete p;              // 必须配对
    return val;
}
constexpr int res = func(); // 编译期分配内存

constexpr string s = "hello";  // ✅ C++20
constexpr vector<int> v{1,2,3};// ✅ C++20


// 支持虚函数
struct Base {
    constexpr virtual int f() { return 1; }
};


//支持try-catch,但是这里不能抛出真正的异常,因为不是常量表达式
constexpr int func() {
    try {
        throw 1;
    } catch(...) {
        return 2;
    }
}
constexpr int res = func(); // ✅ C++20

同时constexpr还引入了可变(mutable)成员。constexpr成员函数中,成员变量是不能修改的,但是我们定义成员变量时,加上mutable修饰,这个成员变量在constexpr成员函数中就可以修改了。

样例:

cpp 复制代码
class A {
	mutable int _i;
	int _j;
public:
	constexpr A(int i, int j)
		:_i(i)
		, _j(j)
	{}

	constexpr int Func1() const
	{
		++_i;// 可以修改

		// ++_j; // 不能修改

		return _i + _j;
	}
	constexpr int Func2() const
	{
		return _i + _j;
	}
};


int main()
{
	constexpr A aa(1, 1);

	int ret1 = aa.Func1();// 这⾥不能加constexpr,因为Func1修改了_i,Func1已经⽆法做到编译时计算了。
	constexpr int ret2 = aa.Func2();
	return 0;
}

同时C++20还引入了constexpr的两个兄弟,一个是consteval,要求是只能在编译时执行计算,另一个是constinit,感兴趣的自己问ai了解一下

相关推荐
AI玫瑰助手7 小时前
Python运算符:比较运算符(等于不等等于大于小于)与返回值
android·开发语言·python
咩咦7 小时前
C++学习笔记22:前置后置 ++/-- 和日期减日期
c++·学习笔记·运算符重载·日期类·前置++·后置++·日期减日期
计算机安禾8 小时前
【c++面向对象编程】第40篇:单例模式(Singleton)的多种C++实现
开发语言·c++·单例模式
_日拱一卒8 小时前
LeetCode:114二叉树展开为链表
java·开发语言·算法
天天进步20158 小时前
从零打造 Python 全栈项目:智能教学辅助系统
开发语言·人工智能·python
一个不知名程序员www8 小时前
算法学习入门---算法题DAY1
c++·算法
kkeeper~8 小时前
0基础C语言积跬步之内存函数
c语言·开发语言
吃好睡好便好8 小时前
在Matlab中绘制杆状图
开发语言·学习·算法·matlab·信息可视化
桀人8 小时前
C++——内存管理——new和delete的超详细解析
开发语言·c++