【C++】C++17的那些新特性

本文首发于 ❄️慕雪的寒舍

学习C++17的新特性

1.构造函数模板推导

在之前,我们如果想用stl容器,都需要用<> 来手动指定参数类型。但在C++17中,我们不需要这么做了。

int main()
{
    std::vector v1 = {1,2,3,4};
    std::pair p1  = {1,2.4234};
    cout << typeid(v1).name() << endl;
    cout << typeid(p1).name() << endl;

    return 0;
}

使用C++11编译,这个代码会报错。报错的意思是让我们指定参数的模板类型。

比如 std::pair p1 = {1,2.4234}; 在C++11中应该写成 std::pair<int,double> p1 = {1,2.4234};

test.cpp:16:10: error: use of class template 'std::pair' requires template arguments
    std::pair p1  = {1,2.4234};
         ^
/usr/bin/../lib/gcc/x86_64-linux-gnu/11/../../../../include/c++/11/bits/stl_pair.h:211:12: note: template is declared here
    struct pair
           ^
3 errors generated.
make: *** [makefile:3: test] Error 1

在C++17中,这样的写法就是可以被通过的了,也能正常推断出参数的类型,分别是一个int的vector,和一个int+double的pair;

$ make
clang++ test.cpp -o test -std=c++17
$ ./test
St6vectorIiSaIiEE
St4pairIidE

2.结构化绑定

我们可以用 auto[变量1,变量2]的方式来接受一个tuple或者pair的返回值,将其绑定到两个不同的变量上。

tuple是C++11新增的一个数据结构,它和pair的用法类似,不同的是元组支持无数个参数。而pair仅支持两个。

std::tuple<int, double> func_tuple()
{
    return std::tuple<int,double>(1, 2.2);
}

std::pair<int, double> func_pair()
{
    return {1,2};
}

int main()
{
    auto [i, d] = func_tuple(); 
    cout << typeid(i).name() << endl;
    cout << typeid(d).name() << endl;

    cout << endl;

    auto [x,y] = func_pair();
    cout << typeid(x).name() << endl;
    cout << typeid(y).name() << endl;

    return 0;
}

使用C++11来编译,编译器会报错,但编译依旧能成功。这是因为我们的编译器是支持C++17的,但又被指定了-std=c++11,所以给用户报了个警告,但没有报错(因为这个语法在C++17里面是正确的)

clang++ test.cpp -o test -std=c++11
test.cpp:34:10: warning: decomposition declarations are a C++17 extension [-Wc++17-extensions]
    auto [i, d] = func_tuple(); 
         ^~~~~~
test.cpp:40:10: warning: decomposition declarations are a C++17 extension [-Wc++17-extensions]
    auto [x,y] = func_pair();
         ^~~~~
2 warnings generated.

运行输出结果如下

$ ./test
i
d

i
d

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

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

// 注意这里的struct的成员一定要是public的,不然外部无法访问,还怎么绑定?
struct Point
{
    int x;
    int y;
};

// 返回值是point的函数
Point func()
{
    return {1, 2};
}

int main()
{
    int array[3] = {1, 2, 3};
    auto [a, b, c] = array;
    cout << a << " " << b << " " << c << endl;
    // 直接推导出两个成员变量并赋值给变量x和y
    const auto [x, y] = func();
    return 0;
}

成功编译并输出结果

$ make
clang++ test.cpp -o test -std=c++17
$ ./test
1 2 3
1 2

自定义类型也能实现结构化绑定,这里从网上扒了一个代码下来,就不自己做测试了

// 需要实现相关的tuple_size和tuple_element和get<N>方法。
class Entry {
public:
    void Init() {
        name_ = "name";
        age_ = 10;
    }

    std::string GetName() const { return name_; }
    int GetAge() const { return age_; }
private:
    std::string name_;
    int age_;
};

template <size_t I>
auto get(const Entry& e) {
    if constexpr (I == 0) return e.GetName();
    else if constexpr (I == 1) return e.GetAge();
}

namespace std {
    template<> struct tuple_size<Entry> : integral_constant<size_t, 2> {};
    template<> struct tuple_element<0, Entry> { using type = std::string; };
    template<> struct tuple_element<1, Entry> { using type = int; };
}

int main() {
    Entry e;
    e.Init();
    auto [name, age] = e;
    cout << name << " " << age << endl; // name 10
    return 0;
}

3.if语句新增初始条件

在之前我们都是用 if(判断条件) 来使用if语句的,C++17中给if新增了一个类似for循环中第一个参数的相同参数

if(初始化条件,判断条件)

比如

    if(int i=20;i<39){
        cout <<"i<39!"<<endl;
    }

运行效果如下

$ ./test
i<39!

4.内联变量

在之前我们想初始化一个类中的static变量,需要在类中定义,类外初始化。但如果是const的static变量,就能直接在类中通过缺省值的方式来初始化。

// 在头文件里面这样是能通过编译的,但是不建议在头文件中初始化static变量,会产生ODR冲突:
// Variable 'value' defined in a header file; variable definitions in header files can lead to ODR violations
struct A {
    static int value;  
    static const int value_c=10;  // const可以直接初始化
};
int A::value = 10;

在C++17中内联变量引入后,我们就可以直接实现在头文件中初始化static非const变量,或者直接用缺省值来初始化了

struct A
{
    static int value;
    static const int value_c = 10;
    // static int value = 10;
};

inline int A::value = 10;

// ========= 或者 ========
struct B
{
    inline static int value = 10;
    inline static const int value_c = 10;
};

相比于原本static变量初始化需要放到另外一个cpp源文件中,这种直接在头文件里面声明+初始化的方式能更好的确定变量的初始值。

5.折叠表达式

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

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

实话说,可变模板参数这部分就没有弄明白过,实际上也没有用过,直接跳过!

6.constexpr+lambda表达式

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

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

规则和普通的constexpr函数相同,参考我的C++11和14的文章。这里做简单说明:

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

7.嵌套命名空间

在之前如果需要嵌套命名空间,需要这样写

namespace A {
    namespace B {
        namespace C {
            void func();
        }
    }
}

C++17中可以直接用类似访问限定符的方式,前面加一个namespace来标明嵌套的命名空间。

// c++17,方便了,可读性也更好
namespace A::B::C {
    void func();
}

8.__has_include预处理表达式

#if defined __has_include // 判断是否支持这个表达式
#if __has_include(<charconv>) // 支持,判断是否存在该头文件
#define has_charconv 1 // 头文件存在,定义一个宏
#include <charconv> // 引用这个头文件
#endif
#endif

如果一个代码会在多个不同的平台下跑,这个功能就很重要。比如我之前写项目的时候需要使用到jsoncpp,在centos和deepin下,安装jsoncpp的include路径是不同的

//centos
#include <json/json.h>
//deepin
#include <jsoncpp/json/json.h>

这种场景下就可以使用上面提到的这个预处理表达式进行判断,来确认你的jsoncpp路径到底在哪里。注意,这只能解决从yum和apt安装的jsoncpp,如果是自己手动安装的,那鬼知道你安装到哪里去了?🤣

所以很多大型项目如果需要使用jsoncpp这种第三方依赖项目,一般都会采用git submodule的方式,直接将第三方库下载到当前项目路径下,以避免不同平台的依赖项include路径不对而导致无法编译程序的问题。

9.this指针捕获(lambda)

在lambda表达式中,采用[this]方式捕获的this指针是值传递捕获的,但在一些情况下,会出现访问已经被释放了的空间的行为;比如如下代码

#include <functional>
#include <iostream>
#include <memory>
using namespace std;

struct Foo
{
    std::unique_ptr<int> p;

    std::function<void()> f()
    {
        p.reset(new int(10));
        return [&]
        {
            cout << 5 << endl;
            cout << *p << endl; // 实际上是这一步报错的
            // 这里对*p的访问可以解析为 *(this->p),但实际上this指针已经被销毁了
            // 注意,这里采用了智能指针,不存在内存泄漏,p指针指向的空间也被销毁了
            // 但我们的报错其实是对this指针解引用的时候就抛出了
            cout << 6 << endl;
        };
    }
};

int main()
{
    auto foo = new Foo();
    cout << 1 << endl;
    auto f = foo->f();  // 获取了一个类内成员函数
    cout << 2 << endl;
    delete foo;  // 销毁这个对象
    cout << 3 << endl;
    // 尝试在销毁后继续使用这个对象,我们是通过lambda中=捕获的this指针来访问对象的
    f();  // 这里直接报错了 Segmentation fault (core dumped)
    cout << 4 << endl;

    return 0;
}

运行这个程序,可以看到是在*p的位置报错退出的;具体的原因参考代码中的注释。

$ ./test
1
2
3
5
Segmentation fault (core dumped)

需要注意,lambda表达式中,使用=和&都会默认采用传值捕获this指针,因为this指针是存在于函数作用域中的一个隐藏参数,并不是独立在成员函数外的变量,所以是可以被捕捉到的;另外,this指针是不能被传引用捕获的,[&this] 的写法是不允许的;

clang++ test.cpp -o test -std=c++17
test.cpp:84:18: error: 'this' cannot be captured by reference
        return [&this]
                 ^
1 error generated.

C++17中提供了一个特殊的写法 [*this] 通过传值的方式捕获了当前对象本身,此时lambda表达式中存在的就是一个对象的拷贝,即便当前对象被销毁了,我们依旧可以通过这个拷贝访问到目标;

代码修改如下:

#include <functional>
#include <iostream>
#include <memory>
using namespace std;

struct Foo
{
    std::shared_ptr<int> p; // 不能用unique_ptr,因为它的拷贝构造函数是被delete禁止使用的

    std::function<void()> f()
    {
        p.reset(new int(10));
        return [*this]
        {
            cout << 5 << endl;
            cout << *p << endl;
            cout << 6 << endl;
        };
    }
};

int main()
{
    auto foo = new Foo();
    cout << 1 << endl;
    auto f = foo->f();  // 获取了一个类内成员函数
    cout << 2 << endl;
    delete foo;  // 销毁这个对象
    cout << 3 << endl;
    // 尝试在销毁后继续使用这个对象,我们是通过lambda中*this捕获的新对象来访问的
    f();  
    cout << 4 << endl;

    return 0;
}

此时重新编译,就能成功访问到指针p指向的对象了,并不受foo对象已经被delete的影响;

$ ./test
1
2
3
5
10
6
4

10.字符串转换

没看懂这两个函数是干嘛的,找到的代码连编译都过不去,跳过吧

新增from_chars函数和to_chars函数

https://zh.cppreference.com/w/cpp/utility/from_chars
https://blog.csdn.net/defaultbyzt/article/details/120151801

11.std::variant

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

int main() { // c++17可编译
    std::variant<int, std::string> var("hello");
    cout << var.index() << endl;
    var = 123;
    cout << var.index() << endl;

    try {
        var = "world";
        std::string str = std::get<std::string>(var); // 通过类型获取值
        var = 3;
        int i = std::get<0>(var); // 通过index获取对应值
        cout << str << endl;
        cout << i << endl;
    } catch(...) {
        // xxx;
    }
    return 0;
}

注意:一般情况下variant的第一个类型一般要有对应的构造函数,否则编译失败:

struct A {
    A(int i){}  
};
int main() {
    std::variant<A, int> var; // 编译失败
}

如何避免这种情况呢,可以使用std::monostate来打个桩,模拟一个空状态。

std::variant<std::monostate, A> var; // 可以编译成功

12.std::optional

https://en.cppreference.com/w/cpp/utility/optional

有的时候,我们想在异常的时候抛出一个异常的对象,亦或者是在出现一些不可预期的错误的时候,返回一个空值。要怎么区分空值和异常的对象呢?

在python中,我们有一个专门的None对象可以来处理这件事。在MySQL中,我们也有NULL来标识空;但在CPP中,我们只剩下一个nullptr,其本质是个指针,与Py中的None和MySQL中的NULL完全不同!如果想用指针来区分空和异常对象,那就需要用到动态内存管理,亦或者是用智能指针来避免内存泄漏。

说人话就是,在CPP中没有一个类似None的含义为空的对象,来告诉调用这个程序的人,到底是发生了错误,生成了一个错误的对象,还是说压根什么都没有弄出来。

于是std::optional就出现了,其可以包含一个类型,并有std::nullopt来专门标识“空”这个含义。

#include <optional>
std::optional<int> StoI(const std::string &s) {
    try {
        return std::stoi(s);
    } catch(...) {
        return std::nullopt;
    }
}

void func() {
    std::string s{"123"};
    std::optional<int> o = StoI(s);
    if (o) {
        cout << *o << endl;
    } else {
        cout << "error" << endl;
    }
}

这里我们进行了if的判断,首先判断变量o本身,为真代表的确返回了一个int值,为假代表返回的是nullopt

随后再使用*o来访问到内部托管的成员。

需要注意这里是两层的逻辑关系,只有optional对象中成功托管了一个指定的参数类型,其本身才是真的。如果想访问它托管的对象,则需要用解引用。

比如这里,我们的o对象托管的是一个bool类型的假,但假并不代表空,o对象本身的判断是真,内部对*o的判断才是判断托管的bool值到底是真是假。

#include <optional>
int main() {
    std::optional<bool> o = false;
    cout << typeid(o).name() << endl;
    if (o) // 这里判断的是optional对象是否有托管一个bool值
    {
        if(*o){ // 这里判断的是托管的bool值本身
            cout << "true" << endl;
        }
        else{
            cout << "false" << endl;
        }
    } else { // 这里则代表托管的是nullopt
        cout << "error" << endl;
    }

    return 0;
}

最终运行打印的结果是false

13.std::any

https://en.cppreference.com/w/cpp/utility/any

这个类型可以托管任意类型的值,与之对应的还有一个std::any_cast来将其托管的值转成我们需要的类型。

#include <any>

int main() { // c++17可编译
    std::any a = 1;
    cout << a.type().name() << " " << std::any_cast<int>(a) << endl;
    a = 2.2f;
    cout << a.type().name() << " " << std::any_cast<float>(a) << endl;
    if (a.has_value()) {
        cout << a.type().name();
    }
    a.reset();
    if (a.has_value()) {
        cout << a.type().name();
    }
    a = std::string("a");
    cout << a.type().name() << " " << std::any_cast<std::string>(a) << endl;
    return 0;
}

输出结果如下

i 1
f 2.2
fNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE a

虽然any的出现让cpp也在一定程度上能实现“弱类型”变量,但在具体的开发中,明确变量的类型依旧比使用any好得多。特别是在变量的类型并不可以被直接转换的情况下。

14.std::apply

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

#include <tuple>
int add(int first, int second) { return first + second; }

auto add_lambda = [](auto first, auto second) { return first + second; };

int main() {
	std::cout << add(std::pair(1, 2)) << "\n"; // error
	
    std::cout << std::apply(add, std::pair(1, 2)) << '\n';
    std::cout << std::apply(add_lambda, std::tuple(2.0f, 3.0f)) << '\n';
}

15.std::make_from_tuple

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

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

16.std::string_view

https://zhuanlan.zhihu.com/p/166359481

https://en.cppreference.com/w/cpp/string/basic_string_view

如果我们只需要一个string的只读类型的话,可以用string_view来托管。其内部只包含一个指向目标字符串的指针,以及字符串的长度。

string_view内部封装了string的所有只读接口,本来就是给你读的。

需要注意的是,因为内部只有一个指针,所以当string_view托管的string被销毁了,与之关联的所有string_view都会失效!同样是因为内部只有一个指针和字符串的长度两个变量,所以在传值拷贝的时候,string_view的效率会高很多。

  • 这和const string& 类型的传值又有什么区别呢?传引用不是也没有拷贝消耗吗?

这个问题很好,我不知道!百度也没有百度出来……

我能想到的就是用string_view作为参数的时候,如果入参是一个常量字符串,此时不需要构造string,而使用const string& 接受常量字符串的时候依旧需要构造一个string对象。这部分就会有一定的消耗。

17.as_const

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

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

18.file_system

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

namespace fs = std::filesystem;
fs::create_directory(dir_path); // 创建文件或者路径
fs::copy_file(src, dst, fs::copy_options::skip_existing); // 文件cp
fs::exists(filename); // 文件是否存在
fs::current_path(err_code); // 获取当前路径

19.shared_mutex

这玩意是个读写锁。简单介绍一下什么是读写锁:

  • 读者可以有多个,写者只能有一个
  • 写锁是互斥的,如果A有锁,B想拿锁就得阻塞等待
  • 读锁是共享的,C有读锁,D也想读,两个人可以一起看
  • 读写锁是互斥的,有人写的时候不能读,有人读的时候不能写

换到专业术语上,就是分为独占锁(写锁)和共享锁(读锁);

在C++14中其实已经有了一个shared_timed_mutex,C++17中这个锁的操作与其基本一致,只不过多了几个和时间相关的接口

try_lock_for(...);
try_lock_shared_for(...);
try_lock_shared_until(...);
try_lock_until(...);

具体使用可以参考

https://zh.cppreference.com/w/cpp/thread/shared_mutex
https://zhuanlan.zhihu.com/p/610781321

The end

关于C++17常用的基本就是这些了,后续遇到新的再更新本文。

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

慕雪华年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值