c++17 新特性

编译环境说明:gcc 8.1 + eclipse +windows 10 

eclipse cpp默认支持c++14,做c++17开发时,需要手动进行配置。

 1、关键字

1)constexpr

c++17扩展了constexpr使用范围,既可以用于if语句,也可以用于lambda表达式。

例1

template<bool ok>
constexpr void foo()
{
    //在编译期间进行判断,if和else语句不生成代码
    if constexpr (ok== true)
    {
        //当ok为true时,下面的else语句块不生成汇编代码。
        cout<<"ok"<<endl;
    }
    else
    {
        //当ok为false时,上面的if语句块不生成汇编代码。
        cout<<"not ok"<<endl;
    }
}

int main() {
    foo<true>(); //输出ok,并且汇编代码中只有cout<<"ok"<<endl;这一句。
    foo<false>();
    return 0;
}

例2: 

int main() {
    constexpr auto add1 = [](int n,int m){
        auto func1 = [=]{return n;};//func1 lambda表达式
        auto func2 = [=]{return m;};//func2 lambad表达式
        return [=]{ return func1()+func2();};
    };

    constexpr auto add2 = [](int n,int m){
        return n+m;
    };

    auto add3 = [](int n,int m){
        return n+m;
    };

    int sum1 = add1(30,40)(); //传入常量,add1在编译期计算,立即返回70.
    int sum2 = add2(sum1,4); //传入非constexpr 变量,add2的constexpr失效,变成运行期lambda.
    constexpr int sum3 = add3(1,2); //sum3为constexpr变量,传入常量值,add3编译期lambda。立即返回3.
    int sum4 = add2(10,2);//传入常量值,add2编译期计算,立即返回12.
    return 0;
}

2)static_assert

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

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

3)auto

扩展autod的推断范围。

auto x1 = { 1, 2 }; //推断出std::initializer_list<int>类型
auto x2 = { 1, 2.0 }; //错误:类型不统一,无法推断
auto x3{ 1, 2 }; //错误:auto的聚合初始化只能一个元素
auto x4 = { 3 }; //推断出std::initializer_list<int>类型
auto x5{ 3 }; //推断出int类型

4)typename

 传统用法:a)用于模板,表示模板参数为类型;b)用于声明某名字是变量。

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;
}

c++17 新特性下typename用法

struct A
{
    int num;
    A()
    {
        cout<<"A Construct"<<endl;
        cout<<"template typename is:"<<typeid(T).name()<<endl;
    }
};


//此处的T可省略,X代表模板类型,T和X前的typename可替换成class
template<template<typename T> typename X>
struct B
{
    X<double> e;
    B()
    {
        cout << "B Construct" <<endl;
    }
};

int main() {
    A<B<A>> a;
    B<A> b;
    return 0;
}

5)inline

扩展用法,可用于定义内联变量,功能与内联函数相似。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;
}

2 语法

1)折叠表达式

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

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, 3, 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)))) = 3
    double s5 = sub2(5, 2, 1, 1);//解包:((((5-)2-)1-)1) = 1
    double s6 = sum_strong();//s6 = 0
    string str1("he");
    string str2("ll");
    string str3("o ");
    string str4("world");
    string str5 = sum(str1, str2, str3, str4);//str5 = "hello world"
    return 0;
}

2)结构化绑定

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

例1:

/*
 * 例子:多值返回
 */
struct S
{
    double num1;
    double 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
    cout<<num1<<" "<<num2<<endl;
    return 0;
}

例2:

template<typename T, typename U>
struct MyStruct
{
    T key;
    U value;
};

int main() {

    list<MyStruct<int,double>> container_list;
    container_list.push_back({1,1.1});
    container_list.push_back({2,2.2});
    container_list.push_back({3,3.3});

    map<int,MyStruct<long long,char>> container_map;
    container_map[1] = {12,'a'};
    container_map[2] = {13,'b'};
    container_map[3] = {14,'c'};

    for(auto[key,value]:container_list)
    {
        cout<<"key:"<<key<<",value:"<<value<<endl;
    }

    for(auto[key,value]:container_map)
    {
        auto [value1,value2] = value;
        cout<<"key:"<<key<<",value:{"<<value1<<","<<value2<<"}"<<endl;
    }

    return 0;
}

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

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

class MyClass
{
public:
    static int a;
};

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

4)条件分支语音初始化

在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;
}

5)聚合初始化

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

struct MyStruct1
{
    int a;
    int b;
};
class MyClass
{
public:
    int a;
    int b;
};

struct MyStruct2
{
    int a;
    MyStruct1 ms;
};
class MyClassA
{
private:
    int a;
    int b;
public:
    MyClassA(int x,int y):a(x),b(y){}
};

int main() {
    MyStruct1 a{10};
    MyClass b{10};
    MyClassA ma{10,20};

    MyStruct2 b1{10, 20};
    MyStruct2 c{1, {}};
    MyStruct2 d{{}, {}};
    MyStruct2 e{{}, {1, 2}};

    return 0;
}

6)嵌套命名空间

简化多层命名空间的写法

//传统写法
namespace A
{
    namespace B
    {
        namespace C
        {

        };
    };
};

//新写法
namespace A::B::C
{

};

 7)lambda表达式捕获*this的值

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

struct MyStrcut{
    double value = 100.7;
    auto f(){
        return [this]{
            return [*this]{
                this->value = 200.2; //error: assignment of member 'MyStrcut::value' in read-only object
                return value; //正确
            };
        }();
    }
    auto g(){
        return []{
            return [*this]{}; //错误,外层lambda表达式没有捕获this
        }();
    }
};

 

8)枚举[类]对象的构造

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

enum MyEnum
{
    value
};
enum byte : unsigned char { };
struct A { byte b; };
void f(byte){};

int main() {
//    MyEnum me {10};//错误:不能用int右值初始化MyEnum类型对象
    byte b { 42 }; //正确

//    byte c = { 42 }; //错误:不能用int右值初始化byte类型对象
    byte d = byte{ 42 }; //正确,其值与b相等
//    byte e { -1 }; //错误:常量表达式-1不能缩小范围为byte类型
//    A a1 = { { 42 } }; //错误:不能用int右值初始化byte类型对象
    A a2 = { byte{ 42 } }; //正确

    A a3{ byte{ 42 } }; //正确
//    f({ 42 }); //错误:无类型说明符
    f(byte{ 42 }); //正确
    return 0;
}

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

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

float value = 0x1111f;

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

新标准中,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);
void operator delete[](void*, std::size_t size, std::align_val_t alignment);

参数说明:

size —— 分配的字节数。必须为alignment的整数倍。
alignment —— 指定的对齐内存值。必须是实现支持的合法对齐。
new的返回值:

成功,返回指向新分配内存起始地址的指针。

用例:

struct alignas(8) A{};
int main() {
    A *a = static_cast<A *>(::operator new(sizeof(A),static_cast<align_val_t>(alignof(A))));
    ::operator delete(a, sizeof(A), static_cast<std::align_val_t>(alignof (A)));
    return 0;
}

11)细化表达式的计算顺序

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

如以下有争议代码段:

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

为解决此情况,新的计算顺序规则为:

a)后缀表达式从左到右求值。包括函数调用和成员选择表达式。

b)赋值表达式从右到左求值。包括复合赋值。

c)从左到右计算机位移操作符的操作数。

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

在c++17之前,类模板构造器的模板参数不能像函数模板的参数一样能被自动推导,比如我们无法写:

std::pair a{1,"a"s}; //c++17
std::pair<int, string> b{1, "b"s}; // C++14及之前
在c++14之前,为了弥补这一缺陷,标准库提供std::make_pair函数,通过函数模板的模板参数实现类型推导。

//相当于std::pair<int, string> c = std::make_pair<int, string>(1, string("c"));
//这里编译器根据 std::make_pair 所带参数的类型,自动推导出了函数模板的参数。
auto c = std::make_pair(1,"c"s); 

这个解决方案不太理想,因为:

a)需要记住make_pair,make_tuple这类用于构造模板类的用法。

b)有些make_xxx函数在功能上并不等价于类模板的构造器,比如make_shared等。

在c++17中,这个问题得到了解决,类的模板构造器的模板参数同样能够被自动推导。

std::pair a{1, "a"s}; // C++17
// 相当于
// std::pair<int, string> a{1, "a"s};
// 和函数模板一样,这里编译器根据 std::pair 构造器所带参数类型,自动推导出了构造器模板的参数。

示例:

vector a1 ={1,2,3};//c++17
vector<int> a2 = {1, 2, 3};  // C++14

function f1 = [](int a){return a + 1;};  // C++17
function<int(int)> f2 = [](int a){return a + 1;};  // C++14

tuple t1{1, 2,5, "a"s};  // C++17
tuple<int, double, string> t2{1, 2.5, "a"s};  // C++14
auto t = make_tuple(1, 2,5, "a"s);  // C++14

sort(a1.begin(), a1.end(), greater{}); // C++17
sort(a1.begin(), a1.end(), greater<>{});  // C++14
sort(a1.begin(), a1.end(), greater<int>{});  // C++11

map m1 = {std::pair{1, "a"s}, {2, "b"s}}; // C++17
map<int, string> m2 = {{1, "a"s}, {2, "b"s}}; // C++14

自定义类模板中的应用

template<typename T>
struct Container{
    Container(T * prt){} //构造器1
    Container(T &v){} //构造器2
    Container(T const& v) {} // 构造器 3

    template<typename D>
    Container(T* ptr, D& deleter) {} // 构造器 4
};
struct Deleter {};

int main() {
    Container c{(int*)0}; // 调用构造器 1
    int x; Container c2{x}; // 调用构造器 2
    Container c3{0}; // 调用构造器 3
    Deleter d;
    Container c4{(int*)0, d}; // 调用构造器 4
    //以上编译器自动推导的结果都是 Container<int>
    return 0;
}

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

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

14)不支持、非标准的属性

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

15)改下继承构造函数

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

例一

struct B1
{
    B1(int) { std::cout << "B1" << std::endl; }
};

struct D1 : B1 {
    using B1::B1;//表示继承B1的构造函数
};

int main() {
    D1 d1(0);    //正确,委托基类构造函数进行初始化,调用B1::B1(int)
    return 0;
}

例二

struct B1
{
    B1(int) { std::cout << "B1" << std::endl; }
};

struct B2
{
    B2(int) { std::cout << "B2" << std::endl; }
};
struct D1 : B1, B2 {
    using B1::B1;//表示继承B1的构造函数
    using B2::B2;//表示继承B2的构造函数
};
struct D2 : B1, B2
{
    using B1::B1;
    using B2::B2;
    //正确,D2::D2(int)隐藏了B1::B1(int)和B2::B2(int)。另外由于B1和B2没有默认的构造函数,因此必须显式调用B1和B2的构造函数
    D2(int) : B1(1), B2(0)
    { std::cout << "D2" << std::endl; }
};

struct D3 : B1
{
    using B1::B1;
};
D3 d3(0);//正确,继承B1的构造函数,即利用B1的构造函数来初始化,输出B1

int main() {
    D2 d(100);//编译通过,输出B1   B2   D2
    return 0;
}

16)内联变量

参见1.5

17)用auto作为非类型模板参数

当模板参数作为非类型时,可用auto自动推导类型。

template<auto T>
void foo()
{
    cout<<T<<endl;
}
int main() {
    foo<100>();//输出100
    foo<int>(); //error: no matching function for call to 'foo<int>()'
    return 0;
}

3 宏

1)__has_include

判断有没有包含某文件。

int main() {

    #if __has_include(<vector>)
     std::cout <<"<vector> has included"<<std::endl;
    #endif
    #if __has_include("istream")
        std::cout << "iostream has included" << std::endl;
    #endif

    return 0;
}

4 属性

1)fallthrough

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

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

2)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;
}

3)mybe_unused

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

[[maybe_unused]] class A {};
[[maybe_unused]] enum B {};
[[maybe_unused]] int C;
[[maybe_unused]] void fun(){};
posted @ 2021-03-25 12:06  钟齐峰  阅读(1143)  评论(0编辑  收藏  举报