总结归纳:C++ 17 新特性

关键字

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

如:

int main()
{
    int ok1, ok2;
    switch (0)
    {
        case 0:
        ok1 = 0;
        [[fallthrough]];
        case 1:
        ok2 = 1;
        [[fallthrough]];
    }
    return 0;
}

nodiscard

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

如:

[[nodiscard]] class A {}; //该属性在这其实没用
[[nodiscard]] enum class B {}; //该属性在这其实没用
class C {};
 
[[nodiscard]] int foo()
{ return 10; }
 
[[nodiscard]] A func1() { return A(); }
[[nodiscard]] B func2() { return B(); }
[[nodiscard]] C func3() { return C(); }
 
int main()
{
    foo();//warning: ignoring return value
    func1();//warning: ignoring return value
    func2();//warning: ignoring return value
    func3();//warning: ignoring return value
    return 0;
}

maybe_unused

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

用法例子:

[[maybe_unused]] class A {};
[[maybe_unused]] enum B {};
[[maybe_unused]] int C;
[[maybe_unused]] void fun();

数据类型

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。

上一页