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;