(Effective Modern C++) – 第四章

智能指针:

Item 18: Use std::unique_ptr for exclusive-ownership resource management

std::unique_ptr<T>用于函数返回的指针这样由很多的好处:

#include <iostream>
#include <iostream>
#include <memory>
class widget {
public:
};
enum class InvestmentType {
	Stock = 0,
	Bond
};
class Investment {
private:
public:
	virtual ~Investment() {}
	virtual InvestmentType getType() = 0;
};
class Stock :public Investment {
public:
	virtual InvestmentType getType() {
		return InvestmentType::Stock;
	}
	Stock(widget, double) { std::cout << "Stock::Stock(widget,double)" << std::endl; }
	Stock() { std::cout << "Stock::Stock()" << std::endl; }
};
class Bond :public Investment {
public:
	virtual InvestmentType getType() {
		return InvestmentType::Bond;
	}
	Bond(widget&, double) { std::cout << "Bond::Bond(int,double)" << std::endl; }
	Bond(widget&&, double) { std::cout << "Bond::Bond(widget&&,double)" << std::endl; }
	Bond() { std::cout << "Bond::Bond()" << std::endl; }
};

int makeInt() {
	return 23;
}
double makeDouble() {
	return 2.36;
}
void log() {
	std::cout << "DISPOSE!" << std::endl;
}
template<typename... Args>
auto makeInvestment(InvestmentType type,Args&&... params)
{
	auto deleter = [](Investment *ins) {
		log();//call process program
		delete ins;
	};
	std::unique_ptr<Investment, decltype(deleter)> pInv(nullptr, deleter);
	if (type == InvestmentType::Bond)
		pInv.reset(new Bond(std::forward<Args>(params)...));
	else if(type== InvestmentType::Stock)
		pInv.reset(new Stock(std::forward<Args>(params)...));
	return pInv;
}
int main()
{
	{
		widget wid;
		auto sto = makeInvestment(InvestmentType::Stock, widget{}, 2.3);
		auto bond = makeInvestment(InvestmentType::Bond);
		auto bond1 = makeInvestment(InvestmentType::Bond, widget{}, makeDouble());
		auto bond2 = makeInvestment(InvestmentType::Bond, wid, makeDouble());
	}
	return 0;
}
  • 函数返回的指针会有很多的用途,例如unique_ptr可以构造为shared_ptr或者有其他的用途(除了exit()调用或者其他的强制退出的情况,不会有内存泄漏)
  • 符合RAII的规则

unique_ptr可以传递自定义的删除器以代替默认的delete或者deleted[]。unique_ptr不允许从指针类型直接地隐式构造为对象。

删除器:

  1. 函数指针:
template<typename... Ts>                 // return type has
std::unique_ptr<Investment,              // size of Investment*
                void (*)(Investment*)>   // plus at least size
makeInvestment(Ts&&... params);          // of function pointer!

使用函数指针作为删除器,只会占用一个指针的空间。如果使用函数对象作为删除器,则占用的大小也取决于对象包含的对象大小。没有捕获对象的lambda表达式不会占用其他的空间。这意味着当自定义删除器即可用函数实现又可用不捕获变量的lambda表达式不会增加unique_ptr的大小。

  • std::unique_ptr<T[]>是指针类型的数组,std::array,std::vetcor,std::string实际上更好的数据结构。我能想到的std::unique_ptr数组唯一有意义场景就是,当你使用C-like风格的API,并且这API返回一个指像数组的指针,数组是从堆分配的,你需要对这个数组负责。
Things to Remember
• std::unique_ptr is a small, fast, move-only smart pointer for managing
resources with exclusive-ownership semantics.
• By default, resource destruction takes place via delete, but custom deleters
can be specified. Stateful deleters and function pointers as deleters increase the
size of std::unique_ptr objects.
• Converting a std::unique_ptr to a std::shared_ptr is easy.

Item 19: Use std::shared_ptr for shared-ownership resource management.

  • 一般情况下,shared_ptr的大小是原始指针的两倍(包含另一个指针指向控制块)。
  • 引用计数是动态分配的,同时用于计数的类型是原子类型。

shared_ptr的删除器与unique_ptr不同,他的删除器不是类型的一部分。所以删除器的大小并不会改变实际的对象的大小。Why?

shared_ptr保存了一个指向控制块(control block)的指针,而这个控制块主要保存:

使用std::make_shared始终创建一个控制块。而从原始指针以及unique_ptr(unique_ptr不使用控制块)构造的shared_ptr对象将会创建一个新的控制块。正是这个原因,如果从同一个原始指针构造两个shared_ptr,这两个shared_ptr创建的控制块是不同的(控制块的引用计数为1),所以他们在退出作用域的时候就会分别销毁分配的对象。

例如:

class wid {
public:
	void process() {
		processed.emplace_back(this);
	}
private:
	std::vector<std::shared_ptr<wid>> processed;
};
……
wid d;
d.process();
//*this对象就会被重复的释放

决解方案是使用std::enable_shared_from_this作为基类来创建一个控制块相同的另一个智能指针:

class wid;
std::vector<std::shared_ptr<wid>> processed;
class wid :public std::enable_shared_from_this<wid> {
public:
	//如果不使用工厂函数,shared_from_this仍然会因为创建了两个不同的指向同一个对象的智能指针造成的重复释放
	std::shared_ptr<wid> getPtr() {
		return shared_from_this();
		//return std::shared_ptr<wid>(&*this);//将会导致重复释放!
	}
private:
	

};

……
	auto widp = std::make_shared<wid>();
	auto th = widp->getPtr();//另一个指针

使用std::enable_shared_from_this的注意事项:

  • 不能在对象的构造函数中使用shared_from_this()函数。
  • 先需要调用enable_shared_from_this类的构造函数,接着调用对象的构造函数,最后需要调用shared_ptr类的构造函数初始化enable_shared_from_this的成员变量weak_this_。然后才能使用shared_from_this()函数。
  • 程序应该统一使用智能指针或者原始指针

如果不会或者不确定有额外的程序使用动态分配的对象,则使用unique_ptr;否则使用shared_ptr。注意:shared_ptr不支持对于对象T[]的从派生类到基类的转换同时也不支持[]运算符。

Things to Remember
• std::shared_ptrs offer convenience approaching that of garbage collection
for the shared lifetime management of arbitrary resources.
• Compared to std::unique_ptr, std::shared_ptr objects are typically
twice as big, incur overhead for control blocks, and require atomic reference
count manipulations.
• Default resource destruction is via delete, but custom deleters are supported.
The type of the deleter has no effect on the type of the std::shared_ptr.
• Avoid creating std::shared_ptrs from variables of raw pointer type.

Item 20: Use std::weak_ptr for std::shared_ptr-like pointers that can dangle.

  • 方法expired()返回当所指的对象是否已经被释放
  • 方法lock()返回当expired()为false时的所指对象的shared_ptr;为true时指向nullptr的shared_ptr
  • shared_ptr可以从另一个weak_ptr对象构造出来。如果另一个weak_ptr的expired()为true,则抛出std::bad_weak_ptr异常。

在observer模式中,使用weak_ptr可以避免原始指针空悬的情况也可以避免shared_ptr造成的内存泄露(循环指向)。weak_ptr不会改变shared_ptr的引用计数而是weak_count的计数。

Things to Remember
• Use std::weak_ptr for std::shared_ptr-like pointers that can dangle.
• Potential use cases for std::weak_ptr include caching, observer lists, and the
prevention of std::shared_ptr cycles.

Item 21: Prefer std::make_unique and std::make_shared to direct use of new.

  • 使用make函数(std::make_unique、std::make_shared)可以避免new函数拷贝参数带来的消耗。
  • make函数同时可以避免在使用shared_ptr作为一个参数时因另一个参数在构造的时候抛出异常而造成的new创建的内存泄露的问题。

使用make函数的优势:

std::shared_ptr<Widget> spw(new Widget);

  • 直接使用new需要首先分配一个空间用于容纳Widget,然后再分配一个空间给控制块。而直接使用make函数可以一次性废分配一块内存。(说明内存分配是耗时的操作!)。
  • 同时,一次性分配内存可以减少程序的所占内存(每分配一块动态内存需要簿记信息)
  • std::allocate_shared的性能分析和std::make_shared一样,所以std::make_shared的性能优势也可以延伸到std::allocate_shared。

不使用make的情况:

  • make函数不能使用自定义的删除器
  • make函数使用了完美转发,当类型的参数接受花括号初始器的时候是无法被调用的
一些类定义了自己的operator newoperator delete函数,这些函数的出现暗示着常规的全局内存分配和回收不适合这种类型的对象。通常情况下,设计这些函数只有为了精确分配和销毁对象,例如,Widget对象的operator newoperator delete只有为了精确分配和回收大小为sizeof(Widget)的内存块才会设计。这两个函数不适合std::shared_ptr的自定义分配(借助std::allocate_shared)和回收(借助自定义删除器),因为std::allocate_shared请求内存的大小不是对象的尺寸,而是对象尺寸加上控制块尺寸。结果就是,使用make函数为那些——定义自己版本的operator newoperator delete的——类创建对象是个糟糕的想法。

一般情况下对象与控制块是分配在一起的。std::weak_ptr的expired方法检查的是引用计数(当前有多少个shared_ptr共享对象)是否为0。也就是说只要控制块的weak count不为0,对象以及控制块的内存就不会被系统回收。但是通过new创建的智能指针由于控制块和对象是分开的,只要最后一个引用计数为0对象就会被销毁!

既要使用自定义删除其又要保证函数调用过程中不会发生内存泄露,一个可行的办法是在函数调用前将new返回的指针保存为一个对象。

std::shared_ptr<Widget> spw(new Widget, cusDel);

processWidget(spw, computeWidget);  // 正确,但没有优化,看下面

当然,如果将spw改为std::move(spw)可以避免增加引用计数而导致的控制块的引用计数的原子操作

Things to Remember
• Compared to direct use of new, make functions eliminate source code duplica‐
tion, improve exception safety, and, for std::make_shared and std::allo
cate_shared, generate code that’s smaller and faster.
• Situations where use of make functions is inappropriate include the need to
specify custom deleters and a desire to pass braced initializers.
• For std::shared_ptrs, additional situations where make functions may be
ill-advised include (1) classes with custom memory management and (2) sys‐
tems with memory concerns, very large objects, and std::weak_ptrs that
outlive the corresponding std::shared_ptrs.

Item 22: When using the Pimpl Idiom, define special member functions in the implementation file.

这个条目主要探讨了Pimpl idiom下的智能指针指向不完整的类型的问题。Pimpl在面向用户的类中保存一个指向其数据结构的指针来降低编译的依赖性。

头文件:

#pragma once
#include <memory>
class Printer
{
private:
	struct PrinterImplement;
	std::unique_ptr<PrinterImplement> _pimpl;
	//std::shared_ptr<PrinterImplement> _pimpl;//使用shared_ptr不需要在实现的文件中定义析构函数等成员 https://stackoverflow.com/questions/40619984/pimpl-idiom-using-shared-ptr-working-with-incomplete-types\
											 	Item22翻译译文:http://blog.csdn.net/big_yellow_duck/article/details/52351729\
												shared_ptr的删除器不是类型的一部分,它所托管的对象在运行时不需要时完整的。而unique_ptr的删除器是类型的一部分,所以保存的类型必须是完整的类型

public:
	Printer(int id);
	//~Printer();//默认生成的析构函数在定义文件中合成
};

源文件:

#include "Printer.h"
#include <vector>
#include <iostream>
//这么做的目的是:在编译器看到Widget析构函数体(即编译器生成销毁std::unique_ptr成员变量的地方)之前,“widget.h”中的Widget::Impl就已经定义了。 \
http://blog.csdn.net/big_yellow_duck/article/details/52351729

struct Printer::PrinterImplement {
	PrinterImplement(int id):_id(id){}
	int _id;
};
/* 使用unique_ptr的时候
Printer::~Printer() {
//_pimpl.~unique_ptr();//if the object being deleted has incomplete class type at the point of deletion and the complete class has a non-trivial destructor or a deallocation function, the behavior is undefined.
std::cout << "Printer::~Printer()" << std::endl;
}
Printer & Printer::operator=(Printer && rhs)
{
std::cout << "Printer::operator=(Printer&&)" << std::endl;
*_pimpl = *rhs._pimpl;
return *this;
}
*/
Printer::Printer(int id):_pimpl(std::make_shared<Printer::PrinterImplement>(id)){}

使用unique_ptr作为Pimpl的指针的时候。用户类没有声明自己的析构函数。编译会自动生成一个隐式内联的析构函数在实例化对象的语句的位置。由于unique_ptr在编译的时候会检查所指的类型是否是不完全类型。所以PrinterImplement在Printer声明的时候是不完全的(使用static_asssert)。因此报错。

  • 解决办法:在Printer的析构函数之前确保PrinterImplement是已经定义了的。这样就不会是不完整的类型。注意定义里自己的析构函数就会组织编译器生成自己的拷贝、移动构造函数/运算符。同一个道理,也需要在实现文件在自己添加这些成员。

自定义的拷贝构造函数和运算符建议使用深拷贝而不是浅拷贝。

使用shared_ptr作为Pimpl指针:std::shared_ptr的删除器不是类型的一部分,需要动态分配所以shared_ptr产生的是更加慢、大(相对与unique_ptr)的代码。shared_ptr不需要使用自定义的析构函数,因为它不需要类型T是完整的类型

unique_ptr和shared_ptr的类型需求:I=incomplete , C=complete

在堆上的unique_ptr就可以避免不完整类型的问题。(也就是类默认生成的析构函数不会销毁unique_ptr)

Things to Remember
• The Pimpl Idiom decreases build times by reducing compilation dependencies
between class clients and class implementations.
• For std::unique_ptr pImpl pointers, declare special member functions in
the class header, but implement them in the implementation file. Do this even
if the default function implementations are acceptable.
• The above advice applies to std::unique_ptr, but not to std::shared_ptr.

CRTP静态分发和动态分发(虚函数):

使用CRTP可以避免使用虚函数造成的虚函数指针和虚函数表占用内存以及间接引用带来的性能损耗。

#include <iostream>
#include <vector>

//这里的问题就是派生类的实现成员必须是公有的(解决方案:使用accessor类,实现的函数秩序是保护的即可)
template<typename T>
class CRTPBase {
public:
	void func() {
		return accessor::func(this->derived());
	}
	T &derived() {
		return static_cast<T&>(*this);//返回派生类的对象
	}
private:
	struct accessor :T {//需要访问派生类的成员
		static void func(T &derived) {
			void (T::*fn)() = &T::func_impl;//实现的函数的指针
			(derived.*fn)();//负责调用
		}
	};
};
class A :public CRTPBase<A> {
public:
protected:
	void func_impl() {
		std::cout << "A::func()" << std::endl;
	}
};
class B :public CRTPBase<B> {
public:
protected:
	void func_impl() {
		std::cout << "B::func()" << std::endl;
	}
};
class A1 :public A {
public:
protected:
	void func_impl() {
		std::cout << "A1::func()" << std::endl;
	}
};
int main()
{
	A a;
	A1 a1;
	B b;
	a.func();
	b.func();
	a1.func();
	return 0;
}

注意,派生类在基类处理函数调用的时候是不可见的,添加友元类可以是基类访问派生类的私有成员但是这样很麻烦。编译时,CRTP就是模拟的虚函数的绑定这样就可以避免虚函数调用在运行时的开销。

引用&参考: