企业响应式网站建设报价,免费制作网页的网站,有做外贸的平台网站吗,网站建设 有道翻译1. 结构化绑定
假设你有两个不同成员的结构体#xff1a;
struct MyStruct
{int i 0;std::string s;
};
MyStruct ms;你可以通过如下声明直接把两个成员绑定到新的变量名#xff1a;
auto[u,v] ms;这种声明方式就称为结构化绑定。
下面这段代码演示了结构化绑定带来的好…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_sizetype::value要返回元素的数量。std::tuple_elementidx,type::type返回第idx个元素的类型。一个全局或成员函数getidx()要返回第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{}; // ERROR1.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
结构化绑定机制是可扩展的你可以为任何类型都添加对结构化绑定的支持。标准库就对pair、tuple、array添加了支持。
arrayint,4 getArray();
auto [a,b,c,d] getArray();tuplechar,float,std::string getTuple();
auto [a,b,c] getTuple();std::mapstd::string,int coll;
auto [pos,ok] coll.insert({new,42});
if(!ok) // 插入失败
{// ...
} 在声明了一个结构化绑定之后你通常不能同时修改所有绑定的变量因为结构化绑定只能一起声明但不能一起使用。然而如果被赋的值可以赋予一个pair或者tuple你可以使用tie()把值一起赋给变量例如
tuplechar,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_sizeCustomer
{static constexpr int value 3; // 有三个成员变量
};// 指定下标为2的类型为long
template
struct tuple_element2, Customer
{using type long; // 最后一个类型是long
};
// 指定其他下标的类型为string
templatesize_t Idx
struct tuple_elementIdx, Customer
{using type string;
};// 定义特化的getter
//templatesize_t auto get(const Customer c);
//template auto get0(const Customer c) { return c.getFirst(); }
//template auto get1(const Customer c) { return c.getLast(); }
//template auto get2(const Customer c) { return c.getVal(); }// 可以使用C17支持的编译期if语句特性
templatesize_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_sizeCustomer
{static constexpr int value 3; // 有三个属性
};// 指定下标为2的类型为long
template
struct tuple_element2, Customer
{using type long; // 最后一个类型是long
};
// 指定其他下标的类型为string
templatesize_t Idx
struct tuple_elementIdx, Customer
{using type string;
};// 定义特化的getter
templatesize_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();
}templatesize_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();
}templatesize_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_guardmutex lg{collMutex}; !coll.empty())
{cout coll.front() \n;
}// 等价于
{lock_guardmutex lg{collMutex}; if(!coll.empty()){cout coll.front() \n;}
}2.2 带初始化的switch语句
例如我们可以声明一个文件系统路径根据它的类别进行处理
#include filesystem
namespace fs std::filesystem; // C17新增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. 内联变量
出于可移植性和易于整合的目的在头文件提供完整的类和库的定义时很重要的。在C17之前只有当这个库既不提供也不需要全局对象的时候才可以这样做。
自从C17开始你可以在头文件中以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。自从C17起如下声明定义了静态数据成员n
struct D
{static constexpr int n 5; // 在C17隐含在前面添加了inline
};在C17之前如果只有声明没有定义。如果D::n以引用传递到一个非内联函数并且该函数调用没有被优化掉的话会导致错误。
int twice(const int i);
cout twice(D::n);这段代码违反了一次定义原则。如果编译器进行了优化那么这段代码可能会像预期一样开始工作也可能因为缺少定义导致链接错误。如果不进行优化那么几乎肯定会因为缺少D::n的定义而导致错误。
因此在C17之前必须在一个编译单元内定义D::n。
constexpr int D::n;3.4 内联变量和thread_local
通过使用thread_local可以为每个线程创建一个内联变量
struct ThreadData
{inline static thread_local string name;
};inline thread_local vectorstring cache; // 每个线程都有一份cache案例
// ThreadData.hpp
#pragma once
#include string
#include iostreamstruct 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 threadvoid 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 name4. 聚合体扩展
C有很多初始化对象的方法。其中之一叫做聚合体初始化这是聚合体转悠的一种初始化方式。
struct Data
{string name;double value;
};Data x {test,6.7};C11之后可以忽略等号
Data x{test,6.7};C17起聚合体可以拥有基类。并且可以使用如下的初始化方法
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} {}
};C17起就无须定义任何构造函数就可以做到
MoreData y{{test,6.7},false}; // OK
MoreData y{test,6.7,false}; // OK4.2 聚合体的定义
总的来说C17中满足如下条件之一的对象被认为是聚合体
数组类类型class、struct、union没有用户定义的和explicit的构造函数没有使用using声明继承的构造函数没有private和protected的非静态数据成员没有virtual函数没有virtual、private、protected的基类
想使用聚合体初始化还必须满足以下约束
基类中没有private和protected成员没有private和protected的构造函数
C17引入了一个新的类型特征is_aggregate来测试一个类型是否是聚合体
templatetypename T
struct D : string,complexT
{string data;
};Dfloat s{{hello},{4.5,6.7},world};
cout is_aggregatedecltype(s)::value; // 14.3 向后的不兼容性
下面这个例子不能通过编译
struct Derived;struct Base
{friend struct Derived;
private:Base() {}
};struct Derived : Base {}int main()
{Derived d1{}; // C17之后ERRORDerived d2;
}C17之前Derived不是聚合体。因为在进行{}创建对象的时候会调用Derived的默认构造函数然后子类的默认构造函数又会调用父类的构造函数即使父类的构造函数是私有的但是因为派生类被声明为友元类因此可以调用父类的私有构造函数。
但在C17之后Derived是一个聚合体会默认认为d1是进行聚合体初始化但是不满足父类的构造函数不能私有的情况因此会导致不能使用花括号来进行初始化。
5. 强制省略拷贝或传递未实质化的对象
C17引入了一个新的规则当以值传递或返回一个临时对象的时候必须省略对该临时对象的拷贝。从效果上讲我们实际上是传递了一个未实质化的对象。
5.1 强制省略临时变量拷贝的动机
自从第一次标准开始C就允许在某些情况下省略拷贝操作即使这么做可能会影响程序的运行结果。例如
class MyClass
{// ...
};void foo(MyClass param)
{// ...
}MyClass bar()
{return MyClass{};
}int main()
{foo(MyClass{});MyClass x bar();foo(bar());
}然而这种优化并不是强制性的也就是说即使优化之后并不会调用拷贝或者移动构造但是它们必须存在。
自从C17起用临时变量初始化对象时省略拷贝变成了强制性。事实上之后我将会看到我们传递为参数或者作为返回值的临时变量将会被用来实质化一个新的对象。这意味着即使不允许MyClass拷贝但也能成功编译。
MyClass bar(MyClass obj) // 传递临时变量会省略拷贝
{return obj; // 仍然需要拷贝/移动支持
}5.2 强制省略临时变量拷贝的作用
这个特性的一个显而易见的作用就是减少拷贝带来更好的性能。尽管很多主流编译器之前就已经对这种进行了优化但现在这一行为有了标准的保证。尽管移动语义能显著的减少拷贝的开销但直接不进行拷贝会带来很大的性能提升。另外这个特性可以减少输出参数的使用转而直接返回一个值。
另一个作用是可以定义一个总是可以工作的工厂函数因为现在它甚至可以返回不允许拷贝或移动的对象。例如
#include utilitytemplatetypename T, typename... Args
T create(Arg... args)
{return T{std::forwardArgs(args)};
}即使像atomic这种既没有拷贝也没有移动构造的类也是可以使用的
#include memory
#include atomicint main()
{int i createint(42);std::unique_ptrint up createstd::unique_ptrint(new int{42});std::atomicint ai cteatestd::atomicint(42);
}另一个效果就是对于移动构造函数被显示删除的类也可以返回临时对象来初始化新的对象
class CopyOnly
{
public:CopyOnly() {}CopyOnly(int) {}CopyOnly(const CopyOnly) default;CopyOnly(CopyOnly) delete;
};CopyOnly ret() {return CopyOnly{}; // C17起OK
}CopyOnly x 42; // C17起ok5.3 更明确的值类型体系
用临时变量初始化新对象时强制省略临时变量拷贝的提议的一个副作用就是为了支持这个提议值类型体系进行了很多修改。
5.3.1 值类型体系
C从C语言继承而来的有左值和右值之后C11引入了可移动对象。引入了将亡值的概念原本的右值被重新命名为纯右值。
左值的例子
只含单个变量、函数或成员的表达式。只含有字符串字面量的表达式。内建的一元*运算符的结果。一个返回左值引用的函数的返回值。
纯右值的例子
除字符串字面量和用户自定义的字面量之外的字面量组成的表达式。内建的一元运算符的运算结果。内建的数学运算符的结果。一个返回值的函数的返回值。一个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 C17起的值类型体系
C17再次明确了值类型体系从广义上来说我们只有两种类型的表达式
glvalue描述对象或函数位置的表达式。prvalue用于初始化的表达式。
而原本的将亡值可以认为是一种特殊的位置它代表一个资源可被回收利用的对象。
C17引入了一个术语临时对象实质化目前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表达式扩展
C11引入了lambda表达式和C14引入的泛型lambda是一个很大的成功。
C17拓展了lambda表达式的应用场景
在常量表达式中使用。在需要当前对象的拷贝时使用。
6.1 constexpr lambda
自从C17起lambda表达式会尽可能的隐式声明constexpr。也就是说任何只使用有效的编译器上下文只有字面量、没有静态变量、没有虚函数、没有try/catch没有new/delete的lambda表达式都可以用作编译期。
例如
auto squared [](auto val) // 隐式constexpr
{
return val * val;
};
arrayint, squared(5) a; // C17起OK为了确认一个lambda表达式能否用于编译期你可以声明为constexpr
auto squared [](auto val) constexpr - int
{return val * val;
};这个表达式将会转换为如下类型
class CompilerSpecificName
{
public:templatetypename Tconstexpr 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}
};在C11和C14里可以通过值或引用捕获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表达式开启一个线程来完成某些任务调用新线程时正确的做法是传递整个对象的拷贝来避免并发和生存周期的问题而不是传递对象的引用。
C14有一个解决方案
class C
{
private:string name;
public:void foo(){auto l1 [thisCopy *this] {cout thisCopy.name \n; };}
};自从C17起你可以通过*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. 新属性和属性特性
从C11起可以指明属性。属性是允许或禁用某些警告的注解。C17引入了新的属性还扩展了属性的使用场景。
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 通用的属性扩展
自从C17起下列有关属性的通用特性变得可用
属性现在可以用来标记命名空间。例如弃用一个命名空间
namespace [[deprecated]] DraftAPI
{// ...
}也可以引入新的一个枚举值作为已有枚举值的替代
enum class City
{Berlin 0,NewYork 1,Mumbai 2,Bombay [[deprecated]] Mumbai
};8. 其他语言特性
C17中一些微小的核心语言特性。
8.1 嵌套命名空间
namespace A::B::C
{// ...
}// 上面代码等价于
namespace A
{namespace B{namespace C{}}
}8.2 有定义的表达式求值顺序
先看一个例子在一个字符串中替换多个子串
string s I heard it even works if you dont believe;
s.replace(0,8,).replace(s.find(even),4,sometimes).replace(s.find(you dont),9,I);通常的假设是前8个字符被空串替换even替换成sometimesyou dont替换成I。结果是
it sometimes works if I believe然而在C17之前最后的结果并没有任何保证。因为查找子串位置的find()函数可能在需要它们的返回值之前的任意时刻调用。事实上所有的find()调用可能在执行第一次替换之前就全部执行因此结果为
it even worsometimesf youIlieve也可能是
it sometimes workIdont believe
it even worsometiIdont believe另外一个例子是输出运算符打印几个相互依赖的值
cout f() g() h();为了解决这种未定义的问题C17标准重新定义了一些运算符的求值顺序因此这些运算符有了固定的求职顺序
对于运算 e1[e2]e1.e2e1.*e2e1-*e2e1 e2e1 e2
e1现在保证一定会在e2之前求值因此求值顺序是从左到右的。然而同一个函数调用中的不同参数的计算顺序仍然是未定义的。
e1.f(a1,a2,a3);a1和a2和a3的求值顺序让人是未定义的。
对于赋值运算 e2 e1e2 e1e2 * e1
e1现在保证一定会在e2之前求值。
new表达式
因此自从C17起会保证replace()操作在find()操作之前。但是对于大多数运算符还是未知的比如
i 0;
i i i;这样的修改可能会影响现有程序的输出。例如
void print10elems(const vectorint v)
{for (int i 0; i 10; i){cout value: v.at(i) \n;}
}int main()
{try{vectorint vec{ 7,14,21,28 };print10elems(vec);}catch (const exception e){cerr EXCEPTION: e.what() \n;}catch (...){cerr EXCEPTION of unknown type\n;}
}C17之前可能的结果是
value: 7
value: 14
value: 21
value: 28
EXCEPTION: ..C17之后的结果保证是
value: 7
value: 14
value: 21
value: 28
value: EXCEPTION: ..8.3 更宽松的用整型初始化枚举值的规则
对于一个有固定底层类型的枚举类型变量C17起可以用一个整型值进行列表初始化。
// 指明底层类型但无作用域枚举类型
enum MyInt : char {};
MyInt i1{42}; // C17起OK// 默认底层类型有作用域枚举
enum class Weekday { mon, tue, wed, thu, fri, sat, sun };
Weekday w1{0}; // C17起OK// 指明底层类型且有作用域枚举
enum class Weekday : char { mon, tue, wed, thu, fri, sat, sun };
Weekday w2{0}; // C17起OK// 没有指明底层类型也无作用域枚举类型
enum Flag { bit 1, bit2 2, bit3 3 };
Flag f1{0}; // ERROR8.4 修正auto类型的列表初始化
自从C17中引入了花括号统一初始化之后每当使用auto代替明确类型进行初始化就会出现一些和直觉不一致的结果
int x{42};
int y{1,2,3}; // ERROR
auto a{42}; // initializer_listint
auto b{1,2,3}; // initializer_listint这些直接使用初始化列表时不一致的行为现在已经被修复了。
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_listint
auto b {1,2,3}; // initializer_listint8.5 十六进制浮点数字面量8
C17允许指定十六进制浮点数字面量。
#include iomanipint main()
{initializer_listdouble values{0x1p4, // 1 * 4 ^ 2 160xA, // A 100xAp2, // 10 * 2 ^ 2 405e0, // 5 * 1.0 50x1.4p2, // 1.25 * 2 ^ 5 51e5, // 1 * 1.0 ^ 5 1000000x1.86Ap16, // 1000000xC.68p2 // 49.625};
}8.6 UTF-8字符字面量
自从C11起C支持u8为前缀的UTF-8字符串字面量。然而C17之前这个前缀不能用于字符字面量。C17修复了这个问题。
auto c u86;8.7 异常声明作为类型的一部分
自从C17之后异常处理声明变成了函数类型的一部分。也就是说如下的两个函数的类型不同
void MightThrow();
void Noexcept() noexcept;在C17之前这两个类型相同就有可能将一个可能抛出异常的函数赋给一个不会抛出异常的函数指针
void (*fp)() noexcept;
fp fNoexcept;
fp MightThrow; // C17起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作为类型的一部分会对泛型库产生一些影响。例如 templatetypename 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); // C17起ERROR
}8.8 单参数static_assert
C17起static_assert()的错误信息的参数变为可选。
#include type_traitstemplatetypename T
class C
{static_assert(is_default_constructible_vT); // C17起有效
};8.9 预处理条件__has_include
C17扩展了预处理增加了一个检查某个头文件是否可以被包含的宏例如
#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