C++17学习记录:新语言功能特性

Posted 河边小咸鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++17学习记录:新语言功能特性相关的知识,希望对你有一定的参考价值。

  • 本篇笔记汇总了C++17中的主要新语言功能特性,根据个人理解与查阅的资料进行记录。
  • 主要参考地址:cppreference
  • C++17为继C++11后的第一个大版本更新,东西相较于C++14多了不少,但是基本上都是以往特性的优化与补充,这里简单进行一些主要新特性的记录。

目录

· 折叠表达式

  在 C++11 中,其引入了可变参数模板,但是可变参数模板需要挨个递归处理可变参数。这就导致就算以同一种方式处理可变参数,也需要重复的函数递归,写起来很笨重。

  比如下面这个例子,仅仅只需要累加求和,但是却需要写两个模板函数使得递归可行,从而遍历每一个参数,写起来相对麻烦且臃肿。

template<typename T>
T add(T&& num)//基础函数

    return num;


template<typename T, typename ... Args>
T add(T&& num, Args&&... a)//递归变参函数

    return num + add(forward<Args>(a)...);//递归调用


int main()

    cout << add(1, 2, 3, 4, 5) << endl;


输出:
15

  折叠表达式是 C++17 新引进的语法特性。使用折叠表达式可以简化对 C++11 中引入的参数包的处理,从而在某些情况下避免使用递归。

   折叠表达式共有四种语法形式。分别为一元的左折叠和右折叠,以及二元的左折叠和右折叠。其支持所有二元运算符的简写,这里仅做简单记录,比如说上文中的例子就可以简化为如下写法(二元左折叠):

template<typename T, typename ... Args>
T add(T&& num, Args&&... args)

    return (num + ... + args);


int main()

    cout << add(1, 2, 3, 4, 5) << endl;


/* g++ test.cpp -o test -std=c++17 */
输出:
15

  总体而言,折叠表达式为变参模板写法提供了更多的可能性,可以在不影响原功能和代码可读性的前提下实现简化。

  

· 类模板实参推导

  简单来说,就是在实例化类模板时,可以自动推出模板参数而不需指定。具体限制为:如果构造函数能够推导出所有模板参数,则可以跳过显式定义模板参数。

  下面是几个简单但方便的例子:

int main()

    std::pair<int, double> p1(2, 4.5);		//ok
    std::pair p2(2, 4.5);					//-std=c++17 ok

    std::vector<int> v1 = 1;				//ok
    std::vector v2 = 1;					//-std=c++17 ok

    std::mutex mx;
    std::lock_guard<std::mutex> lock1(mx);	//ok
    std::lock_guard lock2(mx);				//-std=c++17 ok

  这东西怎么说,我感觉虽然方便但是并不是所有都适合这么搞。因为很容易分不清类型让代码可读性降低,所以我感觉像 pairtuplelock_guard这种临时变量用一下还好(因为其中的模板参数往往已经规定,省略一下可读性影响也不大),如果有作用域比较广的变量最好就不要用了,因为很可能需要费神来思考它的模板参数。

  

· auto 占位的非类型模板形参

  从 C++17 开始,可以使用 auto 来声明一个非类型模板参数。

template<auto N> class S 

...
;

S<42> s1; // OK: type of N in S is int
S<'a'> s2; // OK: type of N in S is char

  如果非类型模板形参的类型包含占位符类型 auto,被推导类型的占位符 (C++20 起),或 decltype(auto),那么它可以被推导。推导会如同在虚设的声明 T x = 模板实参; 中推导变量 x 的类型一样进行,其中 T 是模板形参的声明类型。如果被推导的类型不能用于非类型模板形参,那么程序非良构。

template<auto n>
struct B  /* ... */ ;
 
B<5> b1;   // OK:非类型模板形参的类型是 int
B<'a'> b2; // OK:非类型模板形参的类型是 char
B<2.5> b3; // 错误(C++20 前):非类型模板形参的类型不能是 double

  对于类型中使用了占位符类型的非类型模板形参包,每个模板实参的类型会独立进行推导,而且不需要互相匹配:

template<auto...>
struct C ;
 
C<'C', 0, 2L, nullptr> x; // OK

  

· 编译期的 constexpr if 语句

  constexpr ifC++17 中新特性,可以实现在编译期的条件判断。
  这个东西主要是为了实现泛型编译期处理中的条件判定,可以实现根据 constexpr if 来编译合适的代码段,从而可以在编译期干一部分模板类型判断相关的事情,提高代码的执行效率。

template <typename T>
void bar(T t) 

    if constexpr (has_foo_v<T>) 
    
        t.foo();
        std::puts("yes");
     
    else
    
        std::puts("no");
    


等价于:

template <typename T, typename=std::enable_if_t<has_foo_v<T>>>
void bar(T t) 

    t.foo();
    std::puts("yes");


void bar(...) 

    std::puts("no");

  

· inline 变量

  在 C++17 后,可以给变量加上 inline 标签从而使其成为内联变量。其核心目的就是为了可以在头文件中定义一个全局可用的对象。

class MyClass

    static inline std::string name = ""; // OK since C++17
    ...
;
 
inline MyClass myGlobalObj; // OK even if included/defined by multiple CPP files

  另外声明为 constexpr 的静态成员变量(但不是命名空间作用域变量)是隐式的内联变量,即以下二者等价。

struct D

    static constexpr int n = 5; // C++11/C++14: //声明但未定义
                                // since C++17: 定义
;
struct D

    inline static constexpr int n = 5;
;

想深入了解可以看一下这位大佬的文章: 点我跳转

  

· 结构化绑定

  在我的理解中,这个新特性就是可以对含有多个元素的对象进行一个映射,即绑定指定名称到初始化器的子对象或元素。可能有点类似引用,但不同于引用的是,结构化绑定的类型不必为引用类型。

  第一个用法,绑定数组:

int a[2] = 1,2;
 
auto [x,y] = a; // 创建 e[2],复制 a 到 e,然后 x 指代 e[0],y 指代 e[1]
auto& [xr, yr] = a; // xr 指代 a[0],yr 指代 a[1]

  第二个用法,绑定元组式类型:

float x;
char  y;
int   z;
 
std::tuple<float&,char&&,int> tpl(x,std::move(y),z);
const auto& [a,b,c] = tpl;
// a 指名指代 x 的结构化绑定;decltype(a) 为 float&
// b 指名指代 y 的结构化绑定;decltype(b) 为 char&&
// c 指名指代 tpl 的第 3 元素的结构化绑定;decltype(c) 为 const int

  第三个用法,绑定到数据成员:

struct S 
    mutable int x1 : 2;
    volatile double y1;
;
S f();
 
const auto [x, y] = f(); // x 是标识 2 位位域的 int 左值
                         // y 是 const volatile double 左值

  我感觉,这个新特性的绑定元组部分还是挺有用的,包括接收返回值和范围for的取值等场景用起来都很方便和优雅。比如说下面这个例子,就可以省去取值的部分。

int main()

    vector<pair<int, int>> a1, 2, 3, 4;
    for(auto& [x, y] : a)
    
        cout << x << y << endl;
    

  

· if 和 switch 语句中的初始化器

  简单来说就是在 ifswitch 语句中也可以初始化变量了,类似于在 for 循环里声明新的变量。但是我感觉在 if/switch 中初始化变量的需求应该是不会太大,产生需求的主要场景应该是为了控制变量的作用域。

int main()

    int s = 1;
    if(int a = 0; a == s)		//A部分
    
        cout << a << endl;
    
    else if(int b = 1; b == s)	//B部分
    
        cout << a << endl;
        cout << b << endl;
    
    else						//C部分
    
        cout << a << endl;
        cout << b << endl;
        cout << 222 << endl;
    

	switch(int c = 2; c) 
	
    case 1:
    case 2:
        cout << c << endl;
    break;
    default:
        cout << 333 << endl;
    break;
    


输出:
0
1
2

  如上为在 if/switch 语句中初始化变量。其中在 if 语句部分中,变量 a 的作用域为 A/B/C,变量 b 的作用域为 B/C。而在 switch 语句部分中,变量 c 的作用域为整个 switch

  总体而言这个新特性通过在代码块中添加大括号也可以等价实现,但是看上去可能没那么优雅,所以还是有其独特的优点的。

  

· u8-char

  就是一个新的字符字面量,UTF-8字符字面量(每个字符大小为1字节),格式如下,个人感觉没什么好说的。

u8'c字符':
char c = u8'a'; //sizeof(c) = 1
constexpr char str[] = u8"123456"

  

· 命名空间相关

  简化嵌套命名空间定义: namespace A::B::C ... 等价于 namespace A namespace B namespace C ...
  using 声明多个名称: 拥有多于一个 using 声明符的 using 声明,等价于对应的单个 using 声明符的 using 声明的序列,人话讲就是可以用逗号连接多个 using 声明。cppreference上的例子如下:

void f();
namespace A 
    void g();

 
namespace X 
    using ::f;        // 全局 f 现在作为 ::X::f 可见
    using A::g;       // A::g 现在作为 ::X::g 可见
    using A::g, ::f; // (C++17) OK:命名空间作用域允许双重声明

 
void h()

    X::f(); // 调用 ::f
    X::g(); // 调用 A::g

  但是经过我自己的测试,C++17 我只发现新增了 using 双重定义相关的内容,其余的使用 C++11 标准也可以编译通过。

  

· 将 noexcept 作为类型系统的一部分

  在c++ 17异常处理规范成为函数类型的一部分。也就是说,下面两个函数现在有两种不同的类型:

void f1();
void f2() noexcept; // different type

  在c++ 17之前,这两个函数都具有相同的类型。
  因此,编译器现在将检测如果你使用一个函数抛出异常,而一个函数不抛出任何异常的情况:

void (*fp)() noexcept; // pointer to function that doesn’t throw
fp = f2; // OK
fp = f1; // ERROR since C++17

  当然,在允许抛出函数的地方使用不抛出的函数仍然是有效的:

void (*fp2)(); // pointer to function that might throw
fp2 = f2; // OK
fp2 = f1; // OK

  因此,这个新特性不会破坏那些还没有使用noexcept函数指针的程序,但是现在可以确保您不再违反函数指针中的noexcept规范。想深入了解可以看一下这位大佬的文章: 点我跳转

  

· 新的求值顺序规则

  具体目的和影响可以看一下 StackOverflow 上的这个问题:点我跳转

  • 具体新增的顺序规则如下:(摘自cppreference)
  1. 函数调用表达式中,指名函数的表达式按顺序早于每个参数表达式和每个默认实参。
  2. 函数调用表达式中,每个形参的初始化的值计算和副作用相对于任何其他形参的初始化的值计算和副作用是顺序不确定的。
  3. 用运算符写法进行调用时,每个重载的运算符均遵循其所重载的内建运算符的定序规则。
  4. 下标表达式 E1[E2] 中,E1 的每个值计算和副作用均按顺序早于 E2 的每个值计算和副作用。
  5. 成员指针表达式 E1.*E2 或 E1->*E2 中,E1 的每个值计算和副作用都按顺序早于 E2 的每个值计算和副作用(除非 E1 的动态类型不含 E2 所指的成员)。
  6. 移位运算符表达式 E1<<E2 和 E1>>E2 中,E1 的每个值计算和副作用都按顺序早于 E2 的每个值计算和副作用。
  7. 每个简单赋值表达式 E1=E2 和每个复合赋值表达式 E1@=E2 中,E2 的每个值计算和副作用均按顺序早于 E1 的每个值计算和副作用。
  8. 带括号的初始化器中的逗号分隔的表达式列表中的每个表达式,如同函数调用一般求值(顺序不确定)。

  

· 强制的复制消除

  简单来讲就是在对象的初始化中,当初始化器表达式是一个与变量类型相同的类类型的纯右值(忽略 cv 限定)时,T x = T(T(f())); //仅调用一次 T 的默认构造函数以初始化 x。比如下面这个例子,关掉g++默认的省略优化后,使用不同的标准编译,可以看出以C++17标准编译的话会进行一次复制消除(返回值优化),优化的位置是 main 函数里的无名临时量。

  • 在对象的初始化中,当源对象是无名临时量且与目标对象具有相同类型(忽略 cv 限定)时。当无名临时量为 return 语句的操作数时,称这种复制消除的变体为 RVO,“返回值优化 (return value optimization)”。
class test

public:
    test()
    
        cout << "constructor" << endl;
    
    ~test()
    
        cout << "destructor" << endl;
    
    test(const test& other)
    
        cout << "copy constructor" << endl;
    

;
test Foo()

   test obj;
   return obj;

int main()

    test obj = Foo();


编译:
g++ -g -fno-elide-constructors -Wall t.cpp -o t -std=c++11
输出:
constructor
copy constructor
destructor
copy constructor
destructor
destructor

编译:
g++ -g -fno-elide-constructors -Wall t.cpp -o t -std=c++17
输出:
constructor
copy constructor
destructor
destructor

  想深入了解可以看一下这位大佬的文章: 点我跳转

  

· lambda 表达式捕获 *this

  lambda中可以捕获 *this 辣,可以通过捕获 *this 来将整个当前对象复制一遍。而之前只能捕获 this,即以引用捕获当前对象。我感觉新增这种捕获方式的一个应用场景就是可以避免悬垂引用问题。
  

· constexpr 的 lambda 表达式

  lambda表达式的格式为:[ 捕获 ] ( 形参 ) lambda说明符 约束(可选) 函数体 ,而在 C++17 中其说明符部分新增了一位成员 constexpr

  • constexpr:显式指定函数调用运算符或运算符模板的任意特化为 constexpr 函数。如果没有此说明符但函数调用运算符或任意给定的运算符模板特化恰好满足针对 constexpr 函数的所有要求,那么它也会是 constexpr 的。

  

· 属性命名空间不必重复

  简单来说就是简化下面这种情况:

简化前:
void f() 

    [[rpr::kernel, rpr::target(cpu,gpu)]] // 重复
    doTask();


简化后:
void f() 

    [[using rpr: kernel, target(cpu,gpu)]]
    doTask();

  

· 新属性 [[fallthrough]] [[nodiscard]] 和 [[maybe_unused]]

  • [[fallthrough]](C++17):指示从前一 case 标号直落是有意的,而在发生直落时给出警告的编译器不应该为此诊断。
  • [[nodiscard]](C++17):鼓励编译器在返回值被舍弃时发布警告。(印象里这个比较常用)
  • [[maybe_unused]](C++17):压制编译器在未使用实体上的警告,若存在。

  

· __has_include

  即一种新的源文件包含语法,其作用为检查一个头或源文件是否可以被包含。cppreference上的例子如下,我感觉适配不同版本头文件时可能有用吧。

#if __has_include(<optional>)
#  include <optional>
#  define has_optional 1
   template<class T> using optional_t = std::optional<T>;
#elif __has_include(<experimental/optional>)
#  include <experimental/optional>
#  define has_optional -1
   template<class T> using optional_t = std::experimental::optional<T>;
#else
#  define has_optional 0
#  include <utility>
template<class V> class optional_t 
    V v_; bool has_false;
  public:
    optional_t() = default;
    optional_t(V&& v) : v_(v), has_true 
    V value_or(V&& alt) const&  return has_ ? v_ : alt; 
    /*...*/
;
#endif

  

以上是关于C++17学习记录:新语言功能特性的主要内容,如果未能解决你的问题,请参考以下文章

C++17学习记录:新语言功能特性

C++14学习记录:新语言功能特性

C++14学习记录:新语言功能特性

C++14学习记录:新语言功能特性

C++14学习记录:新语言功能特性

C++11学习记录:核心语言功能特性