《C++标准库》学习笔记 — 通用工具

news/2024/7/20 16:07:34 标签: c++, 内存管理, 类型处理, 分数运算, 标准库

《C++标准库》学习笔记 — 通用工具

  • 一、 智能指针
    • 1、误用shared_ptr
    • 2、make_shared 和 allocate_shared
    • 3、shared_ptr 转型
  • 二、Type Trait 和 Type Utility
    • 1、对重载的弹性支持
    • 2、处理共通类型
    • 3、类型关系 trait 与基本类型
    • 4、类型修饰符
  • 三、class ratio 的编译期分数运算

一、 智能指针

1、误用shared_ptr

我们必须确保同一个指针只被一组 shared_ptr 管理。这里我们列举一个间接破坏此要求的例子:

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class Person
{
public:
	Person()
	{
		cout << "Person " << this << endl;
	}

	void setParentsAndTheirKids(shared_ptr<Person> father = nullptr, shared_ptr<Person> mother = nullptr)
	{
		this->father = father;
		this->mother = mother;

		if (father)
		{
			father->kids.push_back(shared_ptr<Person>(this));
		}

		if (mother)
		{
			mother->kids.push_back(shared_ptr<Person>(this));
		}
	}

	~Person()
	{
		cout << "~Person " << this << endl;
	}
private:
	vector<shared_ptr<Person>> kids;
	weak_ptr<Person> father;
	weak_ptr<Person> mother;
};

int main()
{
	shared_ptr<Person> father(new Person);
	shared_ptr<Person> mother(new Person);
	shared_ptr<Person> kid(new Person);
	kid->setParentsAndTheirKids(father, mother);
}

在这里插入图片描述
我们在调用 setParentsAndTheirKids 时,通过 shared_ptr(this) 向他们的孩子数组中添加了元素,这导致孩子的指针被三个 shared_ptr 所管理,而且它们互相并不知道。这最终导致了孩子指针的多次释放。

这个问题有两种解决方案:第一种是将储存孩子对象的智能指针作为参数传递给函数:

void setParentsAndTheirKids(shared_ptr<Person> father = nullptr, shared_ptr<Person> mother = nullptr, shared_ptr<Person> kid = nullptr)
	{
		this->father = father;
		this->mother = mother;

		if (father)
		{
			father->kids.push_back(kid);
		}

		if (mother)
		{
			mother->kids.push_back(kid);
		}
	}

第二种解决方案是继承 enable_shared_from_this 模板类,借助 shared_from_this 函数我们可以为当前类的每一个实例获取唯一的智能指针。修改后的代码如下:

class Person : public std::enable_shared_from_this<Person>
{
public:
	...
	void setParentsAndTheirKids(shared_ptr<Person> father = nullptr, shared_ptr<Person> mother = nullptr)
	{
		this->father = father;
		this->mother = mother;

		if (father)
		{
			father->kids.push_back(shared_from_this());
		}

		if (mother)
		{
			mother->kids.push_back(shared_from_this());
		}
	}
	...
};

注意到其模板参数为当前类,开始我推测其底层为每个类保存了一个静态容器对象用以保存其实例的智能指针。后来查看底层实现发现并不是这样。它实际上是为每个对象保存了一个弱引用:

template <class _Ty>
class enable_shared_from_this { // provide member functions that create shared_ptr to this
public:
	...
    _NODISCARD shared_ptr<_Ty> shared_from_this() {
        return shared_ptr<_Ty>(_Wptr);
    }

    _NODISCARD shared_ptr<const _Ty> shared_from_this() const {
        return shared_ptr<const _Ty>(_Wptr);
    }
	...
private:
	...
    mutable weak_ptr<_Ty> _Wptr;
};

那么构造对象的时候显然 weak_ptr 使用空值进行初始化,那么它是什么时候被赋值的呢?是在当前对象的 shared_ptr 被构造的时候:

template <class _Ty>
class shared_ptr : public _Ptr_base<_Ty> {
	explicit shared_ptr(_Ux* _Px) { // construct shared_ptr object that owns _Px
        if constexpr (is_array_v<_Ty>) {
            _Setpd(_Px, default_delete<_Ux[]>{});
        } else {
            _Temporary_owner<_Ux> _Owner(_Px);
            _Set_ptr_rep_and_enable_shared(_Owner._Ptr, new _Ref_count<_Ux>(_Owner._Ptr));
            _Owner._Ptr = nullptr;
        }
    }
}

_Set_ptr_rep_and_enable_shared 函数中会将继承于 enable_shared_from_this 对象内部的弱引用对象与当前构造的 shared_ptr 对象关联。仔细想想就会发现这样做相比使用静态容器对象的好处,那就是实现了懒加载。只有需要共享的对象,其弱引用对象才会被赋值。

注意 enable_shared_from_this 不能在构造函数中调用,因为构造时,共享 shared_ptr 对象一定还没有被创建。

2、make_shared 和 allocate_shared

make_sharedallocate_shared 都是标准库提供的用于构建智能指针的函数。它们是被用来优化被共享对象及其相应值控制区块的创建。C++标准手册中也提到,对于下列代码:

std::shared_ptr<T>(new T(args...))

实际上会发生两次内存分配,一次针对T,另一次针对控制块对象。而 make_sharedallocate_shared 仅发生一次内存分配,后者还允许我们自己定义的空间配置器。

3、shared_ptr 转型

转型操作符可以将一个指针转换为不同类型。其语义与其所对应的操作符相同,得到的是不同类型的另一个 shared_ptr。这里我们不能使用普通的转型操作符。因为那会导致不确定的行为:

shared_ptr<void> sp(new Person);
shared_ptr<Person> other(static_cast<Person*>(sp.get()));

这个问题与1中的问题类似,有两个智能指针对象管理同一块内存。我们可以使用 static_pointer_cast 实现智能指针对象之间的相互转换。

shared_ptr<Person> other(static_pointer_cast<Person>(sp));

二、Type Trait 和 Type Utility

C++98中就已经实现了针对空间配置器、迭代器的 type_traits。C++11中提供了多种 type trait 用于处理 type 属性。它是个模板,可以在编译器根据一个或多个 template 实参产生出相应的值或类型。

1、对重载的弹性支持

type trait 是泛型代码的基石,其功能之一是对重载的弾性支持。

如果我们有一个函数 foo,对于整数类型和浮点数类型的实参,它该有不同的实现。通常做法是将它重载,以处理整型和浮点型:

// 整型
void foo(short);
void foo(unsigned short);
void foo(int);

// 浮点型
void foo(float);
void foo(double);
void foo(long double);

实现这些版本的原因在于防止类型不完全匹配时出现二义性的重载。然而,这么做的主要问题在于:每次我们增加新的类型都需要改变代码。type trait 可以帮助我们简化这样的重载,只需:

template<typename T>
void foo_impl(T val, true_type);

template<typename T>
void foo_impl(T val, false_type);

template<typename T>
void foo(T val)
{
	foo_impl(val, is_integral<T>());
}

代码变得很简化。

当然,我们可以自己通过使用模板重写 foo 函数以实现相同的效果。但是,一方面,标准库中已经提供了这样的功能,为何不使用呢?另一方面,每当我们有一个函数需要此功能时,我们都需要为每个类型实现模板函数的特化。这样过于冗余了。

2、处理共通类型

type trait 还可以帮助我们处理共通类型。什么是共通类型呢?那是一个可以用来对付两个不同类型的值得类型,前提是的确存在这么一个类型。例如,不同类型的两个值的总和或最小值,就该使用这个所谓的共通类型。否则,如果我想实现一个函数,判断不同类型的两值中的最小值,其返回类型该是什么呢?我们只需使用 std::common_type<> 就饿可以解决问题:

template<typename T1, typename T2>
typename common_type<T1, T2>::type min(const T1& x, const T2& y);

事实上,common_type 支持任意多个参数,底层使用函数包展开方式实现。

3、类型关系 trait 与基本类型

类型关系 trait 中提供了检查类型关系的模板结构体,如 is_assignableis_constructible。我们需要注意,一个如 int 的类型究竟表现出 lValue 还是 rValue。由于我们不能这样进行赋值:

42 = 77

因此以非引用形式作为 is_assignable 第一类型永远获得 falst_type

cout << boolalpha;
cout << is_assignable<int, int>::value << endl;
cout << is_assignable<int&, int>::value << endl;
cout << is_assignable<int&&, int>::value << endl;

在这里插入图片描述

4、类型修饰符

这一类 trait 可以为类型添加一个属性。需要注意,使用 add_lvalue_reference 会将右值引用变为左值引用类型;而 add_ralue_reference 不会改变左值引用的引用类型:

using ILR = const int&;
using IRR = const int&&;

cout << boolalpha;
cout << is_same_v<ILR, add_rvalue_reference<ILR>::type> << endl;
cout << is_same_v<IRR, add_rvalue_reference<ILR>::type> << endl;
cout << is_same_v<IRR, add_lvalue_reference<IRR>::type> << endl;
cout << is_same_v<ILR, add_lvalue_reference<IRR>::type> << endl;

在这里插入图片描述

三、class ratio 的编译期分数运算

ratio 类定义在 中,其好处在于:
(1)可以将分数运算规约成最简式
(2)进行值检查(如除0操作是无法通过编译的)

using R1 = ratio<24, 9>;
cout << R1::num << "/" << R1::den << endl;

using R2 = ratio<32, 12>;
cout << boolalpha;
cout << ratio_equal_v<R1, R2> << endl;

// zero denominator
// using R3 = ratio<17, 0>;
// R3::type r1; 

在这里插入图片描述
除此之外,ratio 中预定了许多单位,这些单位让我们可以方便地指出纳秒等单位:
在这里插入图片描述


http://www.niftyadmin.cn/n/1340351.html

相关文章

《数据结构、算法与应用 —— C++语言描述》学习笔记 — 优先级队列 — 应用

《数据结构、算法与应用 —— C语言描述》学习笔记 — 优先级队列 — 应用一、堆排序二、机器调度1、使用堆建立 LPT2、实现&#xff08;1&#xff09;修改heap&#xff08;2&#xff09;job&#xff08;3&#xff09;调度类头文件&#xff08;4&#xff09;机器类&#xff08;…

《数据结构、算法与应用 —— C++语言描述》学习笔记 — 优先级队列 — 应用 — 霍夫曼编码

《数据结构、算法与应用 —— C语言描述》学习笔记 — 优先级队列 — 应用 — 霍夫曼编码一、原理及构造过程1、原理2、扩展二叉树与霍夫曼编码3、构造步骤二、实现1、类图2、公共常量和结构体3、基类定义4、霍夫曼树的构建5、生成映射关系6、压缩类定义7、压缩类实现8、解压缩…

《C++标准库》学习笔记 — 通用工具 — Clock 和 Timer

《C标准库》学习笔记 — 通用工具 — Clock 和 Timer一、Chrono 程序库概观二、duration1、duration的算术运算2、Duration 的其他操作3、duration_cast4、rep 和 period三、clock 和 timepoint1、Clock2、Timepoint一、Chrono 程序库概观 Chrono 程序库的设计&#xff0c;是希…

《数据结构、算法与应用 —— C++语言描述》学习笔记 — 竞赛树

《数据结构、算法与应用 —— C语言描述》学习笔记 — 竞赛树一、赢者树二、二叉树的数组描述&#xff08;补充&#xff09;1、声明2、实现三、赢者树1、抽象数据类型2、赢者树的表示3、声明4、初始化5、重新组织比赛6、获取胜者一、赢者树 假定有 n 个选手参加一次网球比赛。…

《C++标准库》学习笔记 — STL — 容器与算法

《C标准库》学习笔记 — STL — 容器与算法一、reverse 迭代器二、Funtion Object1、pass by value 的函数对象2、获得结果的办法&#xff08;1&#xff09;通过引用传值方式传递函数对象&#xff08;2&#xff09;利用 for_each 算法的返回值三、Predicate 和 remove_if四、la…

《数据结构、算法与应用 —— C++语言描述》学习笔记 — 竞赛树 — 应用 — 箱子装载

《数据结构、算法与应用 —— C语言描述》学习笔记 — 竞赛树 — 应用 — 箱子装载一、问题描述二、近似算法三、赢者树和最先适配法四、最先适配法的实现1、问题修复2、find3、FF实现4、测试代码一、问题描述 在箱子装载问题中&#xff0c;箱子的数量不限&#xff0c;每个箱子…

集装箱学习(两):动手模拟AOP

简单的说&#xff0c;Spring是一个轻量级的控制反转(IOC&#xff09;和面向切面&#xff08;AOP)的容器框架。上文已经介绍模拟IoC实现&#xff0c;这篇文章来动手模拟AOP。 AOP简述 面向对象强调"一切皆是对象"&#xff0c;是对真实世界的模拟。然而面向对象也并不是…

《数据结构、算法与应用 —— C++语言描述》学习笔记 — 二叉搜索树

《数据结构、算法与应用 —— C语言描述》学习笔记 — 二叉搜索树一、二叉搜索树1、二叉搜索树特征2、索引二叉搜索树二、抽象数据类型三、二叉搜索树实现1、字典类接口修改2、接口3、查找接口4、删除&#xff08;1&#xff09;父节点扩展&#xff08;2&#xff09;后继节点&am…