C++ 实用特性分享
以下内容主要包含 C++11 及之后(当然也有 C++11 前未被广泛熟知)的实用特性、库等的理论和实践。由于这是分享会内容,为照顾面向人群,内容较为简略,对认真学习并使用 C++ 的老手可能意义不大。
内容部分摘录于 cppreference,大部分也取材于此。另参照 C++ 标准等资料。
另外第一部分关于 auto 的内容大部分与 Effective Modern C++ 开篇一致。然而在这部分动笔前并未参阅照搬此书(以前草草读过但大部分也都忘记了……),写完后才发现结构几乎一摸一样,大概所谓理解透彻了吧……(当然后续还是参阅了以丰富内容……)
1. auto 的几种用法
C++11 前用作自动存储期说明符,相关的关键字有 static, register(现已移除), thread_local 等。
C++11 后开始用作占位符类型,类型推导原则遵从模板实参推导规则。
- If the placeholder-type-specifier is of the form type-constraint opt
auto, the deduced typeT'replacingTis determined using the rules for template argument deduction. … [9.2.9.6.2 dcl.type.auto.deduct]
C++14 引入了 decltype(auto)。因为这里是遵循 decltype 推导的原则,所以可以推导引用类型。
- If the placeholder-type-specifier is of the form type-constraint opt
decltype(auto),Tshall be the placeholder alone. The type deduced forTis determined as described in 9.2.9.5, as thoughEhad been the operand of thedecltype. [9.2.9.6.2 dcl.type.auto.deduct]
示例代码如下。
#include <boost/type_index.hpp>
#include <iostream>
#include <type_traits>
#include <typeinfo>
template <typename T>
void f(T t)
{
// std::cout << typeid(t).name() << std::endl;
std::cout << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;
}
template <typename T>
void g(T& t)
{
// std::cout << typeid(t).name() << std::endl;
std::cout << boost::typeindex::type_id_with_cvr<T>().pretty_name() << std::endl;
}
int main()
{
double arr[20];
const int seven = 7;
f(arr); // Pd, double*
g(arr); // A20_d, double [20]
f(seven); // i, int
g(seven); // i, int const
f(7); // i, int
// g(7); //右值绑定到 non-const 引用,不合法
auto a1 = arr; // Pd, double*
auto& a2 = arr; // A20_d, double [20]
auto b1 = seven; // i, int
auto& b2 = seven; // i, int const&
decltype(auto) c1 = seven; // i, const int
decltype(auto) c2 = (seven); // i, const int&
// static_assert(std::is_same_v<int, decltype(b1)>);
// static_assert(std::is_same_v<const int&, decltype(b2)>);
// static_assert(std::is_same_v<const int, decltype(c1)>);
// static_assert(std::is_same_v<const int&, decltype(c2)>);
std::cout << boost::typeindex::type_id_with_cvr<decltype(b1)>().pretty_name() << std::endl;
std::cout << boost::typeindex::type_id_with_cvr<decltype(b2)>().pretty_name() << std::endl;
std::cout << boost::typeindex::type_id_with_cvr<decltype(c1)>().pretty_name() << std::endl;
std::cout << boost::typeindex::type_id_with_cvr<decltype(c2)>().pretty_name() << std::endl;
}
typeid 用于查看类型,因为名称修饰,可以使用 c++filt -t 来查看原名称。标准 [7.6.1.8 expr.typeid] 章节如下。
- … If the type of the type-id is a reference to a possibly cv-qualified type, the result of the typeid expression refers to a std::type_info object representing the cv-unqualified referenced type. If the type of the type-id is a class type or a reference to a class type, the class shall be completely-defined.
即,typeid 不会展示 cv 限定和引用类型。因此,上述的输入未区分 int 和 const int&,但可根据 static_assert 配合 std::is_same 在编译期检查类型。或者,也可以选用 Boost 库的 Boost.TypeIndex,它根据模板实例化时在 __PRETTY_FUNTION__ 宏中得到原类型的名字并进行处理,最终可以输出可供阅读的、包含 cv 限定和引用类型的类型信息。
简单总结,由于在模板实参推导时,未指定引用和 cv 限定的形参类型会导致传入的实参类型退化(即类似 std::decay<T> 的行为,将数组类型退化为指针、将函数类型退化为指针或得到 std::remove_cv_t<std::remove_refernce_t<T>> 的类型。而 auto 的推导规则与模板实参推导一致,因此会出现 auto 无法直接识别引用类型的情况。
理论上,auto 和 decltype(auto) 可以出现在所有需要填写类型的位置作为占位符。以下未特殊说明的示例均为 C++11 起开始支持的写法。
auto x = func();auto func() -> int { return x; };auto func() { return x; };(C++14)void func(const auto& x);(C++14)template<auto I> struct X;(C++17, 非类型模板形参)const auto& [a, b] = std::make_tuple(1, 2);(C++17, 结构化绑定)
2. 关于 lambda 表达式和算法库
1. 从 std::find_if 说起
示例代码如下。
#include <iostream>
#include <vector>
struct Point {
int x = 0;
int y = 0;
};
Point find(const std::vector<Point>& v) {
for (const auto& point : v) {
if (point.x < 0 && point.y < 0)
return point;
}
return Point{};
}
int main()
{
std::vector<Point> v{
{0, -1},{1, 1},{-1, -1},{0, 0} };
Point p = ::find(v);
std::cout << p.x << " " << p.y << std::endl;
}
这段代码中基于范围的 for 循环在实现中等价于根据 begin() 和 end() 产生范围表达式。等价的代码如下(C++ Insights, use libc++)。
//...
Point find(const std::vector<Point, std::allocator<Point>>& v)
{
{
const std::vector<Point, std::allocator<Point>>& __range1 = v;
std::__wrap_iter<const Point*> __begin1 = __range1.begin();
std::__wrap_iter<const Point*> __end1 = __range1.end();
for (; std::operator!=(__begin1, __end1); __begin1.operator++()) {
const Point& point = __begin1.operator*();
if ((point.x < 0) && (point.y < 0)) {
return Point(point);
}
}
}
return Point{ {0}, {0} };
}
//...
按照此类查找方法需要额外构造对象,而若返回 const 引用也有着无法简单地表示未找到对应元素的情况。因此,可以改用指针或行为类似的迭代器。
std::vector<Point>::iterator find(std::vector<Point>::iterator begin, std::vector<Point>::iterator end) {
for (; begin != end; ++begin) {
if (begin->x < 0 && begin->y < 0)
return begin;
}
return begin;
}
如果将判断的内容改为传入函数对象(或函数指针),既有如下代码。
bool pred(const Point& point) {
return point.x < 0 && point.y < 0;
}
// 若改用函数指针则第三个参数为 bool (*f)(const Point&)
std::vector<Point>::iterator find(std::vector<Point>::iterator begin, std::vector<Point>::iterator end, std::function<bool(const Point&)> f) {
for (; begin != end; ++begin) {
// 若改用函数指针此处也可写为 (*f)(*begin)
// 解引用函数指针生成标识被指向函数的左值
if (f(*begin))
return begin;
}
return begin;
}
int main()
{
//std::vector<Point> v\{\{0, -1}, {1, 1}, {-1, -1}, {0, 0\}\};
// 由于存在函数到指针的隐式转换,若改用函数指针此处可以不改为 &pred
std::vector<Point>::iterator it = ::find(v.begin(), v.end(), pred);
std::cout << it->x << " " << it->y << std::endl;
}
至此,只需要将上述代码通过模板泛型即已经得到了标准库中 std::find_if 的实现。以下代码摘自 libc++ 12。
template <class _InputIterator, class _Predicate>
_LIBCPP_NODISCARD_EXT inline
_LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR_AFTER_CXX17
_InputIterator
find_if(_InputIterator __first, _InputIterator __last, _Predicate __pred)
{
for (; __first != __last; ++__first)
if (__pred(*__first))
break;
return __first;
}
几乎一致。算法库中类似的设施在 C++11 前就已经存在。而基于范围的 for 循环也有类似的替代,std::for_each,实现摘自 libc++ 12。
template <class _InputIterator, class _Function>
inline _LIBCPP_INLINE_VISIBILITY _LIBCPP_CONSTEXPR_AFTER_CXX17
_Function
for_each(_InputIterator __first, _InputIterator __last, _Function __f)
{
for (; __first != __last; ++__first)
__f(*__first);
return __f;
}
2. lambda 表达式和函数对象
算法库中许多传入迭代器和谓词的算法在 C++11 前已经存在。按照之前的示例。
bool pred(const Point& point) {
return point.x < 0 && point.y < 0;
}
std::vector<Point> v{ {0, -1}, {1, 1}, {-1, -1}, {0, 0} };
std::vector<Point>::iterator it = v.begin();
it = std::find_if(v.begin(), v.end(), pred); //函数隐式转化为指针
it = std::find_if(v.begin(), v.end(), &pred);
也可传入重载 operator() 的对象。
struct Pred {
bool operator()(const Point& point) {
return point.x < 0 && point.y < 0;
}
};
it = std::find_if(v.begin(), v.end(), Pred());
C++11 引入 std::bind 后也可以用其构造函数对象。
it = std::find_if(v.begin(), v.end(), std::bind(pred, std::placeholders::_1));
当然,最简洁的是传入 lambda 表达式。
it = std::find_if(v.begin(), v.end(), [](const Point& point) {
return point.x < 0 && point.y < 0;
});
等价的代码如下(C++ Insights, use libc++)。
class __lambda_14_72 {
public:
inline /*constexpr */ bool operator()(const Point& point) const
{
return (point.x < 0) && (point.y < 0);
}
using retType_14_72 = bool (*)(const Point&);
inline /*constexpr */ operator retType_14_72() const noexcept
{
return __invoke;
};
private:
static inline bool __invoke(const Point& point)
{
return (point.x < 0) && (point.y < 0);
}
public:
// /*constexpr */ __lambda_14_72() = default;
};
根据标准(7.5.5.2 [expr.prim.lambda.closure]),lambda 表达式的类型是闭包对象(closure object,类似函数对象)。具体要求和定义可参阅标准或 cppreference。
lambda 表达式有两种捕获方式:[=] 复制捕获和 [&] 引用捕获。此外可以对每个使用的变量单独捕获,以 [a] 方式捕获的为复制捕获,以 [&a] 方式捕获的为引用捕获,而对当前对象的 [this] 捕获为引用捕获,C++17 中引入了对当前对象的 [*this] 复制捕获。
int num = 1;
auto plus = [num](int x) { return x + num; };
auto plus_num = [&num](int x) { return num = x + num; };
plus(2); // =num = 1; return = 3, num = 1
plus_num(2); // &num = 1; return = 3, num = 3
plus(3); // =num = 1; return = 4, num = 3
plus_num(3); // &num = 3; return = 6, num = 6
其中 lambda 表达式的等价代码如下(C++ Insights, use libc++)。
int num = 1;
class __lambda_4_17 {
public:
inline /*constexpr */ int operator()(int x) const
{
return x + num;
}
private:
int num;
public:
__lambda_4_17(int& _num)
: num{_num}
{
}
};
__lambda_4_17 plus = __lambda_4_17{num};
class __lambda_5_21 {
public:
inline /*constexpr */ int operator()(int x) const
{
return num = (x + num);
}
private:
int& num;
public:
__lambda_5_21(int& _num)
: num{_num}
{
}
};
__lambda_5_21 plus_num = __lambda_5_21{num};
// ...
此外还有 [...] 的形参包展开捕获。
template<class... Args>
void f(Args... args)
{
auto print = [args...]() {
// C++17 折叠表达式
// 等价于 ...(std::operator<<(std::operator<<(std::cout, __args0), __args1)...
(std::cout << ... << args);
};
print();
}
f("Hello", ",", "world", "!", "\n");
其中等价代码如下(C++ Insights, use libc++)。
template <>
void f<const char*, const char*, const char*, const char*, const char*>(const char* __args0,
const char* __args1, const char* __args2, const char* __args3, const char* __args4)
{
class __lambda_6_18 {
public:
inline /*constexpr */ void operator()() const
{
std::operator<<(
std::operator<<(
std::operator<<(
std::operator<<(std::operator<<(std::cout, __args0), __args1), __args2),
__args3),
__args4);
}
private:
const char* __args0;
const char* __args1;
const char* __args2;
const char* __args3;
const char* __args4;
public:
__lambda_6_18(const char* ___args0, const char* ___args1, const char* ___args2,
const char* ___args3, const char* ___args4)
: __args0{___args0}
, __args1{___args1}
, __args2{___args2}
, __args3{___args3}
, __args4{___args4}
{
}
};
__lambda_6_18 print = __lambda_6_18{__args0, __args1, __args2, __args3, __args4};
print.operator()();
}
C++14 引入了捕获初始化器,可用于移动捕获、捕获 const 引用等。
int num = 1;
auto plus = [&n = std::as_const(num)](int x) {
return x + n;
};
3. 算法库常用设施
1. <algorithm>
-
std::for_each循环并对每个元素应用函数。
std::string s("Hello, world!"); std::for_each(s.begin(), s.end(), [](char& c) { c = std::toupper(c); }); -
std::all_of,std::any_of,std::none_of检查函数是否对范围内的所有元素返回
true检查函数是否对范围内的任一元素返回true检查函数是否对范围内的所有元素返回false。std::vector v(10, 0); std::iota(v.begin(), v.end(), 0); bool result = std::all_of(v.begin(), v.end(), [](const auto& i) { return i >= 0; }); -
std::find,std::find_if,std::find_if_not返回范围中满足等于传入值的首个元素 返回范围中满足对传入函数返回
true的首个元素 返回范围中满足对传入函数返回false的首个元素 -
std::transform应用给定的函数到范围并存储结果于另一范围。
std::string s("Hello, world!") std::string u; std::transform(s.begin(), s.end(), std::back_inserter(u), [](const char& c) { return std::toupper(c); }); -
std::copy,std::copy_if复制范围内所有元素并存储结果于另一范围。 复制范围内对传入函数返回
true的元素并存储结果于另一范围。std::string s("Hello, world!"); std::string u; std::copy_if(s.begin(), s.end(), std::back_inserter(u), [](const char& c) { return std::isalpha(c); }); -
std::remove,std::remove_if移除范围内等于传入值的所有元素,并返回范围新结尾的尾后迭代器。 移除范围内对传入函数返回
true的所有元素,并返回范围新结尾的尾后迭代器。std::string s("Hello, world!"); s.erase(std::remove_if(s.begin(), s.end(), [](const char& c) { return std::ispunct(c); }), s.end()); // 原范围内容: // 'H' 'e' 'l' 'l' 'o' ',' ' ' 'w' 'o' 'r' 'l' 'd' '!' // std::remove_if 后范围内容: // 'H' 'e' 'l' 'l' 'o' ' ' 'w' 'o' 'r' 'l' 'd' 'd' '!' // remove_if 返回的迭代器指向 ↑ -
std::count,std::count_if返回范围中满足等于传入值的元素数量 返回范围中满足对传入函数返回
true的元素数量std::string s("Hello, world!"); std::cout << std::count_if(s.begin(), s.end(), [](const char& c) { return std::isalpha(c); }); -
std::sample(C++17)范围内随机取样 N 个并存储结果于另一范围。
-
std::shuffle随机重排范围内元素。
-
std::sort按照特定比较结果对范围内元素不稳定排序(默认按照
operator<即std::less<>)。std::vector v(10, 0); std::iota(v.begin(), v.end(), 0); std::shuffle(v.begin(), v.end(), std::mt19937(std::random_device()())); std::sort(v.begin(), v.end(), std::greater()); std::vector<int> s; std::sample(v.begin(), v.end(), std::back_inserter(s), 5, std::mt19937{std::random_device{}()});
2. <random>
-
种子
std::randome_device -
随机数引擎
std::linear_congruential_engine线性同余std::mersenne_twister_engine梅森缠绕std::subtract_with_carry_engine延迟斐波那契- …
常用的预定义引擎为
std::mt19937,32位梅森缠绕器。 -
分布
std::uniform_int_distribution,uniform_real_distribution均匀分布std::bernoulli_distribution伯努利分布std::normal_distribution正态分布- …
示例代码如下。
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dist(0, 9);
std::map<int, int> map;
for (int i = 0; i < 1000000; ++i) {
map[dist(gen)]++;
}
/*
* 结果大致如下:
* 0 100552
* 1 100280
* 2 100019
* 3 99904
* 4 100029
* 5 100026
* 6 99674
* 7 99730
* 8 100235
* 9 99551
*/
3. <numeric>
-
std::iota以传入初始值递增填充范围。
-
std::accumulate,std::reduce- 遍历范围以传入初始值累计计算范围元素。
- 类似
std::transform,但可以按照传入执行策略执行。 (C++17)
示例如下。目前只有 GCC 和 libstdc++ 支持执行策略,根据系统库配置和默认工具链的不同可能会需要链接 Intel tbb 库。
std::vector v(10, 0);
std::iota(v.begin(), v.end(), 1);
std::reduce(std::execution::par, v.begin(), v.end(), 1, std::multiplies());
3. 通用工具库,<tuple> 和 <utility>
1. std::pair 和 std::tuple
std::pair 定义于 <utility>,是一对异构值的模板,成员为 first 和 second,使用 std::make_pair 构造。标准库中主要应用在 std::map、std::unordered_map 等容器中,也用在部分函数的返回值中,例如 std::map 的 try_emplace 等。
std::tuple 定义于 <tuple>,是多个异构值的模板,类似扩展的 std::pair,使用 std::get 获取元素,使用 std::make_tuple 构造。
std::tuple 相关的几个用法如下。
-
std::tie返回含左值引用的 std::tuple 对象。可用于解包
std::pair和std::tuple。auto t = std::make_tuple(1, 2, 3); int a, b, c; std::tie(a, b, c) = t; // a = 1, b = 2, c = 3许多语言用
_表示可忽略的值,包括 Python、Go 等。C++ 中在解包tuple时使用std::ignore。std::set<int> s; // std::set 的 insert 返回 std::pair<iterator,bool> bool result; std::tie(std::ignore, result) = s.insert(1);std::tie根据引用构建std::tuple,而std::tuple默认根据字典序比较大小,因此也可用于自定义结构体比较。struct Rank { uint64_t id; uint32_t score; time_t timestamp; bool operator>(const Rank& rhs) { return std::tie(score, rhs.timestamp, id) > std::tie(rhs.score, timestamp, rhs.id); } }; std::vector<Rank> v{ {6, 300, 1618381861}, {8, 400, 1618554644}, {10, 300, 1618468154}, {12, 500, 1618468154} }; std::sort(v.begin(), v.end(), std::greater<>()); /* * 结果如下: * 12 500 1618468154 * 8 400 1618554644 * 6 300 1618381861 * 10 300 1618468154 */ -
结构化绑定
结构化绑定为解包
std::tuple提供了比std::tie更方便的方法,其应用范围也不仅限于std::tuple。可以实现类似 Go 中result, err := function()写法。类似于依次使用auto推导变量类型并引入当前作用域。auto t = std::make_tuple(1, 2, 3); { // 复制 t 到新的 tuple,再 auto a = std::get<0>(tuple) ... auto [a, b, c] = t; // a = 1, b = 2, c = 3 } { // 创建 t 的引用 tuple,再 auto& a = std::get<0>(tuple) ... auto& [a, b, c] = t; // a = 1, b = 2, c = 3 }此外还可以用于绑定数组和数据成员,但结构化绑定不支持嵌套,也不支持
std::ignore。struct Point { int x; int y; }; std::tuple t = std::make_tuple(Point{0, 0}, Point{1, 1}, Point{-1, -1}); const auto& [p1, p2, p3] = t; // p1 = {0, 0}, p2 = {1, 1}, p3 = {-1, -1} const auto& [x1, x2] = p1; // x1 = 0, x2 = 0结构化绑定除了简化写法外,也能实现一些很难以用
std::tie实现的功能。int x = 0, y = 0, z = 0; auto t = std::make_tuple(std::ref(x), std::ref(y), std::ref(z)); auto& [a, b, c] = t; a = 1, b = 2, c = 3; // x = 1, y = 2, z = 3;上述代码的等价代码如下(C++ Insights, use libc++)。
int x = 0; int y = 0; int z = 0; std::tuple<int&, int&, int&> t = std::make_tuple(std::ref(x), std::ref(y), std::ref(z)); std::tuple<int&, int&, int&>& __t10 = t; int& a = std::get<0UL>(__t10); int& b = std::get<1UL>(__t10); int& c = std::get<2UL>(__t10); ((a = 1), (b = 2)), (c = 3);
2. 通用工具库的新增内容
-
std::optional(C++17)std::optional用于管理一个可能存在的对象及其生存期。其管理的对象可能在std::optional外构造或析构,类似指针,但与指针不同的是std::optional为值语义,其内部包含的值不会发生动态内存分配。虽然
std::optional为值语义,但其接口和使用上很多地方和指针有相似之处。例如,若其不含值,或初始化为空时,内部包含std::nullopt对象,类似nullptr。 此外它也有operator*()和operator->(),若内部不含值则出现未定义行为,也与指针类似。检查其内部是否含值可以使用has_value(),或者类似指针直接隐式转换为bool。除了使用类似指针的解引用运算外,也可使用
value()访问内部值,或者使用value_or()当值存在时获取内部值,当值不存在时获取传入的另一个值。reset()可以用于销毁当前内部值。std::optional<std::pair<int, int>> op = std::nullopt; if (!op) { op = std::make_pair(0, 1); } std::cout << op->first << " " << op->second << std::endl; // 0 1 op.reset(); std::cout << op.value_or(std::make_pair(2, 3)).first << std::endl; // 2, op.has_value() = falsestd::optional在类型理论中属于和类型,更适合用于表示可能失败的值。但由于此特性在 C++17 才引入,而 C++ 之前更多使用指针、特殊返回值或类似std::set的insert返回std::pair<iterator, bool>等方法进行这种判断,因此此特性使用并不太广泛。 -
std::variant,std::any(C++17)略。