众所周知,它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_;
};
我看到的唯一缺点是涉及的额外开销很小:还必须存储指向“析构函数”的指针。
我认为这不是什么大问题,因为 8 字节的开销在 pimpl 用例中是微不足道的,我的问题很有趣:是否有一些实用的技巧来消除由dest_caller_
?
我可以考虑拆分PimplPtr
为声明pimpl.hpp
和定义pimpl_impl.hpp
,并显式实例化template PimplPtr<PimplMe::Impl>::Reset()
in impl.cpp
,但我认为它很难看。
声明dest_caller_
为静态成员不是一个解决方案,至少因为它需要在多线程情况下进行同步。