std::unique_ptr
std::unique_ptr
是一种几乎和原始指针一样高效的智能指针,对所管理的指针资源拥有独占权。由C++11标准引入,用于替代C++98中过时的std::auto_ptr
智能指针。相比而言,std::unique_ptr
的优点有:
- 语义更清晰:
std::auto_ptr
进行拷贝的时候实际执行的是移动语义,但C++98中并没有定义出移动语义,所以使用的时候可能会违背直觉。而std::unique_ptr
利用了C++11中新定义的移动语义,只允许移动操作,禁止拷贝操作,从而让语义更加清晰。 - 允许自定义删除器:由于
std::unique_ptr
将删除器作为自己的成员变量,所以传入自定义删除器之前需要在模板参数中指定删除器的类型std::unique_ptr
。up(nullptr, deleter) - 支持STL容器:在C++98中,容器要求元素必须是可以拷贝的,比如«effective STL»中提到的,对容器中的元素进行
std::sort
时,会从区间中选一个元素拷贝为主元素(pivot),然后再对所有元素进行分区操作。但是std::auto_ptr
的拷贝操作执行的却是移动语义,这样就会造成bug。在C++11中,STL容器是支持移动语义的,std::unique_ptr
只提供移动操作删除了拷贝操作,并且移动操作是noexcept
的(这一点很重要,因为STL容器有些操作需要保证强异常安全会要求要么用拷贝操作要么用无异常的移动操作)。只要不涉及到拷贝的容器操作,比如fill
函数,那么std::unique_ptr
作为容器元素是正确的。
std::unique_ptr的大小
默认情况下,std::unique_ptr
的大小和原始指针一样:
#include
#include
int main(int argc, const char* argv[]) {
std::unique_ptr upNum(new int);
// 输出8(64位操作系统)
std::cout << sizeof(upNum) << std::endl;
return 0;
}
当添加删除器的时候,情况就发生了变化,std::unique_ptr
的大小就等于原始指针的大小加上删除器类型的大小:
#include
#include
void deleter(int* pNum) {
std::cout << "function deleter" << std::endl;
delete pNum;
}
int main(int argc, const char* argv[]) {
std::unique_ptr upNum(new int, deleter);
// 输出8+8=16(函数指针类型的大小也为8)
std::cout << sizeof(upNum) << std::endl;
return 0;
}
这种情况是可以优化的,那就是使用仿函数或者lambda函数作为删除器,当仿函数或lambda函数是无状态(stateless)的时候,那么类类型作为类成员变量的时候,会自动优化掉空类所占用的空间:
#include
#include
int main(int argc, const char* argv[]) {
auto deleter = [](int* pNum) {
std::cout << "lambda deleter" << std::endl;
delete pNum;
};
std::unique_ptr upNum(new int, deleter);
// 输出8
std::cout << sizeof(upNum) << std::endl;
return 0;
}
使用场景1-工厂模式
#include
#include
class Foo {
public:
void greeting() noexcept {
std::cout << "hi! i am foo" << std::endl;
}
};
class Factory {
public:
std::unique_ptr createFoo() {
return std::unique_ptr(new Foo);
}
};
int main(int argc, const char* argv[]) {
auto foo = Factory().createFoo();
// 输出"hi! i am foo"
foo->greeting();
return 0;
}
使用场景2-实现pImpl模式
PIMPL(Pointer to Implementation)是通过将所有成员变量封装到私有的Impl结构体中,自身只保留一个指向结构体对象的一个私有的指针成员。
pImpl模式的优点有:1.降低模块的耦合度,2.提高编译速度,3.提高接口稳定性。
// Foo.h
#pragma once
#include
#include
class Foo {
public:
Foo();
// 需要将~Foo的实现放入Foo.cpp中,避免出现delete imcomplete type错误
~Foo();
// 1.定义了~Foo之后不会自动生成移动函数
// 2.移动构造函数中因为会生成处理异常的代码,所以需要析构成员变量,也会造成delete imcomplete type问题,所以将实现放入Foo.cpp
// 3.移动赋值函数中因为会先删除自己指向的Impl对象指针,也会造成delete imcomplete type问题,所以将实现放入Foo.cpp
Foo(Foo&& rhs) noexcept;
Foo& operator=(Foo&& rhs) noexcept;
// 由于unique_ptr不支持复制,所以无法生成默认拷贝函数
Foo(const Foo& rhs);
Foo& operator=(const Foo& rhs);
void setName(std::string name);
const std::string& getName() const noexcept;
private:
struct Impl;
std::unique_ptr m_upImpl;
};
// Foo.cpp
#include "Foo.h"
#include
struct Foo::Impl {
std::string name;
};
Foo::Foo() : m_upImpl(new Impl) {}
Foo::~Foo() = default;
Foo::Foo(Foo&& rhs) noexcept = default;
Foo& Foo::operator=(Foo&& rhs) noexcept = default;
Foo::Foo(const Foo& rhs) : m_upImpl(new Impl) {
*m_upImpl = *rhs.m_upImpl;
}
Foo& Foo::operator=(const Foo& rhs) {
*m_upImpl = *rhs.m_upImpl;
return *this;
}
void Foo::setName(std::string name) {
m_upImpl->name = name;
}
const std::string& Foo::getName() const noexcept {
return m_upImpl->name;
}
尽量使用std::make_unique
使用std::make_unique
来创建std::unique_ptr
智能指针有以下优点:
- 减少代码重复:从代码
std::unique_ptr
和upFoo(new Foo); auto upFoo = std::make_unique
可以得知使用make_unique只需要写一次Foo就可以,更加符合软件工程中的要求。(); 提高异常安全性:当在函数调用中构造智能指针时,由于执行顺序的不确定性,有可能会造成资源泄露,比如对于代码:
这里调用func函数时,会执行三个步骤#include
#include #include bool priority() { throw std::exception(); return true; } void func(std::unique_ptr upNum, bool flag) { if (flag) { std::cout << *upNum << std::endl; } } int main() { func(std::unique_ptr (new int), priority()); return 0; } new int
std::unique_ptr
构造函数priority
函数
这里唯一可以确定的就是步骤1发生在步骤2之前,但步骤3的次序是不一定的,如果步骤3在步骤1和步骤2中间执行那么就会造成内存泄漏。但是如果使用
make_unique
就不会出现这个问题。
但是std::make_unique
是C++14标准才引入的,所以使用C++11环境的话需要自己实现这个函数:
template
std::unique_ptr make_unique(Ts&&... params)
{
return std::unique_ptr(new T(std::forward(params)...));
}
- 素材来自于«effective modern c++»