学习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
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常用的基本就是这些了,后续遇到新的再更新本文。