这一部分介绍C++17对已有标准库组件的拓展和修改。
自从C++17起,对所有返回值的类型特征使用后缀_v,例如:
std::is_const_v<T>; // C++17
std::is_const<T>::value; // C++11
这适用于所有返回值的类型特征,也可以用作运行时的条件表达式。
特征 | 效果 |
---|---|
is_aggregate | 是否是聚合体类型 |
is_swappable | 该类型是否能调用swap() |
is_nothrow_swapable | 该类型是否能调用swap()并且不会抛出异常 |
is_swappable_with |
特定的两个类型是否能交换(swap()) |
is_nothrow_swappable_with |
特定的两个类型是否能交换(swap())并不会抛出异常 |
has_unique_object_representations | 是否该类型的两个值相等的对象在内存中的表示也一样 |
is_invocable |
是否可以用Args... 调用 |
is_nothrow_invocable |
是否可以用Args... 调用并不会抛出异常 |
is_invocable_r | 该类型是否可以用Args... 调用,且返回RT 类型 |
is_nothrow_invocable_r | 该类型是否可以用Args... 调用,且返回RT 类型并且不会抛出异常 |
invoke_result |
用Args... 作为实参进行调用会返回的类型 |
conjunction | 对bool特征B... 进行逻辑与 |
disjunction | 对bool特征B... 进行逻辑或运算 |
negation | 对bool特征B进行非运算 |
is_execution_policy | 是否执行策略类型 |
另外,is_literal_type<>
和result_of<>
自从C++17起被废弃。
template<typename T>
struct D : string, complex<T>
{
string data;
};
struct C
{
bool operator()(int) const
{
return true;
}
};
string foo(int);
using T1 = invoke_result_t<decltype(foo), int>; // string
// 是否是指针
template<typename T>
struct IsPtr : disjunction<is_null_pointer<T>, // 空指针
is_member_pointer<T>, // 成员函数指针
is_pointer<T>> // 原生指针
{};
int main()
{
D<float> s{ {"hello"},{4.5,6.7},"world" };
cout << is_aggregate_v<decltype(s)> << endl; // 1
cout << is_swappable_v<int> << endl; // 1
cout << is_swappable_with_v<int, int> << endl; // 1
cout << is_invocable_v<C> << endl; // 0
cout << is_invocable_v<C,int> << endl; // 1
cout << is_invocable_v<int(*)()> << endl; // 1
cout << is_invocable_r_v<bool, C, int> << endl; // 1
}
为了从现代的多核体系中受益,C++17标准库引入了并行STL算法来使用多个线程并行处理元素。
许多算法扩展了一个新的参数来指明是否要并行运行算法。
一个简单的计时器辅助类:
设计一个计算器来测量算法的速度。
#include
/*****************************************
* timer to print elapsed time
******************************************/
class Timer
{
private:
chrono::steady_clock::time_point last;
public:
Timer() : last{ chrono::steady_clock::now() }
{}
void printDiff(const std::string& msg = "Timer diff: ")
{
auto now{ chrono::steady_clock::now() };
chrono::duration<double, milli> diff{ now - last };
cout << msg << diff.count() << "ms\n";
last = chrono::steady_clock::now();
}
};
#include
#include // 执行策略,包含并行模式
#include
int main()
{
int numElems = 1000000;
struct Data
{
double value; // 初始值
double sqrt; // 计算平方根
};
// 初始化numElems个还没有计算平方根的值
vector<Data> coll;
coll.reserve(numElems);
for (int i = 0; i < numElems; ++i)
coll.push_back(Data{ i * 4.37,0 });
for (int i = 0; i < 5; ++i)
{
Timer t;
// 顺序计算平方根
for_each(execution::seq, coll.begin(), coll.end(), [](auto& val)
{
val.sqrt = sqrt(val.value);
});
t.printDiff("sequential: ");
// 并行计算平方根
for_each(execution::par, coll.begin(), coll.end(), [](auto& val)
{
val.sqrt = sqrt(val.value);
});
t.printDiff("parallel: ");
cout << endl;
}
}
测试结果中,只有少量元素的时候,串行算法明显快得多。这是因为启动和管理线程占用了太多的事件。但是当元素很大的时候,并行执行的效率就大大提升。
值得使用并行算法的关键在于:
排序是另一个并行算法的例子。
int main()
{
int numElems = 100000;
vector<string> coll;
for (int i = 0; i < numElems / 2; ++i)
{
coll.emplace_back("id" + to_string(i));
coll.emplace_back("ID" + to_string(i));
}
for (int i = 0; i < 5; ++i)
{
Timer t;
// 顺序排序
sort(execution::seq, coll.begin(), coll.end(), [](const auto& a,const auto& b)
{
return string_view{ a }.substr(2) < string_view{ b }.substr(2);
});
t.printDiff("sequential: ");
// 并行排序
sort(execution::par, coll.begin(), coll.end(), [](const auto& a, const auto& b)
{
return string_view{ a }.substr(2) < string_view{ b }.substr(2);
});
t.printDiff("parallel: ");
// 并行化乱序排序
sort(execution::par_unseq, coll.begin(), coll.end(), [](const auto& a, const auto& b)
{
return string_view{ a }.substr(2) < string_view{ b }.substr(2);
});
t.printDiff("parallel unsequential: ");
cout << endl;
}
}
你可以像并行STL算法传递不同的执行策略作为第一个参数。定义在头文件
中。
策略 | 含义 |
---|---|
std::execution::seq | 顺序执行 |
std::execution::par | 并行化顺序执行 |
std::execution::par_unseq | 并行化乱序执行(保证不会出现死锁) |
当处理元素的函数因为未捕获的异常而退出时所有的并行算法会调用terminate()
。
并行算法本身也可能会抛出异常。比如它们申请并行执行所需的临时内存资源时失败了,可能会抛出bad_alloc
异常。
无限制的并行算法:
find_end()
,adjacent_find()
search()
,search_n()
swap_ranges()
replace()
,replace_if()
fill()
generate()
remove()
,remove_if()
,unique()
reverse()
,rotate()
partition()
,stable_sort()
,partial_sort()
is_sorted()
,is_sorted_until()
nth_element()
inplace_merge()
is_heap()
,is_heap_until()
min_element()
,max_element()
,min_max_element()
无并行版本的算法:
accmulate()
partial_sum()
inner_product()
search()
copy_backward()
,move_backward()
sample()
,shuffle()
partition_point()
lower_bound()
,upper_bound()
,equal_range()
binary_serach()
is_permutation()
next_permutation()
,prev_permutation()
push_heap()
,pop_hep()
,make_heap()
,sort_heap()
对于一下算法,可以使用对应的并行算法来替代:
accumulate()
,使用reduce()
或者transform_reduce()
inner_product()
,使用transform_reduce()
C++17还引入了一些补充的算法来实现从C++98就可以的标准算法的并行执行。
reduce
是accumulate()
的并行化版本,但是也有所不同,下面分几个场景介绍:
可结合可交换操作的并行化(例如加法):
void printSum(long num)
{
vector<long> coll;
coll.reserve(num * 4);
for (long i = 0; i < num; ++i)
{
coll.insert(coll.end(), { 1,2,3,4 });
}
Timer t;
auto sum = reduce(execution::par, coll.begin(), coll.end(), 0L);
t.printDiff("reduce: ");
sum = accumulate(coll.begin(), coll.end(), 0L);
t.printDiff("accumulate: ");
cout << "sum: " << sum << endl;
}
int main()
{
printSum(1);
printSum(1000);
printSum(1000000);
}
不可交换操作的并行化(浮点数相加):
void printSum(long num)
{
vector<double> coll;
coll.reserve(num * 4);
for (long i = 0; i < num; ++i)
{
coll.insert(coll.end(), { 0.1,0.3,0.00001 });
}
Timer t;
double sum1 = reduce(execution::par, coll.begin(), coll.end(), 0.0);
t.printDiff("reduce: ");
double sum2 = accumulate(coll.begin(), coll.end(), 0.0);
t.printDiff("accumulate: ");
cout << (sum1 == sum2 ? "equal\n" : "differ\n");
}
int main()
{
cout << setprecision(5); // 浮点数精度
printSum(1); // equal
printSum(1000); // equal
printSum(1000000); // differ
}
不可结合操作的并行化(累积操作改为加上每个值的平方):
void printSum(long num)
{
vector<long> coll;
coll.reserve(num * 4);
for (long i = 0; i < num; ++i)
{
coll.insert(coll.end(), { 1,2,3,4 });
}
auto squareSum = [](auto sum, auto val) {
return sum + val * val;
};
auto sum = accumulate(coll.begin(), coll.end(), 0L, squareSum);
cout << "accumulate(): " << sum << endl;
auto sum1 = reduce(execution::par,coll.begin(), coll.end(), 0L, squareSum);
cout << "reduce(): " << sum << endl;
}
int main()
{
printSum(1);
printSum(1000);
printSum(10000000);
}
reduce()
有可能会导致结果不正确。因为这个操作是不可结合的。假设有三个元素1、2、3。在计算过程中可能导致计算的是:
(0 + 1 * 1) + (2 + 3 * 3) * (2 + 3 * 3)
解决这个问题的方法是用另一个新算法transform_reduce()
。把我们对每一个元素的操作和可交换的结果的累计都并行化。
void printSum(long num)
{
vector<long> coll;
coll.reserve(num * 4);
for (long i = 0; i < num; ++i)
{
coll.insert(coll.end(), { 1,2,3,4 });
}
auto sq = [](auto val) {
return val * val;
};
auto sum = transform_reduce(execution::par, coll.begin(), coll.end(), 0L, plus{}, sq);
cout << "transform_reduce(): " << sum << endl;
}
transform_reduce
的参数:
使用transform_reduce()
进行文件系统操作:
int main(int argc, char* argv[])
{
if (argc < 2)
{
cout << "Usage: " << argv[0] << " \n" ;
return EXIT_FAILURE;
}
namespace fs = filesystem;
fs::path root{ argv[1] };
// 初始化文件树中所有文件路径的列表
vector<fs::path> paths;
try
{
fs::recursive_directory_iterator dirpos{ root };
copy(begin(dirpos), end(dirpos), back_inserter(paths));
}
catch (const exception& e)
{
cerr << "EXCEPTION: " << e.what() << endl;
return EXIT_FAILURE;
}
// 累积所有普通文件的大小
auto sz = transform_reduce(
execution::par,
paths.cbegin(), paths.cend(),
uintmax_t{ 0 },
plus<>(),
[](const fs::path& p)
{
return is_regular_file(p) ? file_size(p) : uintmax_t{ 0 };
}
);
cout << "size of all " << paths.size()
<< "regular files: " << sz << endl;
}
作为并行STL算法的一部分,原本的for_each_n
又有了一个新的并行版本。类似于copy_n()
、fill_n()
、generate_n()
,这个算法需要一个整数参数指出要对范围内多少个元素进行操作。
InputIterator
for_each_n (ExecutionPolicy&& pol,
InputIterator beg,
Size count,
UnaryProc op);
例如:
int main()
{
vector<string> coll;
for (int i = 0; i < 10000; ++i)
{
coll.push_back(to_string(i));
}
// 修改前五个元素
for_each_n(coll.begin(), 5,
[](auto& elem)
{
elem = "value " + elem;
});
for_each_n(coll.begin(), 10,
[](const auto& elem)
{
cout << elem << endl;
});
}
这些算法都定义在
中。
typename iterator_traits<InputIterator>::value_type
reduce (ExecutionPolicy&& pol, // 可选的
InputIterator beg, InputIterator end);
T
reduce (ExecutionPolicy&& pol, // 可选的
InputIterator beg, InputIterator end,
T initVal);
T
reduce (ExecutionPolicy&& pol, // 可选的
InputIterator beg, InputIterator end,
T initVal,
BinaryOp op);
变种一:
T
transform_reduce (ExecutionPolicy&& pol, // 可选的
InputIterator beg,InputIterator end,
T initVal,
BinaryOp op2,UnaryOp op1)
initVal op2 op1(a1) op2 op1(a2) op2 op1(a3) op2 ...
变种二:
T
transform_reduce (ExecutionPolicy&& pol, // 可选的
InputIterator beg1, InputIterator end1,
InputIterator beg2,
T initVal);
T
transform_reduce (ExecutionPolicy&& pol, // 可选的
InputIterator beg1, InputIterator end1,
InputIterator beg2,
T initVal,
BinaryOp1 op1, BinaryOp2 op2);
initVal
。initVal += elem1 * elem2
。op2
,然后对initVal
和上一步的结果调用op1
。initVal = op1(initVal, op2(elem1, elem2))
。OutputIterator
inclusive_scan (ExcutionPolicy&& pol, // 可选的
InputIterator inBeg, InputIterator inEnd,
OutputIterator outBeg,
BinaryOp op, // 可选的
T initVal // 可选的
);
OutputIterator
exclusive_scan (ExcutionPolicy&& pol, // 可选的
InputIterator inBeg, InputIterator inEnd,
OutputIterator outBeg,
T initVal, // 必须的
BinaryOp op // 可选的
);
[inBeg, inEnd)
内每个元素和之前所有元素组合之后的值并写入以outBeg
开头的目标范围。a1 a2 a3 ... aN
,inclusive_scan()
计算initVal op a1, initVal op a1 op a2, initVal op a1 op a2 op a3, ...aN
a1 a2 a3 ... aN
,exclusive_scan()
计算initVal, initVal op a1, initVal op a1 op a2, ... aN-1
plus
。initVal
,将不会添加初始值。第一个输出的值将直接是第一个输入的值。op
不能修改传入的参数。int main()
{
array coll{ 3,1,7,0,4,1,6,3 };
cout << " inclusive scan(): ";
inclusive_scan(coll.begin(), coll.end(),
ostream_iterator(cout, " "));
cout << "\n exclusive_scan(): ";
exclusive_scan(coll.begin(), coll.end(),
ostream_iterator(cout, " "),
0);
cout << "\n inclusive scan(): ";
inclusive_scan(coll.begin(), coll.end(),
ostream_iterator(cout, " "),
plus{},100);
cout << "\n exclusive_scan(): ";
exclusive_scan(coll.begin(), coll.end(),
ostream_iterator(cout, " "),
100, plus{});
}
输出结果:
inclusive scan(): 3 4 11 11 15 16 22 25
exclusive_scan(): 0 3 4 11 11 15 16 22
inclusive scan(): 103 104 111 111 115 116 122 125
exclusive_scan(): 100 103 104 111 111 115 116 122
OutputIterator
transform_inclusive_scan (ExcutionPolicy&& pol, // 可选的
InputIterator inBeg, InputIterator inEnd,
OutputIterator outBeg,
BinaryOp op2, // 必须的
UnaryOp op1, // 必须的
T initVal // 可选的
);
OutputIterator
transform_exclusive_scan (ExcutionPolicy&& pol, // 可选的
InputIterator inBeg, InputIterator inEnd,
OutputIterator outBeg,
T initVal, // 必须的
BinaryOp op2, // 必须的
UnaryOp op1, // 必须的
);
a1 a2 a3 ... aN
,transform_inclusive_scan
计算initVal op2 op1(a1), initVal op2 op1(a1) op2 op1(a2), ... op2 op1(aN)
。a1 a2 a3 ... aN
,transform_exclusive_scan
计算initVal, initVal op2 op1(a1), initVal op2 op1(a1) op2 op1(a2), ... op2 op1(aN-1)
。int main()
{
array coll{ 3,1,7,0,4,1,6,3 };
auto twice = [](int v) { return v * 2; };
cout << " source: ";
copy(coll.begin(), coll.end(), ostream_iterator<int>(cout, " "));
cout << "\n transform_inclusive_scan(): ";
transform_inclusive_scan(coll.begin(), coll.end(),
ostream_iterator<int>(cout, " "),
plus{}, twice);
cout << "\n transform_inclusive_scan(): ";
transform_inclusive_scan(coll.begin(), coll.end(),
ostream_iterator<int>(cout, " "),
plus{}, twice, 100);
cout << "\n transform_exclusive_scan(): ";
transform_exclusive_scan(coll.begin(), coll.end(),
ostream_iterator<int>(cout, " "),
100, plus{}, twice);
}
新的搜索器主要是用来在长文本中搜索字符串(例如单词或者短语)。因此首先演示一下在什么情况下使用它们,以及带来的改变。
字符串成员函数find()
size_t idx = text.find(sub);
算法search()
auto pos = std::search(text.begin(), text.end(), sub.begin(), sub.end());
并行算法search()
auto pos = std::search(execution::par, text.begin(), text.end(), sub.begin(), sub.end());
使用default_searcher
auto pos = search(text.begin(), text.end(), default_searcher{ sub.begin(),sub.end() });
使用boyer_moore_searcher
auto pos = search(text.begin(), text.end(), boyer_moore_searcher{ sub.begin(),sub.end() });
使用boyer_moore_horspool_searcher
auto pos = search(text.begin(), text.end(), boyer_moore_horspool_searcher{ sub.begin(),sub.end() });
Boyer-Moore
和Boyer-Moore-Horspool
搜索器是非常著名的在搜索之前预计算“表”(存放哈希值)的算法,当搜索区间非常大时可以显著加快搜索速度。算法需要随机访问迭代器。
搜索器的性能:
search()
通常是最慢的方法,因为对于text
中的每个字符,我们都要查找以它开头的子串是否匹配搜索目标。find()
可能会更快。这依赖于标准库实现的质量。boyer_moore_searcher
应该是最快的方法。和search()
相比,甚至可能提供50倍到100倍左右的性能。boyer_moore_horspool_searcher
用时间换空间,虽然比boyer_moore_searcher
慢,但占用的内存会更少。search()
的速度是普通的三倍,但远远小于新增的搜索器。另一个使用Boyer-Moore
搜索器的方法是:你可以直接使用搜索器的函数调用运算符,会返回一个匹配子序列开头和尾后迭代器的pair。
boyer_moore_searcher bmsearch{ sub.begin(),sub.end() };
for (auto begend = bmsearch(text.begin(), text.end()); begend.first != text.end();
begend = bmsearch(begend.second, text.end()))
{
cout << "found " << sub << " at index "
<< begend.first - text.begin() << " - "
<< begend.second - text.begin() << endl;
}
原本这种算法使作为字符串搜索器开发的。然而,C++17将它们改进为泛型算法,因此,你可以在一个容器或者范围内搜索子序列。
int main()
{
vector<int> coll;
deque<int> sub{ 0,8,15,... };
auto pos = search(coll.begin(), coll.end(), boyer_moore_searcher{ sub.begin(),sub.end() });
// 或者下面:
boyer_moore_searcher bm{ sub.begin(),sub.end() };
auto [beg, end] = bm(coll.begin(), coll.end());
if (beg != coll.end())
cout << "found subsequence at " << beg - coll.begin() << endl;
}
想要能够使用该算法,元素必须能用在哈希表中,也就是必须提供默认的哈希函数和==
比较符。否则,使用搜索器谓词。
当使用搜索器时,你可以使用谓词。出于两个原因:
例如,这里用一个大小写不敏感的搜索器来搜索子串:
boyer_moore_searcher bmic { sub.begin(), sub.end(),
[](char c)
{
return hash<char>{}(toupper(c));
},
[](char c1,char c2)
{
return toupper(c1) == toupper(c2);
}};
C++新增加了三个辅助函数:size()
、empty()
、data()
。都定义在
头文件中。
该函数允许我们查任何范围的大小,前提是由迭代器接口或者是原生数组。
template<typename T>
void printLast5(const T& coll)
{
auto size{ size(coll) };
cout << size << " elems: ";
auto pos{ begin(coll) };
// 将迭代器递增到倒数第五个元素处
if (size > 5)
{
advance(pos, size - 5);
cout << "...";
}
// 打印剩下的元素
for (; pos != end(coll); ++pos)
{
cout << *pos << " ";
}
cout << endl;
}
类似size()
,empty()
可以检查容器、原生数组、initializer_list
是否为空。
data()
函数允许我们访问集合的原始数据。
新的辅助函数as_const()
可以在不使用static_cast<>
或者add_const_t<>
类型特征的情况下把值转换为响应的const类型。
vector<string> coll;
foo(coll); // 调用非常量版本
foo(as_const(coll)); // 调用常量版本
例如:
int main()
{
vector<int> coll{ 8,5,7,42 };
auto printColl = [&coll = as_const(coll)]
{
cout << "coll: ";
for (int elem : coll)
{
cout << elem << " ";
}
cout << endl;
};
}
C++17提供了一个新的工具函数clamp()
,找出三个值大小居中的那个。
int main()
{
for (int i : {-7, 0, 8, 15})
{
cout << clamp(i, 5, 13) << endl;
}
}
C++17提供了sample()
算法来从一个给定的范围内提取一个随机的子集。有时候也被称为水塘抽样或者选择抽样。
#include
int main()
{
vector<string> coll;
for (int i = 0; i < 10000; ++i)
{
coll.push_back("value" + to_string(i));
}
sample(coll.begin(), coll.end(),
ostream_iterator<string>(cout, "\n"),
10,
default_random_engine{});
}
该函数有以下保证和约束:
int main()
{
vector<string> coll;
for (int i = 0; i < 10000; ++i)
{
coll.push_back("value" + to_string(i));
}
// 用一个随机数种子初始化Mersenne Twister引擎:
random_device rd;
mt19937 eng{ rd() };
vector<string> subset;
subset.resize(100);
auto end = sample(coll.begin(), coll.end(),
subset.begin(),
10,
eng);
for_each(subset.begin(), end, [](const auto& s)
{
cout << "random elem:" << s << endl;
});
}
C++17引入把某个节点从关联或无需容器中移除或移入的功能。
int main()
{
map<int, string> m{ {1,"mango"},{2,"papaya"},{3,"guava"} };
auto nh = m.extract(2); // nh的类型为decltype(m)::node_type
nh.key() = 4;
m.insert(move(nh));
for (const auto& [key, value] : m)
{
cout << key << " : " << value << endl;
}
}
这段代码是把key为2的的元素结点移除了容器,然后修改了key
,最后有移进了容器。C++17之前,如果想修改一个key
,必须删除旧结点然后再插入一个value相同的新节点。如果我们使用节点句柄,将不会发送内存分配。
你也可以使用节点句柄把一个元素从一个容器move到另一个容器。
template<typename T1,typename T2>
void print(const T1& coll1, const T2& coll2)
{
cout << "values:\n";
for (const auto& [key, value] : coll1)
{
cout << " [" << key << " : " << value << "]";
}
cout << endl;
for (const auto& [key, value] : coll2)
{
cout << " [" << key << " : " << value << "]";
}
cout << endl;
}
int main()
{
multimap<double, string> src{ {1.1,"one"},{2.2,"two"},{3.3,"three"} };
map<double, string> dst{ {3.3,"old data"} };
print(src, dst);
dst.insert(src.extract(src.find(1.1)));
dst.insert(src.extract(2.2));
print(src, dst);
}
以节点句柄API为基础,现在所有的关联和无需容器都提供了成员函数merge()
,可以把一个容器中的所有元素合并到另一个容器中。如果源容器中的某个元素和目标容器的元素的key值相同,那么它仍然保留在源容器中。
template<typename T1,typename T2>
void print(const T1& coll1, const T2& coll2)
{
cout << "values:\n";
for (const auto& [key, value] : coll1)
{
cout << " [" << key << " : " << value << "]";
}
cout << endl;
for (const auto& [key, value] : coll2)
{
cout << " [" << key << " : " << value << "]";
}
cout << endl;
}
int main()
{
multimap<double, string> src{ {1.1,"one"},{2.2,"two"},{3.3,"three"} };
map<double, string> dst{ {3.3,"old data"} };
print(src, dst);
// 把src的所有元素和并到dst中
dst.merge(src);
print(src, dst);
}
对于顺序容器vector<>
、deque<>
、list<>
、forward_list<>
,还有容器适配器stack<>
和queue<>
。他们的emplace函数现在返回新插入的对象的引用。例如:
foo(myVector.emplace_back(...));
// 等同于
myVector.emplace_back(...);
foo(myVector.back());
try_emplace()
用移动语义构造了一个新的值。insert_or_assign()
稍微改进了插入/更新元素的方法。考虑如下代码:
map<int,string> m;
m[42] = "hello";
string s{"world"};
m.emplace(42,move(s)); // 可能移动,但42已经存在了可能不会移动
这样调用之后s是否保持原本的值是未定义的。同样使用insert()
之后也可能是这样。
m.insert({42,move(s))});
新的成员函数try_emplace()
保证在没有已经存在元素时才会move走传入的值:
m.try_emplace(42,move(s)); // 如果插入失败不会move
另外,新的成员函数insert_or_assign()
保证把值移动道一个新的元素或者已经存在的元素中。
m.insert_or_assgin(42,move(s)); // 总是会move
自从C++17起,vector
、list
、forward_list
被要求支持不完全类型。
你现在可以定义一个类型,内部递归的包含一个自身类型的容器。例如:
struct Node
{
string value;
vector<Node> children;
};
data()
吧底层的字符序列当做原生C字符串来访问。auto cstr = mystring.data();
cstr[6] = 'w'; // OK
char *cstr = mystring.data(); // OK
C++11引入了一个简单的lock_guard
来实现RAII风格的互斥量上锁:
不幸的是,没有标准化的可变参数模板可以用来在一条语句中同时锁住多个互斥量。
scoped_lock
解决了这个问题。它允许我们同时锁住一个或多个互斥量,互斥量的类型可以不同。
int main()
{
vector<string> alllssues;
mutex alllssuesMx;
vector<string> openlssues;
timed_mutex openlssuesMx;
// 同时锁住两个issue列表
{
scoped_lock lg(alllssuesMx, openlssuesMx);
// .. 操作
}
}
类似于一下C++11代码:
// 同时锁住两个issue列表
{
lock(alllssuesMx, openlssuesMx); // 避免死锁的方式上锁
lock_guard<mutex> lg1(alllssuesMx, adopt_lock);
lock_guard<timed_mutex> lg2(openlssuesMx, adopt_lock);
// .. 操作
}
因此,当传入的互斥量超过一个时,scoped_lock
的构造函数会使用可变参数的快捷函数lock()
,这个函数会保证不会导致死锁。
注意你也可以传递已经被锁住的互斥量
// 同时锁住两个issue列表
{
lock(alllssuesMx, openlssuesMx); // 避免死锁的方式上锁
scoped_lock lg{adpot_lock, alllssuesMx, openlssuesMx};
// .. 操作
}
C++14添加了一个shared_timed_mutex
来支持读/写锁,它支持多个线程同时读一个值,偶尔会有一个线程更改值。现在引入了shared_mutex
,定义在头文件
。
支持以下操作:
假设你有一个共享的vector,被多个线程读取,偶尔会被修改:
vector<double> v; // 共享的资源
shared_mutex vMutex;
int main()
{
if (shared_lock sl(vMutex); v.size() > 0)
{
// 共享读权限
}
{
scoped_lock sl(vMutex);
// 独占写权限
}
}
你现在可以使用一个C++库的特性来检查一个特定的原子类型是否总是可以在无锁的情况下使用。例如:
if constexpr(atomic<int>::is_always_lock_free)
{
// ...
}
else
{
// ...
}
如果一个原子类型的is_always_lock_free
返回true,那么该类型的对象is_lock_free()
成员也一定会返回true:
if constexpr(atomic<T>::is_always_lock_free)
{
assert(atomicT>{}.is_lock_free()); // 绝不会失败
}
在C++17之前,只能使用相应的宏来判断,例如当ATOMIC_INT_LOCK_FREE
返回2的时候相当于is_always_lock_free
返回true。
程序有时候需要处理cache行大小的能力有以下两种:
C++标准库在头文件中引入了两个内联变量:
namespace std
{
inline constexpr size_t hardware_destructive_interference_size; // 可能被不同线程并发访问的两个对象之间的最小偏移量
inline constexpr size_t hardware_constructive_interference_size; // 两个想被放在同一个L1缓存行的对象合起来的最大大小
}
如果你想要在不同的线程里访问两个不同(原子)的对象:
struct Data
{
alignas(std::hardware_destructive_interference_size) int valueForThreadA;
alignas(std::hardware_destructive_interference_size) int valueForThreadB;
};
如果你想要在同一个线程里访问不同的(原子)对象
struct Data {
int valueForThraedA;
int otherValueForTheThreadA;
};
C++中存在一个模式RAII,这是一种安全处理那些你必须要释放或清理的资源的方式。在构造一个对象的时候将所需要的资源分配给它,在析构中将分配的资源进行释放。
然而,有的时候资源的“释放操作”依赖于我们到底是正常执行离开了作用域还是因为异常离开了作用域。一个例子是事务性资源,如果我们正常执行离开了作用域,我们可能想进行提交操作,而当因为异常离开作用域时想进行回滚操作。为了达到这个目的,C++11引入了std::uncaught_expection()
,用法如下:
class Request
{
public:
~Request()
{
if(std::uncaught_expection()) rollback(); // 如果没有异常情况会调用rollback()
else commit();
}
};
然而,在如下场景中使用这个API不能正常工作,当我们正在处理异常时,如果创建了新的Request对象,那么即使在使用它的期间没有异常抛出,它的析构函数总是会调用rollback()
。
try
{
...
}
catch(...)
{
Request r2{...};
}
C++17解决了这个问题,对Request类进行以下修改:
class Request
{
private:
int initialUncaught{std::uncaught_expections()};
public:
~Request()
{
if(std::uncaught_expections() > initialUncaught) rollback(); // 如果没有异常情况会调用rollback()
else commit();
}
};
旧的不带s的API自C++17起被废弃,不应该再被使用。
C++17添加了一些共享指针的改进,且成员函数unique()已经被废弃了。
自从C++17起可以对其使用特定的deleter函数,C++11unique_ptr
已经可以做到了,例如:
std::shared_ptr<std::string> p{new std::string[10], [](std::string* p){
delete[] p;
}};
当实例化是数组时,不再使用operator*,而是使用operator[]:
std::shared_ptr<std::string> ps{new std::string};
*ps = "hello"; // OK
ps[0] = "hello"; // ERROR
std::shared_ptr<std::string[]> parr{new std::string[10]};
*parr = "hello"; // ERROR
parr[0] = "hello"; // OK
除了static_pointer_cast
、dynamic_pointer_cast
、const_pointer_cast
之外,可以调用reinterpret_pointer_cast
。
为了支持在泛型代码中使用弱指针,共享指针提供了一个新的成员weak_type
。例如:
template<typename T>
void ovserve(T sp)
{
typename T::weak_type wp{sp};
}
C++17起,有一个额外的辅助函数可以返回一个指向对象的弱指针:
Person *pp = new Person{};
std::shared_ptr<Person> sp{pp};
std::weak_ptr<Person> wp{pp->weak_from_this()}; // wp分享了sp拥有的所有权
C++17引入了以下的数学函数。
在头文件
中:
在头文件
中:
名称 | 含义 |
---|---|
assoc_laguerre() | 关联Laguerre多项式 |
assoc_legendre() | 关联Legendre函数 |
beta() | beta函数 |
comp_ellint_1() | 第一类完整椭圆积分 |
comp_ellint_2() | 第二类完整椭圆积分 |
comp_elint_3() | 第三类完整椭圆积分 |
cyl_bessel_i() | 规则圆柱贝塞尔函数 |
cyl_bessel_j() | 第一类圆柱贝塞尔函数 |
cyl_bessel_k() | 不规则圆柱贝塞尔函数变体 |
cyl_neumann() | 圆柱诺依曼函数 |
ellint_1() | 第一类不完整椭圆积分 |
elint_2() | 第二类不完整椭圆积分 |
elint_3() | 第三类不完整椭圆积分 |
expint() | 指数积分 |
hermite() | Hermite多项式 |
laguerre() | Laguerre多项式 |
legendre() | Legendre多项式 |
riemann_zeta() | 黎曼zeta函数 |
sph_bessel() | 第一类球形贝塞尔函数 |
sph_legendre() | 关联球形Legendre函数 |
sph_neumann() | 球形诺依曼函数 |
对于时间段和时间点添加了新的舍入函数:
最重要的修正有:
最重要的修正有: