《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 
#include 
#include 
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);
}

《C++标准库》学习笔记 — 通用工具_第1张图片
我们在调用 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 中预定了许多单位,这些单位让我们可以方便地指出纳秒等单位:
《C++标准库》学习笔记 — 通用工具_第2张图片

你可能感兴趣的:(读书笔记,#,《C++标准库》,c++,内存管理,类型处理,分数运算,标准库)