用std::unique_ptr实现pimpl手法必须在.cpp文件中提供析构函数定义

pimpl手法 就是把数据成员提取到类中,用指向该类的指针替代原来的数据成员。因为数据成员会影响内存布局,将数据成员用一个指针替代可以减少编译期依赖,保持 ABI 兼容。

比如对如下类:

//@ A.h
#include <string>
#include <vector>

class A {
    int i;
    std::string s;
    std::vector<double> v;
};

使用 pimpl手法 后:

//@ A.h
class A {
public:
    A();
    ~A();
private:
    struct X;
    X* x;
};

//@ A.cpp
#include "A.h"
#include <string>
#include <vector>

struct A::X {
    int i;
    std::string s;
    std::vector<double> v;
};

A::A() : x(new X) {}
A::~A() { delete x; }

现在使用 std::unique_ptr 替代原始指针,不再需要使用析构函数释放指针:

//@ A.h
#include <memory>

class A {
public:
    A();
private:
    struct X;
    std::unique_ptr<X> x;
};

//@ A.cpp
#include "A.h"
#include <string>
#include <vector>

struct A::X {
    int i;
    std::string s;
    std::vector<double> v;
};

A::A() : x(std::make_unique<X>()) {}

但调用上述代码会出错:

//@ main.cpp
#include "A.h"

int main()
{
	A a; //@ 错误:A::X是不完整类型
}

原因在于 std::unique_ptr 析构时会在内部调用默认删除器,默认删除器的 delete 语句之前会用 static_assert 断言指针指向的不是非完整类型。

//@ 删除器的实现
template<class T>
struct default_delete //@ default deleter for unique_ptr
{
    constexpr default_delete() noexcept = default;

    template<class U, enable_if_t<is_convertible_v<U*, T*>, int> = 0>
    default_delete(const default_delete<U>&) noexcept
    { //@ construct from another default_delete
    }

    void operator()(T* p) const noexcept
    {
        static_assert(0 < sizeof(T), "can't delete an incomplete type");
        delete p;
    }
};

解决方法就是让析构 std::unique_ptr 的代码看见完整类型,即让析构函数的定义位于要析构的类型的定义之后。

//@ A.h
#include <memory>

class A {
public:
    A();
    ~A();
private:
    struct X;
    std::unique_ptr<X> x;
};

//@ A.cpp
#include "A.h"
#include <string>
#include <vector>

struct A::X {
    int i;
    std::string s;
    std::vector<double> v;
};

A::A() : x(std::make_unique<X>()) {}
A::~A() = default; //@ 必须位于A::X的定义之后
  • 使用 pimpl手法 的类自然应该支持移动操作,但定义析构函数会阻止默认生成移动操作,因此会想到添加默认的移动操作声明。
//@ A.h
#include <memory>

class A {
public:
    A();
    ~A();
    A(A&&) = default;
    A& operator=(A&&) = default;
private:
    struct X;
    std::unique_ptr<X> x;
};

//@ A.cpp
#include "A.h"
#include <string>
#include <vector>

struct A::X {
    int i;
    std::string s;
    std::vector<double> v;
};

A::A() : x(std::make_unique<X>()) {}
A::~A() = default; //@ 必须位于A::X的定义之后

但调用移动操作会出现相同的问题。

//@ main.cpp
#include "A.h"

int main()
{
    A a;
    A b(std::move(a)); //@ 错误:使用了未定义类型A::X
    A c = std::move(a); //@ 错误:使用了未定义类型A::X
}

原因也一样,移动操作会先析构原有对象,调用删除器时触发断言。解决方法也一样,让移动操作的定义位于要析构的类型的定义之后。

//@ A.h
#include <memory>

class A {
public:
    A();
    ~A();
    A(A&&);
    A& operator=(A&&);
private:
    struct X;
    std::unique_ptr<X> x;
};

//@ A.cpp
#include "A.h"
#include <string>
#include <vector>

struct A::X {
    int i;
    std::string s;
    std::vector<double> v;
};

A::A() : x(std::make_unique<X>()) {}
A::A(A&&) = default;
A& A::operator=(A&&) = default;
A::~A() = default;

编译器不会为 std::unique_ptr 这类 move-only 类型生成拷贝操作,即使可以生成也只是拷贝指针本身(浅拷贝),因此如果要提供拷贝操作,则需要自己编写。

//@ A.h
#include <memory>

class A {
public:
    A();
    ~A();
    A(A&&);
    A& operator=(A&&);
    A(const A&);
    A& operator=(const A&);
private:
    struct X;
    std::unique_ptr<X> x;
};

//@ A.cpp
#include "A.h"
#include <string>
#include <vector>

struct A::X {
    int i;
    std::string s;
    std::vector<double> v;
};

A::A() : x(std::make_unique<X>()) {}
A::A(A&&) = default;
A& A::operator=(A&&) = default;
A::~A() = default;
A::A(const A & rhs) : x(std::make_unique<X>(*rhs.x)) {}
A& A::operator=(const A & rhs)
{
    *x = *rhs.x;
    return*this;
}

如果使用 std::shared_ptr,则不需要关心上述所有问题

//@ A.cpp
#include "A.h"
#include <string>
#include <vector>

struct A::X {
    int i;
    std::string s;
    std::vector<double> v;
};

A::A() : x(std::make_shared<X>()) {}

实现 pimpl手法 时:

  • std::unique_ptr尺寸更小,运行更快一些,但必须在实现文件中指定特殊成员函数,std::shared_ptr 开销大一些,但不需要考虑因为删除器引发的一系列问题。
  • 但对于 pimpl手法 来说,主类和数据成员类之间是专属所有权的关系,std::unique_ptr 更合适。如果在需要共享所有权的特殊情况下,std::shared_ptr 更合适。