「Notes」C++14新特性浅谈

发布时间:2026/7/14 13:20:45
「Notes」C++14新特性浅谈 原文链接https://liuxizai.ac.cn/post/notes-cpp14/基于 Ubuntu 20.04 的 NOI 2.0 发布后我们或许有机会开始使用 C14。这篇 Blog 将对 OI 中可能会使用到的 C14 的新特性进行简要总结。由于大部分 OIer 被 CCF 迫害可能是 C98 转 C14文中部分特性实际上是来源于 C11 标准的。另一个重要原因是C14 并不是 C 的一个主要版本主要版本C03 C11 C17其被认为是 C11 一个更加完善的版本C11 这一更新经历了整整8年时间引入了大量更改可以说是 C98 以来最重要的更新所以提到 C11 的特性确实很有必要。这篇 Blog 以简洁易懂为主要目标为此可能有些地方会在不影响理解的情况下使用一些不准确的表达这些地方往往都有脚注可以通过我给出的链接查看更详细的内容。constexprC 中一直存在着常量表达式的概念在 C98 中我们可以这样定义一个常量constintSIZE100005;常量将无法修改并且这也是编译器的一个优化机会编译器往往会在编译时就处理好常量表达式的值并在程序中硬编码结果也就是不会在程序运行时再去计算表达式的值。constintMOD1e97;// source codeansans%MOD;// after compilationansans%1000000007;// yes, 1e9 7 has been calculatedansans%(1e97);// no另外常量可以用来初始化数组intlen10;inta[len];// compile errorconstintlen10;inta[len];// okC11 中的constexpr关键字更进一步放宽了常量表达式的限制constintf(){return10;}inta[f()5];// compile errorconstexprintf(){return10;}inta[f()5];// ok你可能已经发现了C11 允许将函数作为常量表达式但是 C11 要求函数必须恰由一条return语句组成而 C14 解除了这一限制但你仍需要保证函数中没有goto语句未进行初始化的变量定义inta;// nointa10;// yes非字面类型的变量定义算术类型如int属于字面类型而自定义类型如string属于非字面类型为了方便 OIer 理解以上内容并不完全准确。lambdaC11 中允许使用匿名函数其能够内联于语句中structnode{intx,y;};std::vectornodearr;// C98boolcmp(node a,node b){returna.xb.x;}std::sort(arr.begin(),arr.end(),cmp);// C11std::sort(arr.begin(),arr.end(),[](node a,node b){returna.xb.x;});两种写法效果都是一样的。具体地说lambda表达式的语法为[捕获](形参){函数体}其中需要具体讲解的是捕获这一部分。捕获分为这样几个类型[]- 空捕获列表lambda表达式只能够使用非局部变量。[names]-names是一个逗号分割的名字列表这些名字为匿名函数所在的局部变量这些局部变量将被拷贝也就是说在函数中修改其值后并不会影响到其本身如果name前面使用了将会使用引用的方式捕获。[]- 隐式的以引用方式捕获所有匿名函数使用的局部变量。[]- 隐式的以值方式即拷贝捕获所有匿名函数使用的局部变量。[, list]-list是一个逗号分割的列表列表中的变量以值方式捕获其他局部变量隐式的以引用方式捕获。[, list]-list是一个逗号分割的列表列表中的变量以引用方式捕获其他局部变量隐式的以值方式捕获。C14 标准中规定了泛型lambda由于过于复杂选择不将其写入 Blog。另外你会发现匿名函数没有规定返回值编译器将会自行判断函数的返回值如果需要指定函数返回值可以使用以下语法[捕获](形参)-返回值类型{函数体}变量模板variable templateC14 允许通过变量模板定义一族变量。templatetypenameT// variable templateconstT piT(3.14159265);templatetypenameT// function templateTcircleArea(T r){returnpiT*r*r;// variable template instantiation}聚合初始化aggregate initialization聚合初始化是 C11 中列表初始化的一种形式。首先聚合体是下列类型之一数组类型满足一下条件的类类型常为struct没有私有或受保护的非静态数据成员在类中声明的非static数据成员没有用户提供的构造函数没有虚成员函数你可以像这样进行聚合初始化structnode{inta,b;intc[3];intd;};node nd{2,3,5,6,3,4};这样初始化过后a2;b3;c{5,6,3};d4;可以发现聚合初始化是按照地址顺序依次进行的所以对于类中的数组成员可以很方便的进行初始化当然这也意味着聚合初始化无法直接指定一些成员进行初始化。在 C20 中允许进行指派初始化器的聚合初始化即可以指定成员进行初始化另一个很重要的特性聚合初始化是递归进行的也就是说其允许嵌套structA{structB{inta;intb;intc;};B d;inte;vectorintf;};这样一个结构体我们仍然可以使用聚合初始化A a{{1,2,3},4,{5,6}};初始化结果如下d.a1;d.b2;d.c3;e4;f{5,6};在 C11 中聚合初始化要求类成员没有默认初始化器int a 10但在 C14 中允许我们这么做所以另外很重要的一点是当聚合初始化与默认初始化器结合时到底会产生怎么样的结果。举个例子说明structA{structB{inta21;intb;intc22;intd;inte23;};B b1{11,12};B b2{11,12,13};intx;};接下来进行聚合初始化A a{{1,2,3,4},{1},5};你会得到这样的结果b1.a1;b1.b2;b1.c3;b1.d4;b1.e23;b2.a1;b2.b0;b2.c22;b2.d0;b2.e23;x5;你会发现b2的初始化好像失效了否则我们应该得到这样的结果b2.a1;b2.b12;b2.c13;b2.d0;b2.e23;初始化器提供的值比类成员少时根据 N3605C14 会采用如下策略从成员的默认初始化器进行初始化如果没有默认初始化器用一个空初始化器列表进行初始化那么我们在对a进行聚合初始化时 实际上为b2提供了值{1}所以b2的初始化器完全失效接下来b2.a从聚合初始化中的到了值其他成员没有得到值所以隐式的按照 N3605 进行初始化。这正是我们得到的结果。autoauto于 C11 引入作为占位类型说明符其能够从初始化器自动推导变量类型。autoa12;// intautob24*7;// intautoc0.17;// doubleautoda;// intautoeac;// double复制代码C14 还允许使用auto自动推断函数返回值类型autof(){return23;}// int如下写法将会被推导为列表初始化器autog{1,2,3,4,5};// std::initializer_listintautoh{1,2,3,4,5};// std::initializer_listint第二种写法在 C17 中被弃用另外auto还常用于无名类型如lambda表达式类型autolambda[](){return912;}std::coutlambda()std::endl;// 21需要注意的是auto说明符要求变量必须拥有初始化器autox;// compile errorautoy10;// okauto x;这种写法在 C 中被允许。如果想要了解更多可以参考 cppreference。基于范围的 for 循环range-based for loopC11 规定了基于范围的for循环其在一个范围上执行for循环是传统for循环一个更加可读的等价版本OI 中常用于图遍历。其语法如下[属性-可选]for(范围声明:范围表达式){循环语句}属性属性说明符序列不在 Blog 中进行说明几乎不会用到。范围声明一个具名变量的声明类型为范围表达式中元素的类型或其引用一般使用auto对其类型进行推导。范围表达式一个序列数组或是定义了begin和end的对象如vector或是一个花括号列表初始化器如{1, 2, 3, 4, 5}。循环语句常规函数体。基于范围的for循环可以用这样的常规for循环替代for(auto__begin首表达式,__end尾表达式;__begin!__end;__begin){范围声明*__begin;循环语句}其中对于数组a[]其首表达式为a尾表达式为(a __bound)__bound为数组长度我们要求数组是有确定长度的。对于定义了begin和end的对象b其首表达式为b.begin()尾表达式为b.end()。否则通过实参依赖查找进行查找。一些实际使用的例子vectorintg[10005];for(autov:g[u]){/* something here */}inta[]{1,2,3,4,5};for(autox:a){x;std::coutx ;}// after - a: {2, 3, 4, 5, 6}for(autox:{1,3,5,7}){std::coutx ;}变参数模板variadic template在我看来无比实用的特性之一你可以在我的template中找到这样一个函数voidinput(){}templatetypenameType,typename...Typesvoidinput(Typearg,Types...args){argreadType();input(args...);}这就是一个变参数模板的使用案例你可以通过input()函数一次性对任意个变量通过快读进行读入。intx,y,z;input(x);// okinput(x,y,z);// ok常用的变参数模板格式和上面大同小异都是通过递归调用input(Type arg, Types... args)递归变参函数input(args...)就是在进行递归调用我们当然需要给这样一个递归函数一个终止条件input()被称为基础函数递归变参数函数最终在这里停止。