实施 pimpl 友好型unique_ptr

Implementing pimpl-friendly unique_ptr

本文关键字:ptr unique 好型 pimpl 实施      更新时间:2023-10-16

众所周知,std::unique_ptr可能不方便地用于实现 pimpl 习语:不能默认析构函数并在头文件中移动运算符(例如,不完整类型的 std::unique_ptr 不会编译(。有些人建议改用std::shared_ptr,因为它使用了一些带有析构函数的技巧来克服它(可能只是类型擦除,但我不确定(。

我尝试为这种情况创建一个特殊的智能指针,这是实现:

#include <utility>
#include <type_traits>
template <class>
class PimplPtr;
template <class T, class... Args>
PimplPtr<T> MakePimplPtr(Args&&... args);
template <class T>
class PimplPtr {
static_assert(std::is_class_v<T>, "PimplPtr is only intented for use with classes");
template <class S, class... Args>
friend PimplPtr<S> MakePimplPtr(Args&&... args);
public:
PimplPtr() = default;
PimplPtr(const PimplPtr&) = delete;
PimplPtr(PimplPtr&& other) {
ptr_ = other.ptr_;
other.ptr_ = nullptr;
dest_caller_ = other.dest_caller_;
}
PimplPtr& operator=(const PimplPtr&) = delete;
PimplPtr& operator=(PimplPtr&& other) {
Reset();
ptr_ = other.ptr_;
other.ptr_ = nullptr;
dest_caller_ = other.dest_caller_;
}
~PimplPtr() {
Reset();
}
void Reset() {
if (!ptr_) {
return;
}
// first call the destructor
dest_caller_(ptr_);
// then free the memory
operator delete(ptr_);
ptr_ = nullptr;
}
T* operator->() const {
return ptr_;
}
T& operator*() const {
return *ptr_;
}
private:
explicit PimplPtr(T* ptr) noexcept 
: ptr_(ptr), dest_caller_(&PimplPtr::DestCaller) {
}
static void DestCaller(T* ptr) {
ptr->~T();
}
using DestCallerT = void (*)(T*);
// pointer to "destructor"
DestCallerT dest_caller_;
T* ptr_{nullptr};
};
template <class T, class... Args>
PimplPtr<T> MakePimplPtr(Args&&... args) {
return PimplPtr{new T(std::forward<Args>(args)...)};
}

或者,可以用打字擦除代替指向函数的指针,尽管我认为它会效率较低。

它的工作原理:

class PimplMe {
public:
PimplMe();
// compiles
PimplMe(PimplMe&&) = default;
~PimplMe() = default;
private:
class Impl;
PimplPtr<Impl> impl_;
};

我看到的唯一缺点是涉及少量的额外开销:还必须存储指向"析构函数"的指针。

我认为这不是什么大问题,因为在 pimpl 用例中 8 字节开销微不足道,我的问题纯粹令人感兴趣:是否有一些实用的技巧可以消除dest_caller_造成的空间开销?

我可以想到将PimplPtr拆分为声明pimpl.hpp和定义pimpl_impl.hpp,并在impl.cpp中显式实例化template PimplPtr<PimplMe::Impl>::Reset(),但我认为这很丑陋。

dest_caller_声明为静态成员不是解决方案,至少因为它在多线程情况下需要同步。

不能默认析构函数并在头文件中移动运算符

解决方案只是在源文件中默认它们。

虽然如何使用独特的指针实现 PIMPL 可能并不明显,但这肯定不是不可能,通过编写可重用的模板,可以方便地重复不明显的部分。

我过去写过以下内容;我还没有检查最新的标准版本是否提供了一种简化它的方法:

// pimpl.hpp (add header guards of your choice)
#include <memory>
template <class T>
class pimpl {
public:
pimpl(pimpl&&);
~pimpl();
template <class... Args>
pimpl(Args&&...);
T* operator->();
const T* operator->() const;
T& operator*();
const T& operator*() const;
private:
std::unique_ptr<T> m;
};

// pimpl_impl.hpp (add header guards of your choice)
#include <utility>
#include "pimpl.hpp"
template <class T>
pimpl<T>::pimpl(pimpl&&) = default;
template <class T>
pimpl<T>::~pimpl() = default;
template <class T>
template <class... Args>
pimpl<T>::pimpl(Args&&... args) : m{new T{std::forward<Args>(args)...}} {}
template <class T>
T* pimpl<T>::operator->() {
return m.get();
}
template <class T>
const T* pimpl<T>::operator->() const {
return m.get();
}
template <class T>
T& pimpl<T>::operator*() {
return *m.get();
}
template <class T>
const T& pimpl<T>::operator*() const {
return *m.get();
}

// usage.hpp (add header guards of your choice)
#include "pimpl.hpp"
struct my_class {
my_class();
~my_class();
private:
pimpl<struct my_impl> m;
};

// usage.cpp
#include "usage.hpp"
#include "pimpl_impl.hpp"
struct my_impl {};
my_class::my_class() = default;
my_class::~my_class() = default;