C++17常用新特性
目录
一、结构化绑定
1.绑定到数组
2.绑定到结构体/类
3.与范围for结合遍历关联式容器
二、if和switch中的初始化语句
1.if初始化语句用法
2.switch初始化语句用法
三、if constexpr
1.普通函数中使用
2.模板函数中使用
四、内联变量
五、类模板参数推导
六、编译期Lambda
1.使用普通函数对象(仿函数)
2.使用Lambda表示式(C++17支持)
九、std::string_view
一、结构化绑定
允许将一个复合类型(数组、结构体/类、元组等)直接解包绑定到多个变量,极大的简化代码,更加高效。
1.绑定到数组
int main()
{int arr[3] = { 1,2,3 };auto& [a, b, c] = arr;cout << a; //1return 0;
}
2.绑定到结构体/类
只有结构体/类中的共有数据成员才可以被绑定。
绑定到std::pair和std::tuple
int main()
{//处理像std::map::insert返回的pair时,结构化绑定很有用map<int, string> myMap;const auto& [iter, success] = myMap.insert({ 1,"blue" });if (success) {cout << "Insertion successful!" << endl;}//绑定到std::tupletuple<int, float, string> tup(61, 1.1, "blue");auto [num, fl, msg] = tup;cout << num << "," << fl << "," << msg << endl;return 0;
}
3.与范围for结合遍历关联式容器
//C++17结构化绑定,清晰明了map<int, string> myMap = { {1,"blue"},{2,"blue"} };for (const auto& [key, value] : myMap) {cout << "Key: " << key << ", Value: " << value << endl;}
二、if和switch中的初始化语句
允许在if/switch的条件判断之前,定义并初始化一个局部变量,该变量的作用域仅限于这个if/switch语句块内。
1.if初始化语句用法
if (init-statement; condition) {// 如果 condition 为 true,执行这里的代码
} else { // 可选的 else// 如果 condition 为 false,执行这里的代码
}
vector<int> v = { 1,2,3,4,5,6 };int target = 3;if (auto iter = std::find(v.begin(), v.end(), target);iter != v.end()) {cout << "Found." << *iter << endl;}else {cout << "Not found." << endl;}
2.switch初始化语句用法
switch (init-statement; expression) {case value1:// ...break;case value2:// ...break;default:// ...
}
map<int, string> colers = { {1,"Blue"},{2,"Green"} ,{3,"Write"} };int colerId = 1;switch (auto it = colers.find(colerId);it != colers.end() ? it->second[0] : ' ') {case 'B':cout << "Found: " << it->second << endl;break;case 'G':cout << "Found: " << it->second << endl;break;default:cout << "Not found." << endl;}
三、if constexpr
编译期if是一种在编译时进行条件判断的机制,它允许在模板代码中根据常量表达式来选择不同的代码路径,不会编译被剔除的代码路径,从而避免一些语法或类型错误。
如果使用的是普通的if,那么编译器会尝试编译所有分支,可能导致类型不匹配的错误。
1.普通函数中使用
#include <iostream>void check(int x) {if constexpr (sizeof(int) == 4) { // 编译期常量std::cout << "int is 4 bytes on this platform\n";} else {std::cout << "int is not 4 bytes\n";}
}int main() {check(10); // 输出:int is 4 bytes on this platform(在大多数平台上)
}
2.模板函数中使用
template <typename T>
void printTypeInfo(const T& value)
{if constexpr (std::is_integral_v<T>) {cout << "Integral type: " << value << endl;}else if constexpr (std::is_floating_point_v<T>) {cout << "Floating point type: " << value << endl;}else if constexpr (std::is_same_v<T,std::string>) {cout << "String type: " << value << endl;}else {cout << "Unknow type" << endl;}
}
编译期常量布尔表达式
C++17标准库<type_traits>中提供的类型特性工具,用于在编译期查询某个类型T的属性。
- std::is_integral_v<T>:判断类型T是不是整型,比如:int、short、long、unsigned、bool等(会在编译期判断类型T是不是整型类型返回true或false)。
- std::is_floating_point_v<T>:判断类型T是不是浮点型,比如:float、double、long double。
- std::is_same_v<T, U>:判断类型T和U是否是完全相同的类型。
四、内联变量
允许在头文件中定义(而不仅仅是声明)一个变量,并且保证这个变量在整个程序中只有一个定义,避免了在多个翻译单元中包含同一个同文件时导致的 "重复定义" 链接错误。
//test.h(C++17之前错误的写法)const string str = "blue";
- 如果a.cpp和b.cpp都包含了test.h头文件,那么在链接时,str这个全局变量会有两个定义(一个在a.obj,一个在b.obj),导致链器报错。
- 传统的解决方法(C++17之前):使用extern关键字在test.h中声明变量,然后在一个仅且一个.cpp文件中定义这个变量,但这比较麻烦。
使用inline关键字:
//test.h(C++17的正确写法)inline string str = "blue";
- 在多个源文件中包含test.h时,链接器会知道这些str变量都指向同一个实体,不会发生重复定义的错误。
五、类模板参数推导
不需要制定模板参数,编译器会根据构造函数参数自动推导。简化模板类实例化,特别是vector、map、pair、tuple等类型。
std::pair p(1, 3.14); //推导为 std::pair<int,double>std::tuple t(1, 3.14, "blue"); //推导为 std::tuple<int,double,const char*> std::vector v = { 1,2,3 }; //推到为 std::vector<int>
六、编译期Lambda
C++17允许Lambda表达式在constexpr上下文中使用,Lambda表达式本身也可以是constexpr,提升运行时效率。
constexpr int calculator()
{auto lam = [](int x) {return x * x;};return lam(3);
}constexpr auto lam = [](int x) {return 2 * x;};
七、std::optional<T>
optional<T>是C++17引入的一个类模板,用于表示一个可能有值,也可能没有值(即”空“)的对象,它的主要目的是用来替代传统通过一些方式,比如:使用nullptr、-1或bool类型的变量,来表示某个值可能有或没有的情况,这种传统的方式是不安全也不太优雅。
C++17中的optional<T>更安全,更现代,不需要依赖特殊值nullptr、-1等避免误用这些特殊值。
struct User
{string name;int age;
};std::optional<User> find_user_by_id(const int& id)
{if (id == 99) {return User{ "张三",20 };}else {return nullopt;}
}int main()
{User user{ "张三",20 };auto o = find_user_by_id(55);if (o.has_value()) {cout << "name: " << o->name << " age: " << o->age << endl;}else {cout << "Not found." << endl;}return 0;
}
- 判断std::optional<T>类型对象o是否有值:if(o.has_value)或if(o)。
- 访问内部数据:o.value()(无值会抛出std::bad_optional_access异常)或o.value_or(def)(较安全,无值返回设置的默认值def)。
- std::nullopt:表示空值。
- std::reset():清空o中的值。
- std::emplace(args...):重新构造o中的新值。
- *o/o->:访问o中的值。
八、std::variant<T...>和std::visit
C++17中的variant是安全类型的联合体,相较于union具有类型安全、现代、功能丰富的优点。
使用传统的union存储多个数据时,需要我们自己跟踪记录当前存的是哪个类型的数据,防止错误的类型会导致未定义行为,variant配合visit则解决了这个问题。
使用std::variant时需要包含头文件<variant>。
std::variant<int, string> var;var = string("hhh");try {cout << std::get<string>(var) << endl;}catch (const bad_variant_access& exception) {cout << exception.what() << endl;}
- 使用std::get<T>获取variant对象当前存储的数据时,该数据类型必须和T的类型一致,负责会抛出std::bad_variant_access异常。
std::visit(visitor,var)传入一个访问者visitor,它会根据variant当前存储的实际类型,调用对应的处理函数。
1.使用普通函数对象(仿函数)
using MyVariant = std::variant<int, double, string>;
class MyVisitor
{
public:void operator()(int i) const{cout << "Is int: " << i << endl;}void operator()(const string& s) const{cout << "Is string" << s << endl;}
};int main()
{MyVariant v1 = 18;MyVariant v2 = string("blue");MyVisitor visitor;std::visit(visitor, v1);std::visit(visitor, v2);reuturn 0;
}
2.使用Lambda表示式(C++17支持)
using MyVariant = std::variant<int, double, string>;
int main()
{MyVariant var = 8888;std::visit([](auto&& arg) {using T = std::decay_t<decltype(arg)>;if constexpr (std::is_same_v<T, int>) {cout << "Is int: " << arg << endl;}else if constexpr (std::is_same_v<T, string>) {cout << "Is string: " << arg << endl;}}, var);return 0;
}
std::decay_t<T>
- std::decay_t<T>是C++11中的类型转换工具,定义在<type_traits>中,他是std::decay<T>::type的简写形式(C++14开始支持_t后缀的快捷方式)。
- std::decay_t<T>主要对类型做了:移除引用、移除const,volatile属性、将数组和函数类型转换为对应的指针类型。
九、std::string_view
string_view是非拥有式字符串视图,提供对字符串数据的只读访问,使用时需包含头文件#include <string_view>
关键点:
- 非拥有式:string_view对象部管理内存,只保存指向字符串的指针和字符串的大小。
- 性能优异:避免了不必要的字符串拷贝和内存分配(底层只是复制指针和大小)。
- 只读属性:不能通过string_view对象修改底层数据。
- 生命周期:必须确保底层数据的生命周期长于string_view对象,防止悬空引用。
string_view几个常见的陷阱:
string_view create_dangerous_view()
{string str("temporary str");string_view dangerous_view = str;return dangerous_view;//Error! str底层管理的数据空间已销毁//dangerous_view仍然指向被销毁的无效空间
}
void dangerous_lifetime()
{string_view dangerous_view;{string str = "temporary str";dangerous_view = str;cout << "在作用域内:" << dangerous_view << endl;}//Error 未定义行为!cout << "在作用域外:" << dangerous_view << endl;return;
}