C++17新特性(一)基本语言特性

1. 结构化绑定

假设你有两个不同成员的结构体:

struct MyStruct
{int i = 0;std::string s;
};
MyStruct ms;

你可以通过如下声明直接把两个成员绑定到新的变量名:

auto[u,v] = ms;

这种声明方式就称为结构化绑定。

下面这段代码演示了结构化绑定带来的好处。在不使用结构化绑定遍历std::map是这样的。

for(const auto& elem : mymap)
{cout << elem.first << " " << elem.second << endl;
}

我们知道map中每一个元素都是一个pair类型,使用结构化绑定:

for(const auto&[key,value] : mymap)
{cout << key << " " << value<< endl;
}
1.1 细说结构化绑定
auto [u,v] = ms;

上面这段代码等价于:

auto e = ms;
aliasname u = e.i;
aliasname v = e.s;

这就意味着u和v是ms的一份成员拷贝的别名。因此,修改u和v变量并不会影响结构体成员的数据,反过来也一样。

当我们将结构化绑定声明为引用,修改变量的值会影响结构体变量的值:

auto& [u,v] = ms;u = 10;
cout << ms.i << endl; // 10

除此之外,使用auto结构化绑定也不会发生类型退化(decay)。例如,我们有一个结构体包含两个原生数组:

struct S
{const char x[6];const char y[3];
};S s1{};
auto [a,b] = s1;

a仍然是const char[6],b仍然是const char[3]

在move语义下,也遵循介绍的规则。被移走的对象会处于一个未定义但却有效的状态。不要对打印的值做任何假设。

MyStruct ms = {42,"Jim"};
auto&& [v,n] = std::move(ms);
cout << v << endl; // 42;
cout << ms.i << endl; // 未定义
1.2 结构化绑定的适用场景

结构化绑定适用以下场景:

  • 对于所有非静态数据成员都是public的结构体或者类。
  • 对于原生数组,可以把每一个元素都绑定在新的变量上。
  • 对于任何类型,可以使用tuple-like API来绑定新的名称,对于一个类型type需要如下组件:
    • std::tuple_size<type>::value要返回元素的数量。
    • std::tuple_element<idx,type>::type返回第idx个元素的类型。
    • 一个全局或成员函数get<idx>()要返回第idx个元素的值。

标准库类型std::pair<>std::tuple<>std::array<>就是提供了这些API。

有的时候,如果结构化绑定的所有元素并非自己想要的,你可以使用_来作为名称,但是同一个作用域只能使用一次。

auto [_,val1] = ms;
1.2.1 结构体和类

结构体绑定需要继承时遵循一定的规则。成员要么直接来自最终的类,要么全部来自一个父类。

struct B
{int a = 1;int b = 2;
};
struct D : B {};auto [x,y] = D{}; // OKstruct D1 : B
{int c = 3;
};auto [i,j,k] = D1{}; // ERROR
1.2.2 原生数组
int arr[] = {47,11};
auto [x,y] = arr;
auto [z] = arr; // ERROR

当数组长度已知是才可以使用结构化绑定。数组按值传入的参数不能使用结构化绑定,否则会退化为相应的指针类型。

C++允许通过引用来返回带有大小信息的数组,结构化绑定可以应用于返回这种数组的函数:

auto getArr() -> int(&)[2];
auto [x,y] = getArr();
1.2.3 pair,tuple,array

结构化绑定机制是可扩展的,你可以为任何类型都添加对结构化绑定的支持。标准库就对pairtuplearray添加了支持。

array<int,4> getArray();
auto [a,b,c,d] = getArray();tuple<char,float,std::string> getTuple();
auto [a,b,c] = getTuple();std::map<std::string,int> coll;
auto [pos,ok] = coll.insert({"new",42});
if(!ok) // 插入失败
{// ...
}   

在声明了一个结构化绑定之后,你通常不能同时修改所有绑定的变量,因为结构化绑定只能一起声明但不能一起使用。然而,如果被赋的值可以赋予一个pair或者tuple,你可以使用tie()把值一起赋给变量,例如:

tuple<char,float,std::string> getTuple();auto [a,b,c] = getTuple();tie(a,b,c) = getTuple(); // a和b和c三个值同时被修改
1.3 为结构化绑定提供Tuple-Like API

你可以通过提供tuple-like API为任何类型添加结构化绑定的支持。

支持只读结构化绑定

class Customer
{
private:string first;string last;long val;
public:Customer(string f, string l, long v): first(f), last(l), val(v){}string getFirst() const{return first;}string getLast() const{return last;}long getVal() const{return val;}
};template<>
struct tuple_size<Customer>
{static constexpr int value = 3; // 有三个成员变量
};// 指定下标为2的类型为long
template<>
struct tuple_element<2, Customer>
{using type = long; // 最后一个类型是long
};
// 指定其他下标的类型为string
template<size_t Idx>
struct tuple_element<Idx, Customer>
{using type = string;
};// 定义特化的getter
//template<size_t> auto get(const Customer& c);
//template<> auto get<0>(const Customer& c) { return c.getFirst(); }
//template<> auto get<1>(const Customer& c) { return c.getLast(); }
//template<> auto get<2>(const Customer& c) { return c.getVal(); }// 可以使用C++17支持的编译期if语句特性
template<size_t Idx>
auto get(const Customer& c)
{static_assert(Idx < 3);if constexpr (Idx == 0)return c.getFirst();else if constexpr (Idx == 1)return c.getLast();elsereturn c.getVal();
}

有了这个,我们可以对自定义类支持只读结构化绑定操作:

int main()
{Customer c{ "Tim","Starr",42 };auto [f, l, v] = c;cout << f << l << v << endl;
}

支持可写结构化绑定

class Customer
{
private:string first;string last;long val;
public:Customer(string f, string l, long v): first(f), last(l), val(v){}const string& getFirst() const{return first;}string& getFirst() {return first;}const string& getLast() const{return last;}string& getLast() {return last;}const long& getVal() const{return val;}long& getVal() {return val;}
};template<>
struct tuple_size<Customer>
{static constexpr int value = 3; // 有三个属性
};// 指定下标为2的类型为long
template<>
struct tuple_element<2, Customer>
{using type = long; // 最后一个类型是long
};
// 指定其他下标的类型为string
template<size_t Idx>
struct tuple_element<Idx, Customer>
{using type = string;
};// 定义特化的getter
template<size_t Idx>
decltype(auto) get(Customer& c)
{static_assert(Idx < 3);if constexpr (Idx == 0)return c.getFirst();else if constexpr (Idx == 1)return c.getLast();elsereturn c.getVal();
}template<size_t Idx>
decltype(auto) get(const Customer& c)
{static_assert(Idx < 3);if constexpr (Idx == 0)return c.getFirst();else if constexpr (Idx == 1)return c.getLast();elsereturn c.getVal();
}template<size_t Idx>
decltype(auto) get(Customer&& c)
{static_assert(Idx < 3);if constexpr (Idx == 0)return std::move(c.getFirst());else if constexpr (Idx == 1)return std::move(c.getLast());elsereturn c.getVal();
}

必须提供3个版本的特化来处理常量对象、非常量对象、可移动对象。为了能返回引用,使用decltype(auto)来作为返回类型。

2. 带初始化的if和switch语句

if和switch语句允许在条件表达式添加一条初始化语句。

// s只在if语句里有效
if(status s = check(); s != status::success)
{return s;
}
2.1 带初始化的if语句

在if语句的条件表达式里定义的变量在整个if语句有效:

if(ostream strm = getLogStrm(); coll.empty())
{cout << "<no data>" << endl;
}
else 
{for(const auto& elem : coll){strm << elem << "\n";}
}

另一个例子是锁的使用:

if(lock_guard<mutex> lg{collMutex}; !coll.empty())
{cout << coll.front() << '\n';
}// 等价于
{lock_guard<mutex> lg{collMutex}; if(!coll.empty()){cout << coll.front() << '\n';}
}
2.2 带初始化的switch语句

例如,我们可以声明一个文件系统路径,根据它的类别进行处理:

#include <filesystem>
namespace fs = std::filesystem; // C++17新增int main()
{string name = "";switch (fs::path p{ name }; status(p).type()){case fs::file_type::not_found:cout << p << "not found\n";break;case fs::file_type::directory:cout << p << ":\n";for (const auto& e : fs::directory_iterator{ p }){cout << "-" << e.path() << '\n';}default:cout << p << "exists\n";break;}
}

3. 内联变量

出于可移植性和易于整合的目的,在头文件提供完整的类和库的定义时很重要的。在C++17之前,只有当这个库既不提供也不需要全局对象的时候才可以这样做。

自从C++17开始,你可以在头文件中以inline的方式定义全局变量/对象。

class MyClass
{inline static string msg{"OK"};
};inline MyClass myGlobalObj; // 可以被多个CPP文件包含
3.1 内联变量产生的动机

C++里不允许在类里面初始化非常量静态成员:

class MyClass
{static string msg{"OK"}; // ERROR
};

可如果在类外面初始化非常量静态成员,如果被多个CPP文件同时包含又会引发链接错误:

class MyClass
{static string msg;
};string MyClass::msg{"OK"};

根据一次定义原则,一个变量或者实体的定义只能在一个编译单元内,除非该变量或者实体被定义为inline

对于一些特殊场景,也有一些解决办法:

可以在类内定义中初始化数字或枚举类型的常量静态成员:

class MyClass
{static const bool trace = false; // OK,字面常量
};

然而,这种方法只能初始化字面类型,比如基本的整型、浮点型、指针类型或者用常量表达式初始化了所有内部非静态成员的类,并且该类不能有用户自定义的或虚的析构函数。

3.2 使用内联变量

现在,使用inline修饰符之后,即使定义所在的头文件被多个CPP包含,也只会有一个全局对象:

class MyClass
{inline static string msg{"OK"};
};inline MyClass myGlobalObj; // 可以被多个CPP文件包含

这里使用的inline和函数声明时的inline有相同的定义:

  • 它可以在多个编译单元中定义,只要所有的定义都是相同的。
  • 它必须在每个使用它的编译单元中定义。

注意,你仍然必须确保你初始化内联变量之前它们的类型必须是完整的。例如,如果你有一个自身类型的static成员,这个成员只能在类型声明后在进行定义:

struct MyType
{int value;MyType(int i) : value(i) {}static MyType max; // 声明
};inline MyType MyType::max{0};
3.3 constexpr static成员现在隐含inline

对于静态成员,constexpr修饰符现在隐含inline。自从C++17起,如下声明定义了静态数据成员n:

struct D
{static constexpr int n = 5; // 在C++17,隐含在前面添加了inline
};

在C++17之前,如果只有声明没有定义。如果D::n以引用传递到一个非内联函数,并且该函数调用没有被优化掉的话,会导致错误。

int twice(const int& i);
cout << twice(D::n);

这段代码违反了一次定义原则。如果编译器进行了优化,那么这段代码可能会像预期一样开始工作,也可能因为缺少定义导致链接错误。如果不进行优化,那么几乎肯定会因为缺少D::n的定义而导致错误。

因此,在C++17之前,必须在一个编译单元内定义D::n

constexpr int D::n;
3.4 内联变量和thread_local

通过使用thread_local可以为每个线程创建一个内联变量:

struct ThreadData
{inline static thread_local string name;
};inline thread_local vector<string> cache; // 每个线程都有一份cache

案例:

// ThreadData.hpp
#pragma once
#include <string>
#include <iostream>struct MyData
{inline static std::string gName = "global"; // 整个程序有一个inline static thread_local std::string tName = "tls"; // 每个线程有一个std::string lName = "local"; // 每个实例有一个void print(const std::string& msg) const{std::cout << msg << '\n';std::cout << "-gName:" << gName << '\n';std::cout << "-tName:" << tName << '\n';std::cout << "-lName:" << lName << '\n';}
};inline thread_local MyData myThreadData; // 每个线程有一个对象// main.cpp
#include "ThreadData.hpp"
#include <thread>void foo()
{myThreadData.print("foo() begin:");myThreadData.gName = "thread2 name";myThreadData.tName = "thread2 name";myThreadData.lName = "thread2 name";myThreadData.print("foo() end");
}int main()
{myThreadData.print("main() begin:");myThreadData.gName = "thread1 name";myThreadData.tName = "thread1 name";myThreadData.lName = "thread1 name";myThreadData.print("main() later:");thread t(foo);t.join();myThreadData.print("main() end");}

输出结果:

main() begin:
-gName:global
-tName:tls
-lName:local
main() later:
-gName:thread1 name
-tName:thread1 name
-lName:thread1 name
foo() begin:
-gName:thread1 name
-tName:tls
-lName:local
foo() end
-gName:thread2 name
-tName:thread2 name
-lName:thread2 name
main() end
-gName:thread2 name
-tName:thread1 name
-lName:thread1 name

4. 聚合体扩展

C++有很多初始化对象的方法。其中之一叫做聚合体初始化,这是聚合体转悠的一种初始化方式。

struct Data
{string name;double value;
};Data x = {"test",6.7};

C++11之后,可以忽略等号:

Data x{"test",6.7};

C++17起,聚合体可以拥有基类。并且可以使用如下的初始化方法:

struct MoreData : Data
{bool done;
};MoreData y{{"test",6.7},false};
4.1 扩展聚合体初始化的动机

如果没有这个特性,派生类都不能使用聚合体初始化,也就是必须要实现如下的构造函数:

struct MoreData : Data
{bool done;MoreData(const string& s,double d,bool b) : Data{s,d},done{b} {}
};

C++17起,就无须定义任何构造函数就可以做到:

MoreData y{{"test",6.7},false}; // OK
MoreData y{"test",6.7,false}; // OK
4.2 聚合体的定义

总的来说,C++17中满足如下条件之一的对象被认为是聚合体:

  • 数组
  • 类类型(class、struct、union)
  • 没有用户定义的和explicit的构造函数
  • 没有使用using声明继承的构造函数
  • 没有private和protected的非静态数据成员
  • 没有virtual函数
  • 没有virtual、private、protected的基类

想使用聚合体初始化还必须满足以下约束:

  • 基类中没有privateprotected成员
  • 没有privateprotected的构造函数

C++17引入了一个新的类型特征is_aggregate<>来测试一个类型是否是聚合体:

template<typename T>
struct D : string,complex<T> 
{string data;
};D<float> s{{"hello"},{4.5,6.7},"world"};
cout << is_aggregate<decltype(s)>::value; // 1
4.3 向后的不兼容性

下面这个例子不能通过编译:

struct Derived;struct Base
{friend struct Derived;
private:Base() {}
};struct Derived : Base {}int main()
{Derived d1{}; // C++17之后ERRORDerived d2;
}

C++17之前,Derived不是聚合体。因为,在进行{}创建对象的时候,会调用Derived的默认构造函数,然后子类的默认构造函数又会调用父类的构造函数,即使父类的构造函数是私有的,但是因为派生类被声明为友元类,因此可以调用父类的私有构造函数。

但在C++17之后,Derived是一个聚合体,会默认认为d1是进行聚合体初始化,但是不满足父类的构造函数不能私有的情况,因此会导致不能使用花括号来进行初始化。

5. 强制省略拷贝或传递未实质化的对象

  • C++17引入了一个新的规则:当以值传递或返回一个临时对象的时候,必须省略对该临时对象的拷贝。
  • 从效果上讲,我们实际上是传递了一个未实质化的对象
5.1 强制省略临时变量拷贝的动机

自从第一次标准开始,C++就允许在某些情况下省略拷贝操作,即使这么做可能会影响程序的运行结果。例如:

class MyClass
{// ...
};void foo(MyClass param)
{// ...
}MyClass bar()
{return MyClass{};
}int main()
{foo(MyClass{});MyClass x = bar();foo(bar());
}

然而,这种优化并不是强制性的,也就是说,即使优化之后并不会调用拷贝或者移动构造,但是它们必须存在。

自从C++17起用临时变量初始化对象时省略拷贝变成了强制性。事实上,之后我将会看到我们传递为参数或者作为返回值的临时变量将会被用来实质化一个新的对象。这意味着即使不允许MyClass拷贝,但也能成功编译。

MyClass bar(MyClass obj) // 传递临时变量会省略拷贝
{return obj; // 仍然需要拷贝/移动支持
}
5.2 强制省略临时变量拷贝的作用

这个特性的一个显而易见的作用就是减少拷贝带来更好的性能。尽管很多主流编译器之前就已经对这种进行了优化,但现在这一行为有了标准的保证。尽管移动语义能显著的减少拷贝的开销,但直接不进行拷贝会带来很大的性能提升。另外这个特性可以减少输出参数的使用,转而直接返回一个值。

另一个作用是可以定义一个总是可以工作的工厂函数,因为现在它甚至可以返回不允许拷贝或移动的对象。例如:

#include <utility>template<typename T, typename... Args>
T create(Arg&&... args)
{return T{std::forward<Args>(args)};
}

即使像atomic这种既没有拷贝也没有移动构造的类也是可以使用的:

#include <memory>
#include <atomic>int main()
{int i = create<int>(42);std::unique_ptr<int> up = create<std::unique_ptr<int>>(new int{42});std::atomic<int> ai = cteate<std::atomic<int>>(42);
}

另一个效果就是对于移动构造函数被显示删除的类,也可以返回临时对象来初始化新的对象:

class CopyOnly
{
public:CopyOnly() {}CopyOnly(int) {}CopyOnly(const CopyOnly&) = default;CopyOnly(CopyOnly&&) = delete;
};CopyOnly ret() {return CopyOnly{}; // C++17起OK
}CopyOnly x = 42; // C++17起ok

5.3 更明确的值类型体系

用临时变量初始化新对象时强制省略临时变量拷贝的提议的一个副作用就是,为了支持这个提议,值类型体系进行了很多修改。

5.3.1 值类型体系

C++从C语言继承而来的有左值右值,之后C++11引入了可移动对象。引入了将亡值的概念,原本的右值被重新命名为纯右值

左值的例子:

  • 只含单个变量、函数或成员的表达式。
  • 只含有字符串字面量的表达式。
  • 内建的一元*运算符的结果。
  • 一个返回左值引用的函数的返回值。

纯右值的例子:

  • 除字符串字面量和用户自定义的字面量之外的字面量组成的表达式。
  • 内建的一元&运算符的运算结果。
  • 内建的数学运算符的结果。
  • 一个返回值的函数的返回值。
  • 一个lambda表达式。

将亡值的例子:

  • 一个返回右值引用的函数的返回值。
  • 把一个对象转换为右值引用的操作的结果。

简单来说:

  • 所有用作表达式的变量名都是左值。
  • 所有用作表达式的字符串字面量是左值。
  • 所有其他的字面量(4.2,true,nullptr)是纯右值。
  • 所有临时对象是纯右值。
  • move()的结果是一个将亡值。
class X
{};X v;
const X c;void f(const X&); // 接受任何值类型
void f(X&&); // 只接受纯右值和将亡值f(v); // 传递了一个可以修改的左值
f(c); // 传递了一个不可以修改的左值
f(X()); // 传递了一个纯右值
f(std::move(v));// 传递了将亡值
5.3.2 C++17起的值类型体系

C++17再次明确了值类型体系,从广义上来说,我们只有两种类型的表达式:

  • glvalue:描述对象或函数位置的表达式。
  • prvalue:用于初始化的表达式。

而原本的将亡值可以认为是一种特殊的位置,它代表一个资源可被回收利用的对象。

C++17引入了一个术语,(临时对象)实质化,目前prvalue就是一种临时对象。因此,临时对象实质化转换,就是一种从右值到将亡值的转换。

void f(const X& p); // 可以接受任何值类型
f(X()); // 传递了一个纯右值,该纯右值实质化为将亡值

以上就是实质化的过程,这个过程并没有创建新的对象。因为右值不在是对象而是可以被用来初始化对象的表达式,当使用右值来初始化对象的时候不再需要右值是可移动的,进而省略临时拷贝的特性可以完美实现。

5.4 未实质化的返回值传递

所有以值返回临时对象的过程都是在传递未实质化的返回值:

  • 当我们返回一个非字符串字面量的字面量时:

    int f1() 
    {return 42;
    }
    
  • 当我们用auto或类型名作为返回类型并返回一个临时对象时:

    auto f2()
    {return MyType{};
    }
    
  • 当我们使用decltype(auto)作为返回类型并返回临时对象时:

    decltype(auto) f3()
    {return MyType{};
    }
    

以上场景都是以值返回一个右值,不需要任何拷贝/移动。

6. lambda表达式扩展

C++11引入了lambda表达式和C++14引入的泛型lambda是一个很大的成功。

C++17拓展了lambda表达式的应用场景:

  • 在常量表达式中使用。
  • 在需要当前对象的拷贝时使用。
6.1 constexpr lambda

自从C++17起,lambda表达式会尽可能的隐式声明constexpr。也就是说,任何只使用有效的编译器上下文(只有字面量、没有静态变量、没有虚函数、没有try/catch,没有new/delete)的lambda表达式都可以用作编译期。

例如:

auto squared = [](auto val) // 隐式constexpr
{
return val * val;
};
array<int, squared(5)> a; // C++17起OK

为了确认一个lambda表达式能否用于编译期,你可以声明为constexpr

auto squared = [](auto val) constexpr -> int 
{return val * val;	
};

这个表达式将会转换为如下类型:

class CompilerSpecificName
{
public:template<typename T>constexpr auto operator()(T val) const{return val * val;}
};

注意以下两个定义是不同的:

auto squared1 = [](auto val) constexpr
{return val * val;	
};constexpr auto squared2 = [](auto val)
{return val * val;	
};

第一个例子是lambda表达式可以在编译期调用,第二个例子是编译期会初始化lambda表达式。

6.1.1 使用constexpr lambda

假设我们有一个字符序列的哈希函数,这个函数迭代字符串中的每一个字符反复更新哈希值:

int main(int argc,char *argv[])
{auto hashed = [](const char* str){size_t hash = 5381;while (*str != '\0'){hash = hash * 33 ^ *str++;}return hash;};// 用于enumenum Hashed {beer = hashed("beer"),wine = hashed("wine"),water = hashed("water")};// 用于case标签switch (hashed(argv[1])){case hashed("beer"):break;case hashed("wine"):break;default:break;}
}

如果我们使用编译期lambda表达式初始化一个容器,那么编译器优化时很可能在编译期就计算出容器的值。

array arr{hashed("beer"),wine = hashed("wine"),water = hashed("water")
};

甚至可以在一个constexpr lambda里使用另一个:

auto hashed = [](const char* str, auto combine)
{size_t hash = 5381;while (*str != '\0'){hash = combine(hash,*str++);}return hash;
};constexpr size_t hv1{ hashed("wine", [](auto h, char c) { return h * 33 + c;})};
constexpr size_t hv1{ hashed("wine", [](auto h, char c) { return h * 33 ^ c;})};
6.2 向lambda表达式传递this指针

当在非静态成员函数里使用lambda时,你不能隐式获取该对象成员的使用权。也就是说,如果你不捕获this的话你将不能在lambda里使用该对象的任何成员。

class C
{
private:string name;
public:void foo(){auto l1 = [] {cout << name << '\n'; }; // ERRORauto l2 = [] {cout << this->name << '\n'; }; // ERROR}
};

在C++11和C++14里,可以通过值或引用捕获this:

class C
{
private:string name;
public:void foo(){auto l1 = [this] {cout << name << '\n'; };auto l2 = [=] {cout << this->name << '\n'; };auto l3 = [&] {cout << this->name << '\n'; };}
};

然而,问题是即使用拷贝的方式捕获this实质上获得的也是引用。当lambda表示的生命周期比该对象的生命周期更长的时候,调用这样的函数就可能导致问题。比如,在lambda表达式开启一个线程来完成某些任务,调用新线程时正确的做法是传递整个对象的拷贝来避免并发和生存周期的问题,而不是传递对象的引用。

C++14有一个解决方案:

class C
{
private:string name;
public:void foo(){auto l1 = [thisCopy = *this] {cout << thisCopy.name << '\n'; };}
};

自从C++17起,你可以通过*this来显示地捕获当前对象的拷贝:

class C
{
private:string name;
public:void foo(){auto l1 = [*this] {cout << name << '\n'; };}
};

这里有一个完整的例子:

class Data
{
private:string name;
public:Data(const string& s) : name(s) {}auto startThreadWithCopyOfThis() const{// 开启并返回新线程,新线程在3秒后使用thisusing namespace std::literals; // 可以使用3s,表示3秒thread t([*this] {this_thread::sleep_for(3s);cout << name << "\n";});return t;}
};int main()
{thread t;{Data d{ "c1" };t = d.startThreadWithCopyOfThis();}t.join();
}

7. 新属性和属性特性

从C++11起,可以指明属性。属性是允许或禁用某些警告的注解。C++17引入了新的属性,还扩展了属性的使用场景。

7.1 [[nodiscard]]属性

新属性[[nodiscard]]可以鼓励编译器在某个函数的返回值未被使用时给出警告。应该是防止返回值未被使用会导致的不当行为,可能是内存泄漏、不必要的开销、未知或出乎意料的行为。

一个很好的例子是:std::async()会在后台异步地执行一个任务并返回一个可以用来等待任务执行结束的句柄。然而,如果返回值没有被使用的话该调用将变成同步的调用,因为在启动任务的语句结束之后未被使用的返回值的析构函数会立即执行,而析构函数会阻塞等待任务运行结束。另一个例子是成员函数empty(),它的作用是检查一个对象或者容器是否为空。

class MyContainer 
{[[nodiscard]] bool empty() const noexcept;
};

如果你对一个不想使用被标记的[[nodiscard]]的函数的返回值,你可以吧返回值转换为void,例如:

(void)coll.empty();

注意,如果成员函数被覆盖或者隐藏时基类中的标记不会被继承:

struct B
{[[nodiscard]] int* foo();
};struct D : B
{int* foo();
};D d;
d.foo(); // 没有警告
7.2 [[maybe_unused]]属性

新的属性[[maybe_unused]]可以避免编译器在某个变量未被使用时发出警告。

void foo(int val, [[maybe_unused]]string msg)
{
#ifdef DEBUGlog(msg);
#endif
}

不能对一条语句使用[[maybe_unused]],因此,不能用这个来抵消[[nodiscard]]的作用。

7.3 [[fallthrough]]属性

新的属性[[fallthrough]]可以避免编译器在switch语句中某一标签缺少break发出警告。(比较鸡肋)

void commentPlace(int place)
{switch(place){case 1:cout << "very";[[fallthrough]];case 2:cout << "well";break;default:break;}
}
7.4 通用的属性扩展

自从C++17起,下列有关属性的通用特性变得可用:

  • 属性现在可以用来标记命名空间。例如,弃用一个命名空间:
namespace [[deprecated]] DraftAPI
{// ...
}

也可以引入新的一个枚举值作为已有枚举值的替代:

enum class City 
{Berlin = 0,NewYork = 1,Mumbai = 2,Bombay [[deprecated]] = Mumbai
};

8. 其他语言特性

C++17中一些微小的核心语言特性。

8.1 嵌套命名空间
namespace A::B::C
{// ...
}// 上面代码等价于
namespace A
{namespace B{namespace C{}}
}
8.2 有定义的表达式求值顺序

先看一个例子,在一个字符串中替换多个子串:

string s = "I heard it even works if you don't believe";
s.replace(0,8,"").replace(s.find("even"),4,"sometimes").replace(s.find("you don't"),9,"I");

通常的假设是前8个字符被空串替换,even替换成sometimesyou don't替换成I。结果是:

it sometimes works if I believe

然而在C++17之前最后的结果并没有任何保证。因为查找子串位置的find()函数可能在需要它们的返回值之前的任意时刻调用。事实上,所有的find()调用可能在执行第一次替换之前就全部执行,因此结果为:

it even worsometimesf youIlieve

也可能是:

it sometimes workIdon't believe
it even worsometiIdon't believe

另外一个例子是,输出运算符打印几个相互依赖的值:

cout << f() << g() << h();

为了解决这种未定义的问题,C++17标准重新定义了一些运算符的求值顺序,因此这些运算符有了固定的求职顺序:

  • 对于运算
    • e1[e2]
    • e1.e2
    • e1.*e2
    • e1->*e2
    • e1 << e2
    • e1 >> e2

e1现在保证一定会在e2之前求值,因此求值顺序是从左到右的。然而,同一个函数调用中的不同参数的计算顺序仍然是未定义的。

e1.f(a1,a2,a3);

a1和a2和a3的求值顺序让人是未定义的。

  • 对于赋值运算:
    • e2 = e1
    • e2 += e1
    • e2 *= e1

e1现在保证一定会在e2之前求值。

  • new表达式

因此,自从C++17起,会保证replace()操作在find()操作之前。但是对于大多数运算符还是未知的,比如:

i = 0;
i = i++ + i;

这样的修改可能会影响现有程序的输出。例如:

void print10elems(const vector<int>& v)
{for (int i = 0; i < 10; ++i){cout << "value: " << v.at(i) << '\n';}
}int main()
{try{vector<int> vec{ 7,14,21,28 };print10elems(vec);}catch (const exception& e){cerr << "EXCEPTION:" << e.what() << '\n';}catch (...){cerr << "EXCEPTION of unknown type\n";}
}

C++17之前可能的结果是:

value: 7
value: 14
value: 21
value: 28
EXCEPTION: ..

C++17之后的结果,保证是:

value: 7
value: 14
value: 21
value: 28
value: EXCEPTION: ..
8.3 更宽松的用整型初始化枚举值的规则

对于一个有固定底层类型的枚举类型变量,C++17起可以用一个整型值进行列表初始化。

// 指明底层类型但无作用域枚举类型
enum MyInt : char {};
MyInt i1{42}; // C++17起OK// 默认底层类型有作用域枚举
enum class Weekday { mon, tue, wed, thu, fri, sat, sun };
Weekday w1{0}; // C++17起OK// 指明底层类型且有作用域枚举
enum class Weekday : char { mon, tue, wed, thu, fri, sat, sun };
Weekday w2{0}; // C++17起OK// 没有指明底层类型也无作用域枚举类型
enum Flag { bit = 1, bit2 = 2, bit3 = 3 };
Flag f1{0}; // ERROR
8.4 修正auto类型的列表初始化

自从C++17中引入了花括号统一初始化之后,每当使用auto代替明确类型进行初始化就会出现一些和直觉不一致的结果:

int x{42};
int y{1,2,3}; // ERROR
auto a{42}; // initializer_list<int>
auto b{1,2,3}; // initializer_list<int>

这些直接使用初始化列表时不一致的行为现在已经被修复了。

int x{42};
int y{1,2,3}; // ERROR
auto a{42};  // int
auto b{1,2,3};  // ERROR

注意,这是一个破坏性的更改,可能导致许多代码的行为无法使用。

注意,当使用auto进行拷贝列表初始化时仍然是initializer_list

auto a = {42}; // initializer_list<int>
auto b = {1,2,3}; // initializer_list<int>
8.5 十六进制浮点数字面量8

C++17允许指定十六进制浮点数字面量。

#include <iomanip>int main()
{initializer_list<double> values{0x1p4, // 1 * 4 ^ 2 = 160xA,   // A = 100xAp2, // 10 * 2 ^ 2 = 405e0,   // 5 * 1.0 = 50x1.4p+2,  // 1.25 * 2 ^ 5 = 51e5,    // 1 * 1.0 ^ 5 = 1000000x1.86Ap+16, // 1000000xC.68p+2 // 49.625};
}
8.6 UTF-8字符字面量

自从C++11起,C++支持u8为前缀的UTF-8字符串字面量。然而,C++17之前,这个前缀不能用于字符字面量。C++17修复了这个问题。

auto c = u8'6';
8.7 异常声明作为类型的一部分

自从C++17之后,异常处理声明变成了函数类型的一部分。也就是说,如下的两个函数的类型不同:

void MightThrow();
void Noexcept() noexcept;

在C++17之前,这两个类型相同,就有可能将一个可能抛出异常的函数赋给一个不会抛出异常的函数指针:

void (*fp)() noexcept; 
fp = fNoexcept;
fp = MightThrow; // C++17起ERROR

但是如果将一个不会抛出异常的函数赋给一个可能抛出异常的函数指针仍然有效:

void (*fp2)(); 
fp = fNoexcept;
fp = MightThrow; 

不仅如此,在派生类重写基类的函数时,也是符合这一规则。

class Base
{
public:virtual void foo() noexcept;
};class Derived : Base
{
public:void foo() override; // ERROR
};

使用传统的异常声明时,函数的是否抛出取决于条件为true或者false

void f1();
void f2() noexcept;
void f3() noexcept(sizeof(int)<4); 

noexcept作为类型的一部分会对泛型库产生一些影响。例如:


template<typename T>
void call(T op1, T op2)
{op1();op2();
}void f1()
{cout << "f1()\n";
}void f2() noexcept
{cout << "f2()\n";
}int main()
{call(f1, f2); // C++17起ERROR
}
8.8 单参数static_assert

C++17起,static_assert()的错误信息的参数变为可选。

#include <type_traits>template<typename T>
class C
{static_assert(is_default_constructible_v<T>); // C++17起有效
};
8.9 预处理条件__has_include

C++17扩展了预处理,增加了一个检查某个头文件是否可以被包含的宏,例如:

#if __has_include(<filesystem>)
#include <filesystem>
#define HAS_FILESYSTEM 1
#elif __has_include (<experimental/filesystem>)
#include <experimental/filesystem>
#define HAS_FILESYSTEM 1
#define FILESYSTEM_IS_EXPERIMENTAL 1
#else 
#define HAS_FILESYSTEM 0
#endif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/636211.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

代码随想录算法训练营day24 || 回溯法原理讲解,77.组合

回溯方法的理论原理与定义 回溯算法是潜藏于递归过程之中一种操作&#xff0c;与递归操作相辅相成&#xff1b;初步理解&#xff0c;有递归必有回溯&#xff0c;使用回溯最好的方式是递归&#xff0c;至于其他的方式有待探索。回溯是一种多重循环的变体&#xff0c;其本质就是…

Qt事件过滤

1.相关说明 监控鼠标进入组件、出组件、点击组件、双击组件的事件&#xff0c;需要重写eventFilter函数 2.相关界面 3.相关代码 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui-&…

数据库(MySQL库表操作)

目录 1.1 SQL语句基础&#xff08;SQL命令&#xff09; 1.1.1 SQL的简介 1.1.2 SQL语句的分类 1.1.3 SQL语句的书写规范 1.2 数据库操作 1.2.1 查看 1.2.2 自建库 1.2.3 切换数据库 1.2.4 删库 1.3 MySQL字符集 1.3.1 MySQL字符集包括&#xff1a; 1.3.2 utf8 和 u…

Linux设备管理模型-01:基础数据结构

文章目录 1. 设备管理模型2. 基本数据结构2.1 kobject2.2 kset 1. 设备管理模型 设备模型是内核提供的一个编写驱动的架构。 设备管理是设备-总线-驱动结构。 linux中的设备是由树状模型组织的&#xff0c;从sysfs中可以查看树状结构。 他本身实现了&#xff1a; 电源管理热…

汽车制动器行业调查:市场将继续呈现稳中向好发展态势

汽车制动器是汽车的制动装置&#xff0c;汽车所用的制动器几乎都是摩擦式的&#xff0c;可分为鼓式和盘式两大类。鼓式制动器摩擦副中的旋转元件为制动鼓&#xff0c;其工作表面为圆柱面;盘式制动器的旋转元件则为旋转的制动盘&#xff0c;以端面为工作表面。 目前市场上主流的…

JAVA和C++ SECS/GEM300开发和概念

编译SECS示例程序 1. 示例程序使用默认路径&#xff1a; D:\SECS 稳定版\SECS Debug\ 2. 该操作分为俩步 ① 将C#的Secs库编译成设备相同Net版本。 如.net3.5、4.0、4.5等等 ② 编译金南瓜SECS demo程序 编译C#的SecsEquip.dll 1. 找到SecsEquip项目 项目文件 使用Visua…

JS的作用域链是静态的,它的取值是在创建阶段而不是调用阶段

问题 对于下面这段代码&#xff0c;您觉得会输出什么&#xff1f; var x 10 function fn() {console.log(x) } function show(f) {var x 20;(function () {f() // 10&#xff0c;而不是 20})() } show(fn)由于第8行的f()执行的就是第2行定于你的fn()函数&#xff0c;而第二行…

麒麟V10挂载iso,配置yum源

本文介绍yum 如何挂载本地镜像源 1) 拷贝镜像到本地 2) 执行以下命令&#xff1a; # mount -o loop 镜像路径及镜像名字 /mnt&#xff08;或 media&#xff09; 挂载前 挂载后 3) 进入/etc/yum.repos.d&#xff08;yum.repos.d 是一个目录&#xff0c;该目录是分析 RPM 软件…

操作系统的一些知识

一、操作系统 1、操作系统的定义 操作系统是一个搞管理的软件。 对下&#xff0c;要管理硬件设备&#xff1b;对上&#xff0c;要给软件提供稳定的运行环境。 操作系统是软件、硬件、用户之间交互的媒介。 2、常见的操作系统 Windows、Linux、Mac 3、操作系统的定位 我们平…

2024华数杯国际数学建模A题思路模型详解

2024华数杯国际数学建模A题思路论文&#xff1a;1.17上午第一时间持续更新&#xff0c;详细内容见文末名片 建立一个模型来描述放射性废水在海水中的扩散速率和方向&#xff0c;考虑到涉及的物理过程和环境因素的复杂性&#xff0c;我们通常会使用一个简化的扩散模型作为起点…

视频剪辑教程:如何批量制作滚动字幕,提升画面质感的方法

在视频剪辑中&#xff0c;字幕的处理是至关重要的一环。合适的字幕不仅能提供必要的信息&#xff0c;还能增强画面的视觉效果。下面详解云炫AI智剪如何批量制作滚动字幕&#xff0c;提升画面质感的方法&#xff0c;助您更好地完成视频剪辑工作。 批量制作滚动字幕的方法&#x…

Mysql详细安装步骤

Linux 安装 MySQL【超详细版】 ​编辑 我叫BuGu    2023-05-11 16:48:10 发布 一、安装 MySQL 的准备工作 1. 查看系统版本 cat /etc/redhat-release2. 查看系统是否已经安装过 MySQL 查看是否安装了 MySQL rpm -qa | grep mysql查看是否有安装 mariadb,该软件与 MySQ…

逆向分析C++类的本质

面向对象的语言中&#xff0c;类这种语言特性是最基本也是最重要的东西。这篇博客记录下从汇编角度去理解类的本质是什么。创建一个对象的本质又是什么。 一.C语言中的结构体和C的类有啥区别 我们知道在C语言中&#xff0c;有语言本身自带的一些内置类型。比如int&#xff0c…

kafka(一)——简介

简介 Kafka 是一种分布式、支持分区、多副本的消息中间件&#xff0c;支持发布-订阅模式&#xff0c;多用于实时处理大量数据缓存的场景&#xff0c;类似于一个“缓存池”。 架构 Producer&#xff1a;消息生产者&#xff1b;Consumer&#xff1a;消息消费者&#xff1b;Brok…

SpringCloud之Nacos的学习、快速上手

1、什么是Nacos Nacos是阿里的一个开源产品&#xff0c;是针对微服务架构中的服务发现、配置管理、服务治理的综合型解决方案&#xff0c;用来实现配置中心和服务注册中心。 Nacos 快速开始 2、安装运行nacos nacos下载地址 下载地址: https://github.com/alibaba/nacos/rel…

【Linux】Linux系统的生态

Linux中安装软件 Linux中安装软件一般有三种方式&#xff1a; 源代码安装rpm包安装yum安装 1.源代码安装 有些软件本来就是开源的&#xff0c;如果不想用别人直接发布好的软件&#xff0c;我们就可以把源代码下载下来&#xff0c;在我们的环境中编译&#xff0c;自己安装 …

防伪技术行业研究:年复合增长率约为10%

近年来&#xff0c;我国各种新的防伪技术不断涌现&#xff0c;部分防伪技术已经达到国际先进水平&#xff0c;并广泛应用于产品防伪、票证防伪等领域&#xff0c;推动了防伪行业的持续、健康发展。 常见的产品防伪技术有&#xff1a;隐形分子技术、二维码防伪、揭开留底防伪、安…

【设计模式 创建型】单例模式

类的单例设计模式&#xff0c;就是采取一定的方法保证在整个的软件系统中&#xff0c;对某个类只能存在一个对象实例&#xff0c;并且该类只提供一个取得其对象实例的方法&#xff08;静态方法&#xff09; 指一个类只有一个实例&#xff0c;且该类能自行创建这个实例的一种模…

「Kafka」Broker篇

「Kafka」Broker篇 主要讲解的是在 Kafka 中是怎么存储数据的&#xff0c;以及 Kafka 和 Zookeeper 之间如何进行数据沟通的。 Kafka Broker 总体工作流程 Zookeeper 存储的 Kafka 信息 启动 Zookeeper 客户端&#xff1a; [atguiguhadoop102 zookeeper-3.5.7]$ bin/zkCli.sh通…

时间复杂度的排序

在计算机科学中&#xff0c;不同的算法有不同的时间复杂度。以下是一些常见的时间复杂度&#xff0c;并按照它们的增长速度从低到高排序&#xff1a; O(1) - 常数时间复杂度&#xff1a; 表示算法的执行时间是固定的&#xff0c;不随输入规模的增加而变化。例如&#xff0c;直接…