总结归纳:C++17新特性
2026/7/21 20:16:46 网站建设 项目流程

关键字

constexpr

扩展constexpr使用范围,可用于if语句中,也可用于lambda表达式中。

#include<iostream> template<bool ok> constexpr void foo() { //在编译期进行判断,if和else语句不生成代码 if constexpr (ok == true) { //当ok为true时,下面的else块不生成汇编代码 std::cout << "ok" << std::endl; } else { //当ok为false时,上面的if块不生成汇编代码 std::cout << "not ok" << std::endl; } } int main() { foo<true>();//输出ok,并且汇编代码中只有std::cout << "ok" << std::endl;这一句 foo<false>();//输出not ok,并且汇编代码中只有std::cout << "not ok" << std::endl;这一句 return 0; }

static_assert

扩展static_assert用法,静态断言的显示文本可选。

如:

static_assert(true, ""); static_assert(true);//c++17支持

typename

扩展用法,允许出现在模板的模板的参数中。

首先回顾一下typename的用法,①用于模板中,表示模板参数为类型;②用于声明某名字是变量名

struct A { typedef int Example; }; //第一种用法:声明模板参数为类型 template<typename T> struct B { }; struct C { typedef typename A::Example E;//第二种用法:声明某名字为一种类型 }; int main() { typename A::Example e;//第二种用法:声明某名字为一种类型 return 0; }

新特性下的typename用法,

如:

#include<iostream> #include<typeinfo> template<typename T> struct A { int num; A() { std::cout << "A Construct" << std::endl; std::cout << "template typename is: " << typeid (T).name() << std::endl; } }; //此处的T可省略,X代表模板类型,T和X前的typename可替换成class template<template<typename T> typename X> struct B { X<double> e; B() { std::cout << "B Construct" << std::endl; } }; int main() { A<B<A>> a; std::cout << "***************************" << std::endl; B<A> b; return 0; }

inline

Inline 变量, inline变量可以让变量有多于一次的定义。C++17之前,我们定义全局变量, 总需要将变量定义在cpp文件中,然后在通过extern关键字来告诉编译器 这个变量已经在其他地方定义过了。 inline变量出现后,我们可以直接将全局变量定义在头文件中,而不用担心出现redefine的错误信息。

扩展用法,可用于定义内联变量,功能与内联函数相似。inline可避免函数或变量多重定义的问题,如果已定义相同的函数或变量(且该函数或变量声明为inline),编译器会自动链接到该函数或变量。

如(不发生错误):

// test.h inline void print() { std::cout << "hello world" << std::endl; } inline int num = 0; // func.h include "test.h" inline void add(int arg) { num += arg; print(); } // main.cpp include "func.h" int main() { num = 0; print(); add(10); return 0; }

auto

从c++11开始,auto关键字能够通过初始化器推导出变量的类型。在c++14中,auto关键字的能力进一步提升,能够通过return语句推导出函数的返回类型。

使用auto关键字能够提高编码效率,同时能够简化重构流程。但是,C++11中的auto推导,往往结果与预期的不同。

c++11 中为了支持统一初始化,引入了新的统一初始化语法,如下所示。

// c++11 auto x3{ 1, 2 }; // std::initializer_list<int> auto x4 = { 3 }; // decltype(x4) is std::initializer_list<int> auto x5{ 3 }; // std::initializer_list<int>

这三种方式初始化的变量,最终类型推导的结果都是 std::initializer_list , 而不是我们认为的int。 这是因为

当用于auto声明变量的表达式是{}括起来的,推导的型别就会变成 std::initializer_list。

在C++17中,对auto表达式推导的规则进行了改变

// c++17 auto x3{ 1, 2 }; // error: not a single element auto x4 = { 3 }; // decltype(x4) is std::initializer_list<int> auto x5{ 3 }; // decltype(x5) is int

对比发现, auto x5{3}, 会直接将变量推导成 x5, 而 x3{1, 2} 这种方式也会编译失败。auto推导的规则变得更加直观。

语法

lambda表达式

lambda也是c++11中引入的,在C++11中,lambda表达式只能用捕获this,this是当前对象的一个只读的引用。

在C++17中,可以捕获this, this是当前对象的一个拷贝,捕获当前对象的拷贝,能够确保当前对象释放后,

lambda表达式能安全的调用this中的变量和方法。

条件表达式中支持初始化语句

c++17中支持在 if 或者switch 语句中进行初始化, 这个能力的出现能够让代码更加的简洁。

// c++17之前 map<int, string> c = { {1,"a"}}; { auto res = c.insert(make_pair(2, "b")); if(!res.second) { cout << "key 1 exist" << endl; } else { cout << "insert success, value:" << res.first->second << endl; } }

上面的一段代码,由于res是一个临时变量,不想影响到后面的代码,所以用一对花括号限制了其作用域。但是如果使用c++17的语法,

在if条件中初始化res,则代码就会显得更加简洁

// c++17 map<int, string> c = { {1,"a"}}; if(auto res = c.insert(make_pair(2, "b")); !res.second ) { cout << "key 1 exist" << endl; } else { cout << "insert success, value:" << res.first->second << endl; }

折叠表达式

用于变长参数模板的解包,只支持各种运算符(和操作符),分左、右折叠

如:

#include<string> template<typename ... T> auto sum(T ... arg) { return (arg + ...);//右折叠 } template<typename ... T> double sum_strong(T ... arg) { return (arg + ... + 0);//右折叠 } template<typename ... T> double sub1(T ... arg) { return (arg - ...);//右折叠 } template<typename ... T> double sub2(T ... arg) { return (... - arg);//左折叠 } int main() { int s1 = sum(1, 2, 2, 4, 5);//解包:((((1+)2+)3+)4+)5 = 15 double s2 = sum(1.1, 2.2, 3.3, 4.4, 5.5, 6.6); double s3 = sum(1, 2.2, 3, 4.4, 5); double s4 = sub1(5, 2, 1, 1);//解包:((((5-)2-)1-)1) = 1 double s5 = sub2(5, 2, 1, 1);//解包:(5-(2-(1-(1)))) = 3 double s6 = sum_strong();//s6 = 0 std::string str1("he"); std::string str2("ll"); std::string str3("o "); std::string str4("world"); std::string str5 = sum(str1, str2, str3, str4);//str5 = "hello world" return 0; }

结构化绑定

用一对包含一个或多个变量的中括号,表示结构化绑定,但是使用结构化绑定时,须用auto关键字,即绑定时声明变量

例子:

/* * 例子:多值返回 */ struct S { double num1; long num2; }; S foo(int arg1, double arg2) { double result1 = arg1 * arg2; long result2 = arg2 / arg1; return {result1, result2};//返回结构体S对象 }; int main() { auto [num1, num2] = foo(10, 20.2);//自动推导num1为double,num2为long return 0; }

允许非类型模板参数进行常量计算

非类型模板参数可传入类的静态成员

如:

class MyClass { public: static int a; }; template<int *arg> void foo() {} int main() { foo<&MyClass::a>(); return 0; }

条件分支语句初始化

在if和switch中可进行初始化

如:

template<long value> void foo(int &ok) { if constexpr (ok = 10; value > 0) { } } int main() { int num = 0; if(int i = 0; i == 0) { } foo<10>(num); switch(int k = 10; k) { case 0:break; case 1:break; default:break; } return 0; }

聚合初始化

在初始化对象时,可用花括号进行对其成员进行赋值

如:

struct MyStruct1 { int a; int b; }; struct MyStruct2 { int a; MyStruct1 ms; }; int main() { MyStruct1 a{10}; MyStruct2 b{10, 20}; MyStruct2 c{1, {}}; MyStruct2 d{ {}, {}}; MyStruct2 e{ {}, {1, 2}}; return 0; }

嵌套命名空间

简化多层命名空间的写法

如:

//传统写法 namespace A { namespace B { namespace C { }; }; }; //新写法 namespace A::B::C { };

lambda表达式捕获*this的值

lambda表达式可捕获*this的值,但this及其成员为只读

如:

struct MyStruct { double ohseven = 100.7; auto f() { return [this] { return [*this] { this->ohseven = 200.2;//错误,只读变量不可赋值 return ohseven;//正确 }; }(); } auto g() { return []{ return [*this]{};//错误,外层lambda表达式没有捕获this }(); } };

枚举[类]对象的构造

可以给枚举[类]对象赋值

如:

enum MyEnum { value }; MyEnum me {10};//错误:不能用int右值初始化MyEnum类型对象 enum byte : unsigned char { }; byte b { 42 }; //正确 byte c = { 42 }; //错误:不能用int右值初始化byte类型对象 byte d = byte{ 42 }; //正确,其值与b相等 byte e { -1 }; //错误:常量表达式-1不能缩小范围为byte类型 struct A { byte b; }; A a1 = { { 42 } }; //错误:不能用int右值初始化byte类型对象 A a2 = { byte{ 42 } }; //正确 void f(byte); f({ 42 }); //错误:无类型说明符 enum class Handle : unsigned int { value = 0 }; Handle h { 42 }; //正确

十六进制单精度浮点数字面值

以0x前缀开头的十六进制数,以f后缀的单精度浮点数,合并,就有了十六进制的单精度浮点数

如:

int main() { float value = 0x1111f; return 0; }

基于对齐内存的动态内存分配

谈到动态内存分配,少不了new和delete运算符,新标准中的new和delete运算符新增了按照对齐内存值来分配、释放内存空间的功能(即一个新的带对齐内存值的new、delete运算符重载)

函数原型:

void* operator new(std::size_t size, std::align_val_t alignment); void* operator new[](std::size_t size, std::align_val_t alignment); void operator delete(void*, std::size_t size, std::align_val_t alignment);

细化表达式的计算顺序

为了支持泛型编程和重载运算符的广泛使用,新特性将计算顺序进行的细化

如以下争议代码段:

#include<map> int main() { std::map<int, int> tmp; //对于std::map的[]运算符重载函数,在使用[]新增key时,std::map就已经插入了一个新的键值对 tmp[0] = tmp.size();//此处不知道插入的是{0, 0}还是{0, 1} return 0; }

为了解决该情况,新计算顺序规则为:

  1. 后缀表达式从左到右求值。这包括函数调用和成员选择表达式。
  2. 赋值表达式从右向左求值。这包括复合赋值。
  3. 从左到右计算移位操作符的操作数。

模板类的模板参数自动推导

定义模板类的对象时,可以不指定模板参数,但必须要在构造函数中能推导出模板参数

如:

template<class T> struct A { explicit A(const T&, ...) noexcept {} // #1 A(T&&, ...){} // #2 }; int i; A a1 = { i, i }; //错误,不能根据#1推导为右值引用,也不能通过#1实现复制初始化 A a2{i, i}; //正确,调用#1初始化成功,a2推导为A<int>类型 A a3{0, i}; //正确,调用#2初始化成功,a2推导为A<int>类型 A a4 = {0, i}; //正确,调用#2初始化成功,a2推导为A<int>类型 template<class T> A(const T&, const T&) -> A<T&>; // #3 template<class T> explicit A(T&&, T&&) -> A<T>; // #4 A a5 = {0, 1}; //错误,#1和#2构造函数结果相同(即冲突)。根据#3推导为A<int&>类型 A a6{0, 1}; //正确,通过#2推断为A<int>类型 A a7 = {0, i}; //错误,不能将非静态左值引用绑定到右值。根据#3推导为A<int&>类型 A a8{0, i}; //错误,不能将非静态左值引用绑定到右值。根据#3推导为A<int&>类型 template<class T> struct B { template<class U> using TA = T;//定义别名 template<class U> B(U, TA<U>);//构造函数 }; B b{(int*)0, (char*)0}; //正确,推导为B<char *>类型

简化重复命名空间的属性列表

如:

[[ using CC: opt(1), debug ]] void f() {} //作用相同于 [[ CC::opt(1), CC::debug ]] void f() {}

不支持、非标准的属性

在添加属性列表时,编译器会忽略不支持的非标准的属性,不会发出警告和错误。

改写与继承构造函数

在类的继承体系中,构造函数的自动调用是一个令人头疼的问题。新特性引入继承与改写构造函数的用法。

例:

#include<iostream> struct B1 { B1(int) { std::cout << "B1" << std::endl; } }; struct D1 : B1 { using B1::B1;//表示继承B1的构造函数 }; D1 d1(0); //正确,委托基类构造函数进行初始化,调用B1::B1(int)

__has_include

判断有没有包含某文件

如:

int main() { #if __has_include(<cstdio>) printf("hehe"); #endif #if __has_include("iostream") std::cout << "hehe" << std::endl; #endif return 0; }

属性

fallthrough

用于switch语句块内,表示会执行下一个case或default

如:

intmain(){intok1,ok2;switch(0){case0:ok1=0;[[fallthrough]];case1:ok2=1;[[fallthrough]];}return0;}

nodiscard

可用于类声明、函数声明、枚举声明中,表示函数的返回值没有被接收,在编译时会出现警告。

如:

[[nodiscard]]classA{};//该属性在这其实没用[[nodiscard]]enumclassB{};//该属性在这其实没用classC{};[[nodiscard]]intfoo(){return10;}[[nodiscard]]Afunc1(){returnA();}[[nodiscard]]Bfunc2(){returnB();}[[nodiscard]]Cfunc3(){returnC();}intmain(){foo();//warning: ignoring return valuefunc1();//warning: ignoring return valuefunc2();//warning: ignoring return valuefunc3();//warning: ignoring return valuereturn0;}

maybe_unused

可用于类、typedef、变量、非静态数据成员、函数、枚举或枚举值中。用于抑制编译器对没用实体的警告。即加上该属性后,对某一实体不会发出“没有用”的警告。

用法例子:

[[maybe_unused]]classA{};[[maybe_unused]]enumB{};[[maybe_unused]]intC;[[maybe_unused]]voidfun();

数据类型

c++17的标准库也进行了扩充, 新增了下面几种数据类型:

std::variant

std::variant是类型安全的联合体,是一个加强版的 union,variant支持更加复杂的数据类型,例如map,string等等

std::optional

std::optional表示一个可能存在的值。 当我们通过函数创建一个对象时,通常使用通过函数返回错误码,而通过出参返回对象本身。

如果通过optional返回创建的实例,就会变得更加直观,

std::optional 提供了下面几个方法:

has_value() // 检查对象是否有值 value() // 返回对象的值,值不存在时则抛出 std::bad_optional_access 异常 value_or() // 值存在时返回值,不存在时返回默认值

std::any

一个类型安全的可以保存任何值的容器

std::string_view

string_view我最早使用的是boost版本的,c++17中的string_view 和 boost类似。

string_view可以理解成原始字符串一个只读引用。 string_view 本身没有申请额外的内存来存储原始字符串的data,

仅仅保存了原始字符串地址和长度等信息。 在很多情况下,我们只是临时处理字符串,本不需要对原始字符串的一份拷贝。

使用string_view可以减少不必要的内存拷贝,可以提高程序性能。相比使用字符串指针,string_view做了更好的封装。

需要注意的是,string_view 由于没有原始字符串的所有权,使用string_view 一定要注意原始字符串的生命周期。

当原始的字符串已经销毁,则不能再调用string_view。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询