上一篇翻译了C++14的新特性简介,这篇就是Anthony Calandra在https://github.com/AnthonyCalandra/modern-cpp-features/blob/master/CPP17.md 中对C++17重要的新特性的简介。原文中有些地方写得不是很好理解所以对其做了少量修改
C++14的内容主要是对C++11的补充,被称为C++11的完整版。相比之下C++17的新内容要多很多,是完善的再完善
C++20由于好多特性都没被支持好,本身又属于非常大的一次变革,所以那部分就暂不翻译了
新特性一览
语言新特性
标准库新特性
类模板的模板参数推断(Template argument deduction for class templates)
对类模板的模板参数的推断就像编译器对函数参数的推导一样,只是如今可以用在模板类的构造中了
template <typename T = float>
struct MyContainer {
T val;
MyContainer() : val{} {}
MyContainer(T val) : val{val} {}
// ...
};
MyContainer c1 {1}; // OK MyContainer<int>
MyContainer c2; // OK MyContainer<float>
用auto来声明非类型的模板参数(Declaring non-type template parameters with auto)
遵循着auto的推断规则,同时也遵循那些被允许的类型*的非类型模板参数(C++Primer16.1中有介绍)列表,模板参数可以从它的实参的类型中推断出来了
template <auto... seq>
struct my_integer_sequence {
// Implementation here ...
};
// Explicitly pass type `int` as template argument.
auto seq = std::integer_sequence<int, 0, 1, 2>();
// Type is deduced to be `int`.
auto seq2 = my_integer_sequence<0, 1, 2>();
* ——例如:由于你不能用一个double类型作为模板参数的类型,因而也不能对其进行auto推断
折叠表达式(Folding expressions)
折叠表达式是为了优化C++11引入的参数包而生的。折叠表达式可以在一个二元运算符上对类参数包进行折叠
(... op e)
或(e op ...)
的表达式,op
是一个二元运算符而e
是一个未展开的参数包时,称此为一元折叠op
是一个二元运算符,称此为二元折叠。这里e1
或e2
的两者之一是未展开的参数包,且注意两个op
需要时是相同的运算符template <typename... Args>
auto sum(Args... args) {
// Unary folding.
return (... + args);
}
sum(1.0, 2.0f, 3); // == 6.0
template <typename... Args>
bool logicalAnd(Args... args) {
// Binary folding.
return (true && ... && args);
}
bool b = true;
bool& b2 = b;
logicalAnd(b, b2, true); // == true
auto对花括号初始化的新推断规则(New rules for auto deduction from braced-init-list)
当使用统一初始化运算符(花括号)时auto的推断发生了一些变化。在之前auto x {3};会推断出一个std::initializer_list<int>,如今会推断出int
auto x1 {1, 2, 3}; // error: not a single element
auto x2 = {1, 2, 3}; // x2 is std::initializer_list<int>
auto x3 {3}; // x3 is int
auto x4 {3.0}; // x4 is double
Lambda的常量表达式形式(constexpr lambda)
想要得到编译期的Lambda的话可以使用constexpr关键字
auto identity = [](int n) constexpr { return n; };
static_assert(identity(123) == 123);
constexpr auto add = [](int x, int y) {
auto L = [=] { return x; };
auto R = [=] { return y; };
return [=] { return L() + R(); };
};
static_assert(add(1, 2)() == 3);
constexpr int addOne(int n) {
return [n] { return n + 1; }();
}
static_assert(addOne(1) == 2);
Lambda可以值捕获this了(Lambda capture this by value)
之前的时候在Lambda中捕获的this只能是引用形式的。一个有问题的例子是对于引用捕获,Lambda要求其对象必须确实存在,但有可能到了调用的时候已经超过了目标对象的生命周期。现在我们可以使用*this(C++17)得到对当前对象的一个拷贝,而用this(C++11)则会继续捕获目标的引用
struct MyObj {
int value {123};
auto getValueCopy() {
return [*this] { return value; };
}
auto getValueRef() {
return [this] { return value; };
}
};
MyObj mo;
auto valueCopy = mo.getValueCopy();
auto valueRef = mo.getValueRef();
mo.value = 321;
valueCopy(); // 123
valueRef(); // 321
内联变量(Inline variables)
inline关键字现在可以像用在函数上一样用在变量上了。一个被声明内联的变量会得到和内联函数一样的语义
// Disassembly example using compiler explorer.
struct S { int x; };
inline S x1 = S{321}; // mov esi, dword ptr [x1]
// x1: .long 321
S x2 = S{123}; // mov eax, dword ptr [.L_ZZ4mainE2x2]
// mov dword ptr [rbp - 8], eax
// .L_ZZ4mainE2x2: .long 123
这也可以被用来声明或定义一个静态成员变量,这样它就可以不用必须在源文件中初始化了
struct S {
S() : id{count++} {}
~S() { count--; }
int id;
static inline int count{0}; // declare and initialize count to 0 within the class
};
嵌套的命名空间定义(Nested namespaces)
可以用命名空间解析运算符(在命名空间后用作用域运算符)来定义一个嵌套的命名空间了
namespace A {
namespace B {
namespace C {
int i;
}
}
}
// vs.
namespace A::B::C {
int i;
}
结构化绑定(Structured bindings)
这是一个对非结构化初始化的建议,就是现在允许了当expr是一个类似tuple的对象时可以用auto [ x, y, z ] = expr;来初始化,对象中的元素会被绑定到x,y和z上
类似tuple的对象包括std::tuple
, std::pair
, std::array
和其他一些聚合结构
using Coordinate = std::pair<int, int>;
Coordinate origin() {
return Coordinate{0, 0};
}
const auto [ x, y ] = origin();
x; // == 0
y; // == 0
std::unordered_map<std::string, int> mapping {
{"a", 1},
{"b", 2},
{"c", 3}
};
// Destructure by reference.
for (const auto& [key, value] : mapping) {
// Do something with key and value
}
带有初始化器的条件语句(Selection statements with initializer)
if和switch的新版本条件语句简化了常见的代码模式并帮助用户进一步保持代码紧凑
{
std::lock_guard<std::mutex> lk(mx);
if (v.empty()) v.push_back(val);
}
// vs.
if (std::lock_guard<std::mutex> lk(mx); v.empty()) {
v.push_back(val);
}
Foo gadget(args);
switch (auto s = gadget.status()) {
case OK: gadget.zip(); break;
case Bad: throw BadFoo(s.message());
}
// vs.
switch (Foo gadget(args); auto s = gadget.status()) {
case OK: gadget.zip(); break;
case Bad: throw BadFoo(s.message());
}
常量表达式if(constexpr if)
可以编写一些依据编译期状态初始化的代码了
template <typename T>
constexpr bool isIntegral() {
if constexpr (std::is_integral<T>::value) {
return true;
} else {
return false;
}
}
static_assert(isIntegral<int>() == true);
static_assert(isIntegral<char>() == true);
static_assert(isIntegral<double>() == false);
struct S {};
static_assert(isIntegral<S>() == false);
UTF-8的字符字面量(UTF-8 character literals)
一个以u8开头的字符字面量是char类型的。UTF-8字符字面量的值与它的ISO 10646标准的字符点值相同。
char x = u8'x';
枚举的直接列表初始化(Direct list initialization of enums)
枚举现在可以用花括号直接初始化了
enum byte : unsigned char {};
byte b {0}; // OK
byte c {-1}; // ERROR
byte d = byte{1}; // OK
byte e = byte{256}; // ERROR
[[fallthrough]], [[nodiscard]], [[maybe_unused]]属性(fallthrough, nodiscard, maybe_unused attributes)
C++17还带来了三个新的属性:[[fallthrough]]
, [[nodiscard]]
和[[maybe_unused]]
[[fallthrough]]
向编译器指明从某个switch的case滑下(也就是没有break打断)是设计中的行为switch (n) {
case 1: [[fallthrough]]
// ...
case 2:
// ...
break;
}
[[nodiscard]]
会在拥有这个属性的类或函数的返回值被忽略时提出警告[[nodiscard]] bool do_something() {
return is_success; // true for success, false for failure
}
do_something(); // warning: ignoring return value of 'bool do_something()',
// declared with attribute 'nodiscard'
// Only issues a warning when `error_info` is returned by value.
struct [[nodiscard]] error_info {
// ...
};
error_info do_something() {
error_info ei;
// ...
return ei;
}
do_something(); // warning: ignoring returned value of type 'error_info',
// declared with attribute 'nodiscard'
[[maybe_unused]]
向编译器指明某个变量或参数可能不会被使用到是设计中的行为
void my_callback(std::string msg, [[maybe_unused]] bool error) {
// Don't care if `msg` is an error message, just log it.
log(msg);
}
std::variant
标准库模板类std::variant(变体/变种)代表了一个类型安全的union。一个std::variant的实例每个时刻都只保留候选类型中的一个值(当然也可以是无值的),就像联合一样
std::variant<int, double> v{ 12 };
std::get<int>(v); // == 12
std::get<0>(v); // == 12
v = 12.0;
std::get<double>(v); // == 12.0
std::get<1>(v); // == 12.0
std::optional
标准库模板类std::optional(可选项)维护了一个可选的包含值,例如,一个可能存在也可能不存在的值。一个常见的可选项的使用情形就是作为可能失败的函数的返回值
std::optional<std::string> create(bool b) {
if (b) {
return "Godzilla";
} else {
return {};
}
}
create(false).value_or("empty"); // == "empty"
create(true).value(); // == "Godzilla"
// optional-returning factory functions are usable as conditions of while and if
if (auto str = create(true)) {
// ...
}
std::any
这是对任何类型的单值的一个类型安全的容器
std::any x {5};
x.has_value() // == true
std::any_cast<int>(x) // == 5
std::any_cast<int&>(x) = 10;
std::any_cast<int>(x) // == 10
std::string_view
是对一个字符串的非拥有的引用,可以运用成员函数自由地改变观测这个字符串的方式,构造速度很快但是无法修改这个字符串。对在字符串上提供一个抽象很有用(例如进行字符串分析)
// Regular strings.
std::string_view cppstr {"foo"};
// Wide strings.
std::wstring_view wcstr_v {L"baz"};
// Character arrays.
char array[3] = {'b', 'a', 'r'};
std::string_view array_v(array, std::size(array));
std::string str {" trim me"};
std::string_view v {str};
v.remove_prefix(std::min(v.find_first_not_of(" "), v.size()));
str; // == " trim me"
v; // == "trim me"
std::invoke
这可以带有参数地包装调用一个Callable可调用对象。可调用对象就是类似 std::function
或 std::bind
那样的可以类似普通的函数那样被调用的对象
template <typename Callable>
class Proxy {
Callable c;
public:
Proxy(Callable c): c(c) {}
template <class... Args>
decltype(auto) operator()(Args&&... args) {
// ...
return std::invoke(c, std::forward<Args>(args)...);
}
};
auto add = [](int x, int y) {
return x + y;
};
Proxy<decltype(add)> p {add};
p(1, 2); // == 3
std::apply
可以用tuple来作为参数包装调用一个可调用对象
auto add = [](int x, int y) {
return x + y;
};
std::apply(add, std::make_tuple(1, 2)); // == 3
std::filesystem
新的文件系统库std::filesystem提供了在文件系统中控制多文件,多目录,多路径的标准方法
就像下面例子,在有可用空间的情况下将一个大文件拷贝到一个临时路径中
const auto bigFilePath {"bigFileToCopy"};
if (std::filesystem::exists(bigFilePath)) {
const auto bigFileSize {std::filesystem::file_size(bigFilePath)};
std::filesystem::path tmpPath {"/tmp"};
if (std::filesystem::space(tmpPath).available > bigFileSize) {
std::filesystem::create_directory(tmpPath.append("example"));
std::filesystem::copy_file(bigFilePath, tmpPath.append("newFile"));
}
}
std::byte
新的类型std::byte提供了以位格式表示数据的标准方法。std::byte比起char和unsigned char的好处在于它不是一种字符类型也不是一种算术类型,因此它只有可用的重载运算符只有位运算符
std::byte a {0};
std::byte b {0xFF};
int i = std::to_integer<int>(b); // 0xFF
std::byte c = a & b;
int j = std::to_integer<int>(c); // 0
注意std::byte只是一个枚举enum而已,多亏了枚举类型的直接列表初始化特性才能向上面一样优雅地使用它
拼接map和set(Splicing for maps and sets)
现在可以在避免拷贝,移动,堆内存分配的高额代价下移动结点和合并容器了
从一个map移动元素到另一个map中:
std::map<int, string> src {{1, "one"}, {2, "two"}, {3, "buckle my shoe"}};
std::map<int, string> dst {{3, "three"}};
dst.insert(src.extract(src.find(1))); // Cheap remove and insert of { 1, "one" } from `src` to `dst`.
dst.insert(src.extract(2)); // Cheap remove and insert of { 2, "two" } from `src` to `dst`.
// dst == { { 1, "one" }, { 2, "two" }, { 3, "three" } };
插入一整个set进去:
std::set<int> src {1, 3, 5};
std::set<int> dst {2, 4, 5};
dst.merge(src);
// src == { 5 }
// dst == { 1, 2, 3, 4, 5 }
插入比容器自身寿命还长的元素:
auto elementFactory() {
std::set<...> s;
s.emplace(...);
return s.extract(s.begin());
}
s2.insert(elementFactory());
改变一个map元素的key:
std::map<int, string> m {{1, "one"}, {2, "two"}, {3, "three"}};
auto e = m.extract(2);
e.key() = 4;
m.insert(std::move(e));
// m == { { 1, "one" }, { 3, "three" }, { 4, "two" } }
并行算法(Parallel algorithms)
许多STL算法例如copy,find,sort等,开始支持并行执行策略(parallel execution policies):串行seq,并行par,无序并行par_unseq
std::vector<int> longVector;
// Find element using parallel execution policy
auto result1 = std::find(std::execution::par, std::begin(longVector), std::end(longVector), 2);
// Sort elements using sequential execution policy
auto result2 = std::sort(std::execution::seq, std::begin(longVector), std::end(longVector));