C++ 实现pimpl友好的唯一ptr

C++ 实现pimpl友好的唯一ptr,c++,pimpl-idiom,template-instantiation,C++,Pimpl Idiom,Template Instantiation,众所周知,std::unique_ptr可能无法方便地用于实现pimpl习惯用法:不能在头文件的右侧使用默认的析构函数和移动操作符(例如,)。有些人建议改用std::shared_ptr,因为它使用了一些析构函数技巧来克服它(可能只是类型擦除,但我不确定) 我尝试为这种情况创建一个特殊的智能指针,下面是实现: #include <utility> #include <type_traits> template <class> class PimplPtr;

众所周知,
std::unique_ptr
可能无法方便地用于实现pimpl习惯用法:不能在头文件的右侧使用默认的析构函数和移动操作符(例如,)。有些人建议改用
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)...)};
}
#包括
#包括
模板
PimplPtr类;
模板
PimplPtr生成PimplPtr(参数和参数);
模板
类PimplPtr{
静态断言(std::is_class_v,“PimplPtr仅用于类”);
模板
friend PimplPtr MakePimplPtr(Args&&…Args);
公众:
PimplPtr()=默认值;
PimplPtr(常量PimplPtr&)=删除;
PimplPtr(PimplPtr和其他){
ptr_uu=other.ptr_u;
other.ptr u=空ptr;
dest\u caller\ux=other.dest\u caller\ux;
}
PimplPtr&运算符=(常量PimplPtr&)=删除;
PimplPtr和操作员=(PimplPtr和其他){
重置();
ptr_uu=other.ptr_u;
other.ptr u=空ptr;
dest\u caller\ux=other.dest\u caller\ux;
}
~PimplPtr(){
重置();
}
无效重置(){
如果(!ptr_){
返回;
}
//首先调用析构函数
目的地呼叫方(ptr);
//然后释放内存
操作员删除(ptr);
ptr=空ptr;
}
T*运算符->()常量{
返回ptr;
}
T&运算符*()常数{
返回*ptr_2;;
}
私人:
显式PimplPtr(T*ptr)无异常
:ptr_(ptr),dest_调用者_(&PimplPtr::DestCaller){
}
静态无效调用方(T*ptr){
ptr->~T();
}
使用DestCallerT=void(*)(T*);
//指向“析构函数”的指针
DestCallerT dest_caller_;
T*ptr{nullptr};
};
模板
PimplPtr生成PimplPtr(参数&&…参数){
返回PimplPtr{newt(std::forward(args)…)};
}
或者,可以用类型擦除替换函数指针,但我认为这样效率较低

它的工作原理是:

class PimplMe {
public:
    PimplMe();

    // compiles
    PimplMe(PimplMe&&) = default;
    ~PimplMe() = default;
private:
    class Impl;
    PimplPtr<Impl> impl_;
};
类PimplMe{
公众:
PimplMe();
//汇编
PimplMe(PimplMe&&)=默认值;
~PimplMe()=默认值;
私人:
类Impl;
PimplPtr impl;
};
我所看到的唯一缺点是所涉及的额外开销很小:还必须存储指向“析构函数”的指针

我认为这没什么大不了的,因为8字节的开销在pimpl用例中是微不足道的,我的问题很有趣:是否有一些实用的技巧来消除由
dest\u caller\uu
引起的空间开销

我可以考虑将
PimplPtr
拆分为声明
pimpl.hpp
和定义
pimpl\u impl.hpp
,并在
impl.cpp
中显式实例化
模板PimplPtr::Reset()
,但我认为这很难看

dest\u 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可能并不明显,但这肯定不是不可能的,并且通过编写一个可重用的模板,可以方便地重复不明显的部分

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

// 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
使用
unique\u ptr
,只要ju在携带
unique\u ptr的类中声明析构函数即可。示例:也是的,您可以声明它(
~Class();
),然后实现(使用
~Class(){}
,或者使用
~Class()=default;
)。一旦pimpl完成。好的,但我认为你所做的工作超出了这个要求的价值。通过将dtor的定义移动到实现文件中,在pimpl被完全定义之后,您就完成了所有设置。有趣的阅读:您可以(而且通常应该)为
pimpl
使用
unique\u ptr
,只要ju在类中声明一个带
unique\u ptr
的析构函数。示例:也是的,您可以声明它(
~Class();
),然后实现(使用
~Class(){}
,或者使用
~Class()=default;
)。一旦pimpl完成。好的,但我认为你所做的工作超出了这个要求的价值。通过将dtor的定义移动到实现文件中,在pimpl完全定义之后,您就可以全部设置好了。“解决方案只是将它们默认在源文件中。”我知道这个解决方案,并找到了它boilerplate@NikitaPetrenko样板?您将析构函数的最后一行放在实现文件中以使其工作。将其与您的建议进行比较。@TEDLYNMO我的建议只为整个库编写了一次,它渐进地更好:)@NikitaPetrenko但您的解决方案不是需要调用
MakePimplPtr
的样板文件吗?我看不到样板的减少。@NikitaPetrenko你用最少的样板来换取运行时的惩罚?我对这个想法非常怀疑。pimpl模式需要大量的样板文件。如果您想要减少样板文件,那么应该跳过pimpl模式
// 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;