c++11扩展
constexpr
constexpr 全称 const expression
顶级const和底层const
C++⽤为了好区分,把本⾝被const修饰叫做顶层const,把指向的对象被const 修饰叫做底层const。
大多数对象被const修饰都叫顶层const,指针被const修饰时,*左边的const叫底层const,*右边 的const叫做顶层const。
const修饰引⽤时,这个const是底层const。
constexpr和常量表达式
1、常量表达式是指值不会改变并且在编译过程中就能得到计算结果的表达式,
字⾯值、常量表达式 初始化的const对象 都是常量表达式,要注意 变量 初始化的const对象不是常量表达式。
2、constexpr(constant expression)是C++11引⼊的⼀个关键字,⽤于指定常量表达式。它允许在编 译时计算表达式的值,从⽽提⾼运⾏时性能并增强类型安全性。
3、constexpr可以修饰变量,constexpr修饰的变量⼀定是常量表达式,且必须⽤常量表达式初始化, 否则会报错。
4、constexpr可以修饰指针,constexpr修饰的指针是顶层const,也就是指针本⾝。
是不是常量表达式有什么关系呢? 会影响编译器的优化。
constexpr和函数
1、constexpr普通函数,要求函数声明的参数和返回值都是字⾯值类型(整形、浮点型、指针、引⽤ 等),函数返回值类型不能是空。要求函数体中,只包含⼀条 return 返回语句,不能定义局部变 量,循环条件判断等控制流,并且返回值必须是常量表达式。
constexpr int size() {return 10; }constexpr int func(int x) {return 10 + x; }constexpr int factorial(int n) {return n <= 1 ? 1 : n * factorial(n - 1); }//普通constexpr函数 仅需要一条return语句 constexpr int fxx(int x) {int i = x;i++;cout << i << endl;return 10 + x; }int main() {constexpr int x = size();constexpr int y = func(1);constexpr int z = factorial(5);cout << x << endl;cout << y << endl;cout << z << endl;// 编译时,N会被直接替换为10,constexpr函数默认就是inlineconstexpr int N1 = size();int arr1[N1];// func传10时,func函数返回值是常量表达式,所以N2是常量表达式constexpr int N2 = func(10);int arr2[N2];// func传10时,func函数返回值是常量表达式,所以N2是常量表达式int i = 10;//constexpr int N3 = func(i); // 报错func返回的不是常量表达式int N4 = func(i); // 不报错constexpr函数返回的不一是常量表达式int N5 = func(10);constexpr int N6 = fxx(10); // 报错return 0; }
触发编译期求值,需要在传入constexpr函数的参数是常量表达式,并且使用constexpr修饰的变量接收constexpr函数的返回值。否则,就退化至普通函数的调用即运行时计算。
2、constexpr构造函数,constexpr不能修饰⾃定义类型,但是⽤constexpr修饰类的构造函数后可以 就可以。
该类的所有成员变量必须是字⾯类型(literal type),
constexpr构造函数必须在初始化列表 初始化所有成员变量,
构造对象实参必须使⽤常量表达式,
构造函数的函数体必须为空,
析构函数必须是平凡 的不做任何实际清理⼯作。(也就是要求这个类足够简单)
class Date{ public:constexpr Date(int year, int month, int day):_year(year), _month(month), _day(day){//cout << "constexpr Date(int year, int month, int day)" << endl;}private:int _year;int _month;int _day; }; int main(){int x = 2025;//constexpr Date d0(x, 9, 8); // 报错constexpr Date d1(2025, 9, 8); // 不报错,编译时替换计算Date d2(x, 9, 8); // 不报错,运行时调用计算return 0; }
3、constexpr成员函数(类似于修饰普通函数),
constexpr成员函数⾃动成为 const 成员函数,这意味着它们不能修改对象的 成员变量,其他要求跟普通函数⼀样。
另外constexpr成员函数不能是虚函数。
class Date{ public:constexpr Date(int year, int month, int day):_year(year), _month(month), _day(day){//cout << "constexpr Date(int year, int month, int day)" << endl;}constexpr int GetYear() const{return _year;} private:int _year;int _month;int _day; }; int main(){int x = 2025;//constexpr Date d0(x, 9, 8); // 报错constexpr Date d1(2025, 9, 8);constexpr int y = d1.GetYear();cout << y << endl;Date d2(x, 9, 8); // 不报错,运行时调用计算d2.GetYear();return 0; }
当这里使用运行时调用计算,也能调用constexpr修饰的成员函数。因为这里的编译期求值和运行时求值都是支持的。
4、constexpr 可以修饰模板函数,但由于模板中类型的不确定性,因此模板函数实例化后的函数是否 符合常量表达式函数的要求也是不确定的。C++11 标准规定,如果 constexpr 修饰的模板函数实例 化结果不满⾜常量表达式函数的要求,则 constexpr 会被⾃动忽略,即该函数就等同于⼀个普通函 数。
template<typename T> constexpr T Func(T t){return t; }int main(){string ret1 = Func(string("111111")); // 普通函数constexpr int ret2 = Func(10);return 0; }
总结:
使用constexpr修饰了普通函数、构造函数、成员函数还是模版函数,如果在各方面条件都符合的情况下,就进行编译期计算提高效率,否则就退化至运行时计算。
c++14中的constexpr
局部变量:允许声明和初始化局部变量(只要在constexpr上下⽂中使⽤)
控制流语句:⽀持if条件分⽀、for/while循环、switch语句等
多return语句:函数体不再限于单⼀return语句
⽀持更复杂的返回类型:如void返回,⾃定义类、STL容器(std::array)、其他符合constexpr要求的 复合类型
// 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;
}int main(){constexpr size_t len = stringLength("Hello"); // 编译期计算:5constexpr size_t n = factorial(5); // 编译期计算:5cout << len << endl;cout << n << endl;return 0;
}
C++14允许constexpr函数返回⾮基本类型,包括:⾃定义类、STL容器std::array、其他符合 constexpr要求的复合类型。
struct Point {constexpr Point(double x, double y) : _x(x), _y(y){_x++;//cout << "constexpr Point(double x, double y)" << endl;}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 = {1,2,3,4,5};for (size_t i = 0; i < arr.size(); ++i) {//arr[i] = i * i;}return arr;
}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();return 0;
}
c++17中的constexpr
进⼀步模糊了编译时和运⾏时的界限。
if constexpr - 编译期条件分⽀
if constexpr 是 C++17 引⼊的⼀种条件编译语句,它允许在编译时根据常量表达式的结果决定编译哪 部分代码,未选择的分⽀代码不会编译成指令,直接丢弃。
template <typename T> auto get_value(T t) {if constexpr (std::is_pointer_v<T>) {return *t; // 仅当T为指针类型时实例化} else {return t; // ⾮指针类型时实例化} } // 使⽤⽰例 int x = 42; auto v1 = get_value(x); // 返回x本⾝ auto v2 = get_value(&x); // 解引⽤返回42
constexpr lambda 表达式
1、lambda表达式可标记为constexpr。
2、捕获必须是编译期常量。
3、函数体需满⾜constexpr函数要求。
int main() {// constexpr lambda示例constexpr int n = 10;int y = 0;constexpr auto square = [n](int x) constexpr { return x * x * n; };constexpr int result = square(5); // 编译期计算:250return 0; }
c++20中的constexpr
动态内存分配的编译期⽀持:
1、new / delete ⽀持:允许在 constexpr 上下⽂中使⽤动态内存分配。
2、编译期容器:使得 std::vector 和 std::string 等容器的编译期实现成为可能。
3、内存⽣命周期:所有分配的内存在编译期必须被释放。
constexpr int dynamic_memory_example() {int* p = new int{ 42 }; // 编译期分配int value = *p;delete p; // 必须显式释放return value; } int main() {constexpr int v = dynamic_memory_example(); // 42return 0; }
try-catch 的全⾯⽀持:
1、完整语法⽀持:允许 try-catch 块。
2、实际限制:不能真正抛出异常(否则不是常量表达式)。
3、错误处理:主要⽤于模板约束和编译期错误检测,异常必须在编译期捕获和处理,不能传播到运⾏时。
#include<iostream> using namespace std; constexpr int safe_divide(int a, int b) {try {if (b == 0)throw "Division by zero";elsereturn a / b;}catch (...) {return 0; // 编译期异常处理} } int main() {constexpr int val1 = safe_divide(10, 2); // 5constexpr int val2 = safe_divide(10, 0); // 报错 }
constexpr 联合体(union):
编译期活跃成员切换:可以在编译期改变联合体的活跃成员。
constexpr构造函数:允许定义constexpr构造函数来初始化联合体。
成员访问限制:只能访问当前活跃成员(编译期检查)。
constexpr 可变(mutable)成员:
constexpr成员函数中,成员变量是不能修改的,但是我们定义成员变量时,加上mutable修饰,这个 成员变量在constexpr成员函数中就可以修改了。
class A {mutable int _i;int _j; public:constexpr A(int i, int j):_i(i), _j(j){}constexpr int Func() const{++_i; // 可以修改//++_j; // 不能修改return _i + _j;} }; int main() {constexpr A aa(1, 1);constexpr int ret = aa.Func();return 0; }
constexpr 虚函数⽀持:
之前虚函数是不⽀持定义为constexpr函数的,C++20中开始⽀持。
class Base { public:virtual constexpr int value() const { return 1; } };class Derived : public Base { public:virtual constexpr int value() const override { return 2; } };constexpr int get_value(const Base& b) {return b.value(); // 编译期多态调用 }int main(){constexpr int ret1 = get_value(Base());constexpr int ret2 = get_value(Derived());return 0; }
处理类型
auto
auto是⼀个类型说明符,他让编译器替我们分析表达式的类型。
1、编译器推导auto类型时,有时候也会和初始值的类型不⼀样,编译器会适当的改变结果类型,使其 更符合初始化规则。
⾸先使⽤引⽤其实是使⽤引⽤的对象,特别是当引⽤被⽤作初始值时,真正参 与初始化的其实是引⽤对象的值,所以编译器推导auto为引⽤对象的类型,⽽不是引⽤。
其次⼀个 带有const属性的值初始化auto对象推导时忽略掉顶层const,保留底层const。
2、auto不能⾃动推导出引⽤类型,所以我们如果想将auto推导为引⽤类型,需要明确的指出: auto& x = i;
3、auto不能推导出顶层const,如果想使⽤auto推导出顶层const,需要明确的指出: const auto x = ci;
※※※※※(这里比较绕)
4、设置⼀个类型为auto引⽤时,初始值中的顶层const属性仍然保留,否则存在权限放⼤问题。
总结:
完整的代码
int main()
{int i = 0;auto k = i; // k类型为intk++;int& ri = i;auto j = ri; // j类型为intj++;const int ci = 42; // 顶层constint* const p1 = &i; // 顶层constconst int* p2 = &ci; // 底层constauto r1 = ci; // r1类型为int,忽略掉顶层constr1++;auto r2 = p1; // r2类型为int*,忽略掉顶层constr2++;auto r3 = p2; // r3类型为const int*,保留底层const// (*r3)++; // 报错const int& ri1 = ci; // 底层constconst int& ri2 = i; // 底层constauto r4 = ri1; // r4类型为int,因为ri1是ci的别名,ci本身的const是一个顶层const被忽略掉了r4++;auto r5 = ri2; // r5类型为intr5++;const auto r7 = ci; // r7类型为const intauto& r8 = ri1; // r8类型为const int&auto& r9 = ri2; // r9类型为const int&auto& r10 = ci; // r10类型为const int&auto& r11 = ri; // r11类型为int&//r7++; // 报错//r8++; // 报错//r9++; // 报错//r10++; // 报错r11++;return 0;
}
1、auto& 声明⼀个左值引⽤,它只能绑定到左值,如果初始化对象有const属性,推导时会保持 const 限定符,否则涉及权限放⼤。
2、const auto& 声明⼀个const 左值引⽤,既可以绑定到左值⼜可以绑定到右值,不会修改绑定 对象。
3、auto&& 是万能引⽤,遵循引⽤折叠(只有右值引用碰到右值引用才能推导出右值引用,其余的都是左值引用)的规则,既可以绑定到左值⼜可以绑定到右值,初始化表达 式⾃动推导为左值引⽤或右值引⽤,如果初始化对象有const属性,推导时会保持 const 限定符。
void func(int& x)
{cout << "void func(int& x)" << endl;
}
void func(int&& x)
{cout << "void func(int&& x)" << endl;
}
void func(const int& x)
{cout << "void func(const int& x)" << endl;
}
void func(const int&& x)
{cout << "void func(const int&& x)" << endl;
}int main() {int x = 10;const int cx = 20;auto& rx1 = x; //int&auto& rx2 = cx; //const int&func(rx1);func(rx2);cout << "---------------" << endl;const auto& rx3 = x; // const int&const auto& rx4 = cx; // const int&func(rx3);func(rx4);cout << "---------------" << endl;auto&& rx5 = x; // int&auto&& rx6 = cx; // const int&func(rx5);func(rx6);cout << "---------------" << endl;auto&& rx7 = move(x); // int&&++rx7;//右值引用是能修改的auto&& rx8 = move(cx); // const int&&//++rx8;//const 右值引用是不能修改的func(rx7);func(rx8);cout << "---------------" << endl;func(forward<int>(rx7));func(forward<const int>(rx8));return 0;
}
运行效果:
尾置返回类型
允许将函数的返回类型放在参数列表之后⽽不是函 数名前。
(C++14引⽤了auto做返回类型时, 返回类型⾃动推导,很多地⽅就不太需要尾置返回类型了。)
为什么需要尾置返回类型
1、提高代码可读性:特别是当返回类型很⻓或复杂时
2、支持Lambda表达式:Lambda表达式的返回类型必须使⽤尾置语法
3、模版编程:在模板函数中,返回类型可能依赖于参数类型
应用场景
// 1. 复杂返回类型
auto getComplexType() -> std::map<std::string, std::vector<int>> {// ...
}// 2. 依赖参数类型的返回类型(以前不支持auto自动推导)
template <typename T, typename U>auto add(T t, U u) -> decltype(t + u) {return t + u;
}// 3. lambda表达式
auto lambda = [](int x) -> double { return x * 1.5; };
decltype
1、如果我们希望⽤表达式推出变量的类型,但是不想⽤表达式的值初始化变量,那么这时可以使⽤ decltype。
decltype(f()) x; 需要注意的是编译器并不会实际调⽤f函数,⽽是⽤f的返回类 型作为x的类型。
2、decltype 处理const和引⽤的⽅式和auto也有所不同, decltype(const变量表达式) x ,x 的类型推出类型为const T,decltype会保留顶层const; decltype(引⽤变量表达式) x ,x的 类型推出类型为T &,decltype会保留引⽤;要注意这⾥跟auto是完全不同的。
3、decltype还有⼀些特殊处理⽐较奇怪, decltype(*p) x; x的类型是T&,decltye推导解引⽤表 达式时,推出类型是引⽤; decltype((i)) x; x的类型是T&,decltye推导解括号括起来的左 值表达式时,推出类型是引⽤;
int main()
{int i = 0;const int ci = 0;const int& rci = ci;decltype(i) n;decltype(i) m = 1; // m的类型是intdecltype(ci) x = 1; // x的类型是const intm++;//x++; // 报错decltype(rci) y = x; // y的类型是const int&// y++; // 报错// decltype(rci) z; // 报错int* p1 = &i;decltype(p1) p2 = nullptr; // p2的类型是int*// 特殊处理decltype(*p1) r1 = i; // r1的类型是int&,解引用表达式推导出的内容是引用decltype(i) r2; // r2的类型是intdecltype((i)) r3 = i; // r3的类型是int&, (i)是一个表达式,变量是一种可以赋值特殊表达式,所以会推出引用类型r1++;r3++;return 0;
}
auto必须要通过初始化值推导类型,像类的成员变量这种就没办法使⽤auto,decltype可以很好的 解决这样的问题
#include <vector>
using namespace std;
template <typename T>
class A
{
public:void func(T& container){_it = container.begin();}
private:// 这里不确定是iterator还是const_iterator,也不能使用auto//typename T::iterator _it;// 使用decltype推导就可以很好的解决问题decltype(T().begin()) _it;
};int main()
{const vector<int> v1;A<const vector<int>> obj1;obj1.func(v1);vector<int> v2;A<vector<int>> obj2;obj2.func(v2);return 0;
}
decltype还可以⽤来解决函数尾置返回类型的问题,有时⼀个函数模板的类型跟是不确定的,跟某 个参数对象有关,需要进⾏推导,直接⽤decltype推导去做返回类型是不⾏了,因为C++是前置语 法,往前找不到这个推导对象,这个对象在参数⾥⾯,所以auto做返回值,然后->decltype(对象) 做尾置推导。
#include <vector>
#include <list>
using namespace std;template<class R, class Iter>
R Func(Iter it1, Iter it2)
{R x = *it1;++it1;while (it1 != it2){x += *it1;++it1;}return x;
}// c++14新标准提供
//template<class R, class Iter>
//auto Func(Iter it1, Iter it2)
//{
// auto x = *it1;
// ++it1;
// while (it1 != it2)
// {
// x += *it1;
// ++it1;
// }
// return x;
//}int main()
{vector<int> v = { 1,2,3 };list<string> lt = { "111","222","333" };// 这里无法调用上面的函数,因为函数模板只能通过实参推导模板类型,无法推导R//auto ret1 = Func(v.begin(), v.end());//auto ret2 = Func(lt.begin(), lt.end());// 显示实例化能解决问题,但是调用就很麻烦auto ret1 = Func<decltype(*v.begin())>(v.begin(), v.end());auto ret2 = Func<decltype(*lt.begin())>(lt.begin(), lt.end());cout << ret1 << endl;cout << ret2 << endl;
}
decltype(auto) 是 C++14 引⼊的特性,它结合了 auto 的便利性和 decltype 的精确类 型推导能⼒。
int main()
{int i = 0;int& ri = i;const int ci = 42; // 顶层constint* const p1 = &i; // 顶层constconst int* p2 = &ci; // 底层constauto j = ri; // j类型为intdecltype(auto) j1 = ri; // j1类型为int&++j1;auto r1 = ci; // r1类型为int,忽略掉顶层constdecltype(auto) rr1 = ci; // rr1类型为const intr1++;//rr1++;auto r2 = p1; // r2类型为int*,忽略掉顶层constdecltype(auto) rr2 = p1; // rr1类型为int* constr2++;//rr2++;auto r3 = p2; // r3类型为const int*,保留底层constdecltype(auto) rr3 = p2; // rr3类型为const int*// (*rr3)++;return 0;
}
typedef和using
c++98中我们⼀般使⽤typedef重定义类型名,也很⽅便,但是typedef不⽀持带模板参数的类型重 定义。C++11中新增了using可以替代typedef,using 的别名语法覆盖了 typedef 的全部功能,不 少场景还更清晰⼀些,⽐如函数指针的重定义,其次最⼤的变化是⽀持带模板参数重定义的语法。
using 类型别名 = 类型;
#include <map>
#include <string>
using namespace std;// using支持带模板参数的类型重定义
template<class Val>
using Map = map<string, Val>;template<class Val>
using MapIter = typename map<string, Val>::iterator;int main()
{Map<int> countMap;Map<string> dictMap;MapIter<int> cit = countMap.begin();MapIter<string> dit = dictMap.begin();// C++20⽀持using enum Color; // 引⼊Color枚举值到当前作⽤域Color c = Red; // 现在可以直接使⽤return 0;
}
强类型枚举
传统C++ 枚举存在以下主要问题:
1、隐式转换为整型:枚举值会⾃动转换为整数,可能导致意外⾏为。
2、污染外围作⽤域:枚举值会泄漏到包含它的作⽤域中(暴露在全局,上面有两个Red)。
3、⽆法指定底层类型:不能明确控制枚举使⽤的存储⼤⼩。
强类型枚举语法:
1、enum class 或 enum struct (两者等价)
2、可选的底层类型( : UnderlyingType )
3、枚举值必须通过枚举名作⽤域访问
语法如下
例子如下:
enum class Color { Red, Green, Blue };
enum class TrafficLight { Red, Yellow, Green };int main()
{Color col1 = Color::Red;//int col2 = Color::Green;Color col2 = Color::Green;TrafficLight tcol = TrafficLight::Red;return 0;
}
static_assert
static_assert 是C++11引⼊的编译时断⾔机制,它允许开发者在编译期间检查条件是否满⾜,如 果条件不满⾜,则会导致编译错误。 static_assert 是C++元编程和模板编程中⾮常有⽤的⼯具, 它可以帮助开发者在编译期捕获错误,提⾼代码的健壮性。
c++17起可以省略错误消息
常见场景
// 1、类型检查
template<typename T>
void process(T value) {static_assert(std::is_integral<T>::value, "T must be an integral type");// 函数实现...
}// 2、编译时常量验证
constexpr int buffer_size = 1024;
static_assert(buffer_size > 0, "Buffer size must be positive");
static_assert(buffer_size % 4 == 0, "Buffer size must be divisible by 4");'// 3、平台或架构检查
static_assert(sizeof(void*) == 8, "This code requires 64-bit platform");// 4、类型⼤⼩验证
static_assert(sizeof(int) == 4, "int must be 4 bytes");
与运行时assert的区别
特性 | static_assert | assert |
检查时机 | 编译时 | 运行时 |
影响 | 编译错误 | 程序终止 |
表达式要求 | 必须为编译时常量表达式 | 任何表达式 |
用途 | 类型检查、编译时常量验证 | 运行时逻辑验证 |
std::tuple
std::tuple 是 C++11 引⼊的⼀个模板类,它允许将多个不同类型的值组合成⼀个单⼀的对象。类 似于结构体,但不需要预先定义类型名称。tuple(元组)是⼀个固定⼤⼩的异构值集合,可以包含不 同类型的元素。它是 std::pair 的泛化版本, pair 只能保存两个元素,⽽ tuple 可以保存任 意数量的元素。
创建Tuple
#include <tuple>
int main() {// 创建⼀个包含3个元素的tuple: int, double, stringstd::tuple<int, double, std::string> t1(10, 3.14, "hello");// 使⽤make_tuple⾃动推导类型auto t2 = std::make_tuple(20, 2.718, "world");// C++17起可以使⽤类模板参数推导std::tuple t3(30, 1.618, "cpp"); // ⾃动推导为tuple<int, double, const char*>return 0;
}
访问元素
#include <tuple>
int main() {// 创建⼀个包含3个元素的tuple: int, double, stringstd::tuple<int, double, std::string> t1(10, 3.14, "hello");// 通过索引访问std::cout << std::get<0>(t1) << std::endl; // 输出10std::cout << std::get<1>(t1) << std::endl; // 输出3.14std::cout << std::get<2>(t1) << std::endl<< std::endl; // 输出"hello"// 修改std::get<0>(t1) = 100; // 修改第⼀个元素// C++14起可以通过类型访问(类型必须唯⼀)std::cout << std::get<int>(t1) << std::endl; // 输出100std::cout << std::get<double>(t1) << std::endl; // 输出3.14return 0;
}
解包tuple
int x;double y;
std::string z;
// 使⽤std::tie解包
std::tie(x, y, z) = t1;// C++17结构化绑定
auto [a, b, c] = t1;
模版元编程
现代C++的⼀个进化⽅向就是在编译时做更多的⼯作,模板元编程(Template Metaprogramming, TMP)是C++中⼀种利⽤模板机制在编译期进⾏计算和代码⽣成的⾼级技术。它通过模板特化、递归实 例化和类型操作,在编译时完成传统运⾏时才能处理的任务,从⽽实现零运⾏时开销的优化。
模版元编程的核心概念
模版元编程的本质是将计算从运⾏时转移到编译期,利⽤编译器作为"计算引擎"⽣成⾼效代码。其核 ⼼思想包括:
1、编译期计算:所有运算在编译阶段完成,结果直接嵌⼊最终程序
2、类型操作:通过模板参数推导和类型萃取(Type Traits)操作类型
3、递归模板实例化:通过递归展开实现循环和条件逻辑
4、零运⾏时开销:结果在编译期确定,不增加程序运⾏负担
模版元编程基础语法
基础模版结构
模版元编程主要使⽤类模板(⽽⾮函数模板),因为类模板可以包含类型成员和静态成员,再利⽤模板特 化和递归实现。
编译期值计算
编译期计算阶乘
template <unsigned N>
struct Factorial {static const unsigned value = N * Factorial<N - 1>::value;
};// 终止条件特化
template <>
struct Factorial<0> {static const unsigned value = 1;
};int main()
{constexpr unsigned fact5 = Factorial<5>::value; // 编译时计算出120std::cout << fact5 << std::endl;return 0;
}
编译期类型计算
编译时获取或修改类型信息的操作。
template<typename T>
struct is_pointer {static constexpr bool value = false;
};// 针对指针类型的偏特化
template<typename T>
struct is_pointer<T*> {static constexpr bool value = true;
};// 主模板,默认情况类型不同
template<typename T, typename U>
struct is_same {static constexpr bool value = false;
};
// 特化版本,当两个类型相同时
template<typename T>
struct is_same<T, T> {static constexpr bool value = true;
};// 移除 const
// 主模板,默认情况下不改变类型
template <typename T>
struct remove_const {using type = T;
};// 针对 const T 的特化版本,移除 const
template <typename T>
struct remove_const<const T> {using type = T;
};// 移除 指针
template <typename T>
struct remove_pointer {using type = T;
};
template <typename T>
struct remove_pointer<T*> {using type = T;
};int main()
{static_assert(is_pointer<int*>::value, "int* is a pointer");//static_assert(is_pointer<int>::value, "int is not a pointer");static_assert(is_same<int, int>::value, "int and int should be the same");//static_assert(is_same<int, float>::value, "int and float should be different");static_assert(is_same<remove_const<int>::type, int>::value, "is not int");static_assert(is_same<remove_const<const int>::type, int>::value, "is not int");remove_const<const int>::type x = 0;x++;std::cout << x << std::endl;return 0;
}
模版特化的时候,无论特化的形参是const类型,指针类型,引用类型,该特化版本的原生泛化类型 依旧是T。使用using raw = T 或者 type_traits可以取得。
类型萃取(type_traits)
类型萃取是C++模板元编程中的核⼼技术,它允许在编译时检查和修改类型特性。C++11版本开始 标准库在 <type_traits> 头⽂件中提供了⼤量类型萃取⼯具。类型萃取是通过模板特化技术实 现的编译期类型操作,主要⽤途包括:检查类型特性、修改/转换类型、根据类型特性进⾏编译期 分⽀。
标准库常见的类型萃取
#include <type_traits>// 1、基础类型检查
std::is_void<void>::value; // true
std::is_integral<int>::value; // true
std::is_floating_point<float>::value; // true
std::is_pointer<int*>::value; // true
std::is_reference<int&>::value; // true
std::is_const<const int>::value; // true// 2、复合类型检查
std::is_function<void()>::value; // true
std::is_member_object_pointer<int (Foo::*)>::value; // true
std::is_compound<std::string>::value; // true (⾮基础类型)// 3、类型关系检查
std::is_same<int, int32_t>::value; // 取决于平台
std::is_base_of<Base, Derived>::value;
std::is_convertible<From, To>::value;// 4、类型修改
std::add_const<int>::type; // const int
std::add_pointer<int>::type; // int*
std::add_lvalue_reference<int>::type; // int&
std::remove_const<const int>::type; // int
std::remove_pointer<int*>::type; // int
std::remove_reference<int&>::type; // int// 5、条件类型选择
std::conditional<true, int, float>::type; // int
std::conditional<false, int, float>::type; // float// 6、类型推导
// 函数的返回结果类型
std::result_of<F(Args...)>::type; // C++17以后被废弃
std::invoke_result<F, Args...>::type; // C++17以后使⽤这个
c++17为类型萃取添加了 _v 和 _t 后缀的便利变量模板和类型别名
// C++11⽅式
std::is_integral<int>::value;
std::remove_const<const int>::type;// C++14、C++17 更简洁的⽅式
std::is_integral_v<int>;
std::remove_const_t<const int>;// C++17 引⼊的辅助变量模板
template<typename T>
inline constexpr bool is_integral_v = is_integral<T>::value;// C++14 引⼊的辅助别名模板
template<typename T>
using remove_const_t = typename remove_const<T>::type;
主要使用了,模版变量和using关键字实现。
类型萃取库 - 使用样例
template<typename T>
void process(T value) {if constexpr (std::is_pointer_v<T>) {// 指针类型的处理std::cout << "Processing pointer: " << *value << std::endl;}else if constexpr (std::is_integral_v<T>) {// 整数类型的处理std::cout << "Processing integer: " << value * 2 << std::endl;}else if constexpr (std::is_floating_point_v<T>) {// 浮点类型的处理std::cout << "Processing float: " << value / 2.0 << std::endl;}else {// 默认处理std::cout << "Processing unknown type" << std::endl;}
}int main()
{// 使用int i = 42;process(i); // Processing integer: 84process(&i); // Processing pointer: 42process(3.14); // Processing float: 1.57process("hello"); // Processing unknown typereturn 0;
}
这里的函数模版会在编译阶段根据传入的参数,对特定的if语句进行编译,其他的判断语句则不会走。
下面是不同平台网络编程的类型萃取
#if defined(_WIN32)
#include<winsock2.h>
using socket_t = SOCKET;
#else
using socket_t = int;
#endiftemplate<typename T>
void close_handle(T handle) {if constexpr (std::is_same_v<T, SOCKET>) {closesocket(handle);}else {close(handle);}
}
类型萃取在STL中的一些使用。※※※※※
下面是自定义的vector模版,里面实现了一个vector迭代器构造的功能。采用的逻辑是遍历传入的迭代器区间,然后逐个push进vector。
这样面临一个问题就是当这样的区间很大的时候,就会涉及到vector的扩容操作,扩容会去开辟更大的空间,拷贝数据,释放旧空间,这样是一个很大的耗时操作。但是如果可以使用迭代器提前预开辟好空间,就会很好提升性能和效率。
但是,迭代器分为很多种,InputIterator、OutputIterator、ForwardIterator、BidirectionalIterator、RandomAccessIterator。在这些迭代器中分为ForwardIterator支持前向++,BidirectionalIterator支持单向前向++和后向 --,RandomAccessIterator 所有操作都支持。
所以这里进行迭代器萃取,根据不同的迭代器去定制操作。首先这里需要提前知道的就是每个 stl 容器中的迭代器中都有一个 iterator_category 的标识,描述是哪一种迭代器,它的类型就是类似于 random_access_iterator_tag 或者 bidirectional_iterator_tag 这样的。也为后续的迭代器萃取提供了便利,取出容器中原生的迭代器类型。
在下面的自定义 vector 中,RandomAccessIterator 支持加减 n,而其他的迭代器 只能通过迭代计数计算个数了。
#include<list>
#include<string>
#include<vector>namespace hzp {/*struct input_iterator_tag {};struct output_iterator_tag {};struct forward_iterator_tag : public input_iterator_tag {};struct bidirectional_iterator_tag : public forward_iterator_tag {};struct random_access_iterator_tag : public bidirectional_iterator_tag {};*//*迭代器类型萃取*/template <class Iterator>struct iterator_traits {typedef typename Iterator::iterator_category iterator_category;typedef typename Iterator::value_type value_type;typedef typename Iterator::difference_type difference_type;typedef typename Iterator::pointer pointer;typedef typename Iterator::reference reference;};template < class T>struct iterator_traits<T*> {typedef std::random_access_iterator_tag iterator_category;typedef T value_type;typedef ptrdiff_t difference_type;typedef T* pointer;typedef T& reference;};template <class InputIterator>inline typename iterator_traits<InputIterator>::difference_type__distance(InputIterator first, InputIterator last,std::input_iterator_tag) {typename iterator_traits<InputIterator>::difference_type n = 0;while (first != last) {++first; ++n;}return n;}template <class RandomAccessIterator>inline typename iterator_traits<RandomAccessIterator>::difference_type__distance(RandomAccessIterator first, RandomAccessIterator last,std::random_access_iterator_tag) {return last - first;}template <class InputIterator>inline typename iterator_traits<InputIterator>::difference_typedistance(InputIterator first, InputIterator last) {using IterType = typename iterator_traits<InputIterator>::iterator_category;return __distance(first, last, IterType());}template<class T>class vector{public:typedef T* iterator;// 类模板的成员函数,也可以是一个函数模板template <class InputIterator>vector(InputIterator first, InputIterator last){// 为了提高效率可以提前resize// 这里InputIterator是模板参数,随机迭代器支持减计算个数// 单向和双向迭代器只是迭代计数计算个数// reserve(last - first);reserve(hzp::distance(first, last));while (first != last){// push_back(*first);++first;}}void reserve(size_t n) {}private:iterator _start = nullptr;iterator _finish = nullptr;iterator _end_of_storage = nullptr;};
}int main()
{std::string s("hello world");hzp::vector<char> v1(s.begin(), s.end());std::list<int> lt(10000, 1);hzp::vector<int> v2(lt.begin(), lt.end());return 0;
}
SFINAE
SFINAE是Substitution Failure Is Not An Error⾸字⺟缩写,意思是"替换失败不是错误",在模板 参数推导或替换时,如果某个候选模板导致编译错误(如类型不匹配、⽆效表达式等),编译器不 会直接报错,⽽是跳过该候选,尝试其他可⾏的版本。如果最后都没匹配到合适的版本,再进⾏报 错。
SFINAE经典应⽤场景函数重载
//版本1:仅适用于可递增的类型(如 int)
template<typename T>
//这里的decltype(++x, void())参数是一个逗号表达式,检测++x的合法性
auto foo(T x) -> decltype(++x, void()) {std::cout << "foo(T): " << x << " (can be incremented)\n";
}// 版本2:回退版本
void foo(...) {std::cout << "foo(...): fallback (cannot increment)\n";
}int main() {foo(42); // 调用版本1(int 支持 ++x)foo(std::string("1111")); // 调用版本2(string 不支持 ++x)
}
调用foo的两个重载版本,foo(42),首先尝试版本1,,42能自增,就会走版本1,而不会走版本二。foo(std::string("1111")) 的"1111"字符串不能自增,不会走版本1,而会尝试版本2。
版本1中的decltype(++x, void())参数是一个逗号表达式,用以检测++x的合法性,实际的返回值是void()对象
//版本1:仅适用于可递增的类型(如 int)//C++17
template<typename T>
auto foo(T x) -> std::void_t<decltype(x.size())> {std::cout << "foo(T): " << x << " (can be incremented)\n";
}// 版本2:回退版本
void foo(...) {std::cout << "foo(...): fallback (cannot increment)\n";
}int main() {foo(42); // 调用版本1(int 支持 ++x)foo(std::string("1111")); // 调用版本2(string 不支持 ++x)
}
c++17之后便很少写逗号表达式了。std::void_t<decltype(x.size())的尾置返回类型的关键是std::void_t,其原型如下。
也就是void_t是一个模版类型,但是,其传入的参数都不会用,最总返回的依旧是void。等价于上面使用的逗号表达式。其作用也是去检查decltype参数的合法性。
std::enable_if 是⼀个类型萃取
它是 SFINAE 的典型应⽤,⽤于在编译时启⽤/禁⽤函数模板。
#include <type_traits>
#include <iostream>
// 对于整数类型启用此重载
template<typename T>
typename std::enable_if_t<std::is_integral_v<T>, T>
add_one(T t) {return t + 1;
}// 对于浮点类型启用此重载
template<typename T>
typename std::enable_if_t<std::is_floating_point_v<T>, T>//这里的第二个参数类似于匿名类型参数
add_one(T t) {return t + 2.0;
}// 模板参数的检查
template<typename T, typename K = std::enable_if_t<std::is_integral_v<T>, T>>//这里的第二个参数类似于匿名类型参数void process_integer(T value) {// 只接受整数类型
}template<typename T, typename = std::enable_if_t<std::is_integral_v<T>>>//这里的第二个参数类似于匿名类型参数void process_integer(T value) {// 只接受整数类型
}int main() {std::cout << add_one(5) << "\n"; // 调用整数版本,输出6std::cout << add_one(3.14) << "\n"; // 调用浮点版本,输出4.14// std::cout << add_one("xxxx") << "\n";// add_one("hello"); // 编译错误,没有匹配的重载process_integer(1);process_integer(1.1); // 编译错误,没有匹配的重载
}
模版元编程优缺点分析
优点
1、零运⾏时开销:所有计算在编译期完成
2、类型安全:编译期类型检查
3、⾼度抽象:可构建灵活通⽤的库
缺点
1、编译时间⻓:复杂的模板实例化会增加编译时间
2、 学习成本增加:很多模板元编程的写法晦涩难懂,⼤ 增加学习成本
3、 错误信息晦涩:模板错误通常难以理解
4、 调试困难:难以调试编译期计算