当前位置:   article > 正文

c++ 17 新特性理解_std::monostate

std::monostate

链接:https://pan.baidu.com/s/1XxFjLXEAQJoPkKhaDNNqFQ 
提取码:tgss

1构造函数模板推导

在C++17前构造一个模板类对象需要指明类型:

pair<int, double> p(1, 2.2); // before c++17

std::tuple<int, double, std::string> tq(64, 128.0, "Caroline"); 

C++17就不需要特殊指定,直接可以推导出类型,代码如下:std::tuple eee(64, 128.0, "Caroline");

  1. pair p(1, 2.2); // c++17 自动推导
  2. vector v = {1, 2, 3}; // c++17

如果是tuple, 则是  std::tuple eee(64, 128.0, "Caroline"); 

复习:

tuple是C++11新标准里的类型。它是一个类似pair类型的模板。pair类型是每个成员变量各自可以是任意类型,但是只能有俩个成员,而tuple与pair不同的是它可以有任意数量的成员。但是每个确定的tuple类型的成员数目是固定的.

可以通过构造函数和make_tuple进行定义初始化

std::tuple<std::string, std::string, int> t2 = std::make_tuple("Caroline", "Wendy", 1992);

get(t)返回t的第i个数据成员

具体见:c++ pair 、make_pair、tuple_baidu_16370559的博客-CSDN博客 

2.结构化绑定(Structured bindings)

结构化绑定提供了类似其他语言中提供的多返回值的功能。到目前为止,我们可以通过 std::tuple 来构造一个元组,囊括多个返回值。但缺陷是显而易见的,我们没有一种简单的方法直接从元组中拿到并定义元组中的元素,尽管我们可以使用 std::tie 对元组进行拆包,但我们依然必须非常清楚这个元组包含多少个对象,各个对象是什么类型。

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

std::tuple<int,double,std::string> f() {
    return std::make_tuple(1,2.3,"456");
}
auto [x,y,z] = f(); // x,y,z 分别被推导为int,double,std::string

结构化绑定还可以改变对象的值,使用引用即可:

  1. // 进化,可以通过结构化绑定改变对象的值
  2. int main() {
  3. std::pair a(1, 2.3f);
  4. auto& [i, f] = a;
  5. i = 2;
  6. cout << a.first << endl; // 2
  7. }

注意结构化绑定不能应用于constexpr

constexpr auto[x, y] = std::pair(1, 2.3f); // compile error, C++20可以

 结构化绑定不止可以绑定pair和tuple,还可以绑定数组和结构体等

  1. int array[3] = {1, 2, 3};
  2. auto [a, b, c] = array;
  3. cout << a << " " << b << " " << c << endl;
  4. // 注意这里的struct的成员一定要是public的
  5. struct Point {
  6. int x;
  7. int y;
  8. };
  9. Point func() {
  10. return {1, 2};
  11. }
  12. const auto [x, y] = func();

3.if-switch语句初始化 

C++17前if语句需要这样写代码:

  1. int a = GetValue();
  2. if (a < 101) {
  3. cout << a;
  4. }
  1. // if (init; condition)
  2. if (int a = GetValue()); a < 101) {
  3. cout << a;
  4. }

 4.内联变量

C++17前只有内联函数,现在有了内联变量,我们印象中C++类的静态成员变量在头文件中是不能初始化的,但是有了内联变量,就可以达到此目的:

  1. // header file
  2. struct A {
  3. static int value;
  4. };
  5. inline int A::value = 10;
  6. // ==========或者========
  7. struct A {
  8. inline static int value = 10;
  9. }

延伸:内联函数,

如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方 。

  内联函数和普通函数的区别

1)内联含函数比一般函数在前面多一个inline修饰符

2)内联函数是直接复制“镶嵌”到主函数中去的,就是将内联函数的代码直接放在内联函数的位置上,这与一般函数不同,主函数在调用一般函数的时候,是指令跳转到被调用函数的入口地址,执行完被调用函数后,指令再跳转回主函数上继续执行后面的代码;而由于内联函数是将函数的代码直接放在了函数的位置上,所以没有指令跳转,指令按顺序执行

3)一般函数的代码段只有一份,放在内存中的某个位置上,当程序调用它是,指令就跳转过来;当下一次程序调用它是,指令又跳转过来;而内联函数是程序中调用几次内联函数,内联函数的代码就会复制几份放在对应的位置上.

利:避免了指令的来回跳转,加快程序执行速度

弊:代码被多次复制,增加了代码量,占用更多的内存空间

递归函数不能使用内联函数

inline可避免函数或变量多重定义的问题,如果已定义相同的函数或变量(且该函数或变量声明为inline),编译器会自动链接到该函数或变量。如在一个头文件使用inline定义过变量,只要其他的头文件包含该头文件,就可以直接使用该变量。

具体见:c++ inline 函数及变量_baidu_16370559的博客-CSDN博客

5.折叠表达式

C++17引入了折叠表达式使可变参数模板编程更方便:

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

  1. template <typename ... Ts>
  2. auto sum(Ts ... ts) {
  3. return (ts + ...);
  4. }
  5. int a {sum(1, 2, 3, 4, 5)}; // 15
  6. std::string a{"hello "};
  7. std::string b{"world"};
  8. cout << sum(a, b) << endl; // hello world

 6.constexpr lambda表达式

C++17前lambda表达式只能在运行时使用,C++17引入了constexpr lambda表达式,可以用于在编译期进行计算。

  1. int main() { // c++17可编译
  2. constexpr auto lamb = [] (int n) { return n * n; };
  3. static_assert(lamb(3) == 9, "a");
  4. }

注意:constexpr函数有如下限制:

函数体不能包含汇编语句、goto语句、label、try块、静态变量、线程局部存储、没有初始化的普通变量,不能动态分配内存,不能有new delete等,不能虚函数。

扩展constexpr使用范围,可用于if语句中,也可用于lambda表达式中。

if constexpr (ok == true)
{
    //当ok为true时,下面的else块不生成汇编代码
    std::cout << "ok" << std::endl;
}

7.namespace嵌套

  1. namespace A {
  2. namespace B {
  3. namespace C {
  4. void func();
  5. }
  6. }
  7. }
  8. // c++17,更方便更舒适
  9. namespace A::B::C {
  10. void func();)
  11. }

8.__has_include预处理表达式

可以判断是否有某个头文件,代码可能会在不同编译器下工作,不同编译器的可用头文件有可能不同,所以可以使用此来判断。

int main()
{
#if __has_include(<cstdio>)
    printf("hehe");
#endif
#if __has_include("iostream")
    std::cout << "hehe" << std::endl;
#endif

return 0;
}

9.在lambda表达式用*this捕获对象副本

正常情况下,lambda表达式中访问类的对象成员变量需要捕获this,但是这里捕获的是this指针,指向的是对象的引用,正常情况下可能没问题,但是如果多线程情况下,函数的作用域超过了对象的作用域,对象已经被析构了,还访问了成员变量,就会有问题。

  1. struct A {
  2. int a;
  3. void func() {
  4. auto f = [this] {
  5. cout << a << endl;
  6. };
  7. f();
  8. }
  9. };
  10. int main() {
  11. A a;
  12. a.func();
  13. return 0;
  14. }

所以C++17增加了新特性,捕获*this,不持有this指针,而是持有对象的拷贝,这样生命周期就与对象的生命周期不相关啦。

  1. struct A {
  2. int a;
  3. void func() {
  4. auto f = [*this] { // 这里
  5. cout << a << endl;
  6. };
  7. f();
  8. }
  9. };
  10. int main() {
  11. A a;
  12. a.func();
  13. return 0;
  14. }

10.新增Attribute

我们可能平时在项目中见过declspec, __attribute , #pragma指示符,使用它们来给编译器提供一些额外的信息,来产生一些优化或特定的代码,也可以给其它开发者一些提示信息。

例如:

  1. struct A { short f[3]; } __attribute__((aligned(8)));
  2. void fatal() __attribute__((noreturn));

 [[fallthrough]],用在switch中提示可以直接落下去,不需要break,让编译期忽略警告

[[nodiscard]] :表示修饰的内容不能被忽略,可用于修饰函数,标明返回值一定要被处理

[[maybe_unused]] :提示编译器修饰的内容可能暂时没有使用,避免产生警告

11.字符串转换

新增from_chars函数和to_chars函数

    const std::string str{ "123456098" };
    int value = 0;
    const auto res = std::from_chars(str.data(), str.data() + 4, value);

   则 :value = 1234;

           res.ptr= 56098;

12.std::variant

具体见:

https://mp.csdn.net/mp_blog/creation/editor/120419967

C++17增加std::variant实现类似union的功能,但却比union更高级,举个例子union里面不能有string这种类型,但std::variant却可以,还可以支持更多复杂类型,如map等。

注意,不允许使用空变量、具有引用成员的变量、具有c样式数组成员的变量和具有不完整类型(如void)的变量。没有空的状态:这意味着对于每个构建的对象,必须至少调用一个构造函数。默认构造函数初始化第一个类型(通过第一个类型的默认构造函数):

1.默认情况下,变量的默认构造函数调用第一个备选项的默认构造函数:

2.成员函数index()可用于查明当前设置了哪个选项(第一个选项的索引为0)。

3.访问值的通常方法是调用get<>()获取对应的选项值。可以传递它的索引或者类型

#include <variant>
 
std::variant<int, std::string> var{"hi"}; // initialized with string alternative
std::cout << var.index(); // prints 1
var = 42; // now holds int alternative
std::cout << var.index(); // prints 0

var = "world";
...
try {
std::string s = std::get<std::string>(var); // access by type   

std::cout << s << std::endl;   //world

var = 3;
int i = std::get<0>(var); // access by index    

std::cout << i << std::endl;   //3
}
catch (const std::bad_variant_access& e) { // in case a wrong type/index is used

如果没有为第一个类型定义默认构造函数,则调用该变量的默认构造函数会在编译时错误:
#include <iostream>
#include <variant>
 
struct NoDefConstr
{
    NoDefConstr(int i)
    {
        std::cout << "NoDefConstr::NoDefConstr(int) called\n";
    }
};
 
int main()
{
    std::variant<NoDefConstr, int> v1; // ERROR: can’t default construct first type
 
    return 0;

不过辅助类型std::monostate提供了处理这种情况的能力,还提供了模拟空状态的能力。

std::monostate
为了支持第一个类型没有默认构造函数的variant对象,提供了一个特殊的helper类型:std::monostate。类型std::monostate的对象总是具有相同的状态,因此,它们总是相等的。它自己的目的是表示另一种类型,这样variant就没有任何其他类型的值。也就是说,std::monostate可以作为第一种替代类型,使变体类型默认为可构造。

std::variant<std::monostate, NoDefConstr> v2; // OK
std::cout << "index: " << v2.index() << '\n'; // prints 0

4.赋值和emplace()操作对应于初始化:

std::variant<int, int, std::string> var; // sets first int to 0, index()==0
var = "hello"; // sets string, index()==2
var.emplace<1>(42); // sets second int, index()==1

13.std::apply

这个函数定义在tuple头文件中,函数签名如下:

  1. template <class F, class Tuple>
  2. constexpr decltype(auto) apply(F&& f, Tuple&& t);

该函数接受两个参数,第一个是一个函数对象,第二个是一个Tuple对象 

使用std::apply可以将tuple展开作为函数的参数传入,见代码:

int add(int first, int second) { return first + second; }
auto add_lambda = [](auto first, auto second) { return first + second; };
int main() {
    std::cout << std::apply(add, std::pair(1, 2)) << '\n';
    std::cout << add(std::pair(1, 2)) << "\n"; // error
    std::cout << std::apply(add_lambda, std::tuple(2.0f, 3.0f)) << '\n';     输出为5
}

14.std::make_from_tuple

std::apply可以解决函数调用时,tuple转参数列表,但是如果希望调用的是构造函数怎么办?构造函数毕竟没办法直接获取函数指针。

使用make_from_tuple可以将tuple展开作为构造函数参数。

template <class T, class Tuple>

constexpr T make_from_tuple(Tuple&& t);


struct Foo {
    Foo(int first, float second, int third) {
        std::cout << first << ", " << second << ", " << third << "\n";
    }
};
int main() {
   auto tuple = std::make_tuple(42, 3.14f, 0);
   std::make_from_tuple<Foo>(std::move(tuple));
}

15.std::string_view

我们传递一个string时会触发对象的拷贝操作,大字符串的拷贝赋值操作会触发堆内存分配,很影响运行效率,有了string_view就可以避免拷贝操作,平时传递过程中传递string_view即可

它只有两个成员变量,指向内存的指针和可以访问到的长度。它对它指向的内存是只读的,std::string_view的任何方法都不会改变执行的内存。

  1. void func(std::string_view stv) { cout << stv.data() << endl; }
  2. int main(void) {
  3. std::string str = "Hello World";
  4. std::cout << str << std::endl;
  5. std::string_view stv(str.c_str(), str.size());
  6. cout << stv.data() << endl;
  7. func(stv);
  8. return 0;
  9. }

std::string_view对应的是string,std::string_view背后是basic_string_view模板,因此它有几种字符串变种:

using string_view    = basic_string_view<char>;
using u16string_view = basic_string_view<char16_t>;
using u32string_view = basic_string_view<char32_t>;
using wstring_view   = basic_string_view<wchar_t>;

16.as_const

C++17使用as_const可以将左值转成const类型

  1. std::string str = "str";
  2. const std::string& constStr = std::as_const(str);

17.file_system

C++17正式将file_system纳入标准中,提供了关于文件的大多数功能,基本上应有尽有,这里简单举几个例子:

命名空间及头文件

  1. #include<filesystem>
  2. using namespace std::filesystem;
  1. namespace fs = std::filesystem;
  2. fs::create_directory(dir_path);
  3. fs::copy_file(src, dst, fs::copy_options::skip_existing);
  4. fs::exists(filename);
  5. fs::current_path(err_code);

path 类:说白了该类只是对字符串(路径)进行一些处理,这也是文件系统的基石。
directory_entry 类:功如其名,文件入口,这个类才真正接触文件。 
directory_iterator 类:获取文件系统目录中文件的迭代器容器,其元素为 directory_entry对象(可用于遍历目录)
file_status 类:用于获取和修改文件(或目录)的属性(需要了解C++11的强枚举类型(即枚举类))

常用函数

void copy(const path& from, const path& to) :目录复制
path absolute(const path& pval, const path& base = current_path()) :获取相对于base的绝对路径
bool create_directory(const path& pval) :当目录不存在时创建目录
bool create_directories(const path& pval) :形如/a/b/c这样的,如果都不存在,创建目录结构
bool exists(const path& pval) :用于判断path是否存在
uintmax_t file_size(const path& pval) :返回目录的大小
file_time_type last_write_time(const path& pval) :返回目录最后修改日期的file_time_type对象
bool remove(const path& pval) :删除目录
uintmax_t remove_all(const path& pval) :递归删除目录下所有文件,返回被成功删除的文件个数
void rename(const path& from, const path& to) :移动文件或者重命名

作用:

1.可以替代遍历文件,更快速,更简单。

18.static_assert

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

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

19.auto

扩展auto的推断范围。

在c++ 11 中,auto不能定义数组。现在c++ 17 可以。

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类型

20.typename

扩展用法,允许出现在模板的模板的参数中。

回顾一下typename的用法,①用于模板中,表示模板参数为类型;

//第一种用法:声明模板参数为类型

template<typename T>

struct B { };

②用于声明某名字是变量名

struct A
{
    typedef int Example;
};
int main()
{
    typename A::Example e;//第二种用法:声明某名字为一种类型
    return 0;
}

在模板的模板的参数中

template<template<typename T> typename X>
struct B
{
    X<double> e;
    B() { std::cout << "B Construct" << std::endl; }
};

21.允许非类型模板参数进行常量计算

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

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

22.if/switch初始化

对于C++17标准,if和switch语句有以下新形式:

  • if (init; condition)
  • switch (init; condition)

对于初始语句定义的变量,在if-else及switch-case部分都可见,而在if和switch的外层作用域不可见,如此能够缩小变量作用域。

if(int i = 0; i == 0){

}

switch(int k = 10; k)
{
    case 0:break;
    case 1:break;
    default:break;
}

23.聚合初始化

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

具体见:CSDN

24.枚举[类]对象的构造

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

enum class Handle : unsigned int { value = 0 };

Handle h { 42 };

25.十六进制单精度浮点数字面值

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

float value = 0x1111f;

26.基于对齐内存的动态内存分配

谈到动态内存分配,少不了new和delete运算符,新标准中的new和delete运算符新增了按照对齐内存值来分配、释放内存空间的功能(即一个新的带对齐内存值的new、delete运算符重载)

void* operator new(std::size_t size, std::align_val_t alignment);

参数说明:
size —— 分配的字节数。必须为alignment的整数倍。
alignment —— 指定的对齐内存值。必须是实现支持的合法对齐。
new的返回值:
成功,返回指向新分配内存起始地址的指针。

27.简化重复命名空间的属性列表

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

28.改写与继承构造函数

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

#include<iostream>
 
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的构造函数
};
D1 d1(0);    //错误:函数冲突,
 
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;
}

29.用auto作为非类型模板参数

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

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

30.fallthrough

该属性主要用于switch语句中。在C++的switch语句中,如果当前case分支中不加break, 便会执行下一个case分支的代码。

    switch (n) { 
    case 1: { 
        cout << "work through one \n"; 
                [[fallthrough]];
    } 
    case 2: { 
        cout << "work through two \n"; 
                [[fallthrough]];
    } 
    case 3: { 
        cout << "work through three \n"; 
                [[fallthrough]];
    } 
    default: { 
        cout << "work through default \n"; 
    } 
    }

31.nodiscard

nodiscard是c++17引入的一种标记符,其语法一般为[[nodiscard]]或[[nodiscard("string")]](c++20引入),含义可以理解为“不应舍弃”。nodiscard一般用于标记函数的返回值或者某个类,当使用某个弃值表达式而不是cast to void 来调用相关函数时,编译器会发出相关warning
[[nodiscard]] int func(){return 1;}; // C++17
[[nodiscard("nodiscard_func_1")]] int func_1(){return 2;};  // C++20
func(); // warning
func_1(); // warning

保存函数返回值,或者使用cast to void

[[nodiscard]] int func(){return 1;}; // C++17
int a = func(); // no warning
static_cast<void>(func()); // no warning

32.maybe_unused

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

[[maybe_unused]] int C;

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号