【问题标题】:Implementing pimpl-friendly unique_ptr实现 pimpl 友好的 unique_ptr
【发布时间】:2019-08-07 23:22:31
【问题描述】:

众所周知,std::unique_ptr 可能不方便用于实现 pimpl 惯用语:可能不会默认析构函数并将运算符移动到头文件中(例如,std::unique_ptr with an incomplete type won't compile)。有些人建议改用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,并在impl.cpp 中显式实例化template PimplPtr&lt;PimplMe::Impl&gt;::Reset(),但我认为它很难看。

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

【问题讨论】:

标签: c++ pimpl-idiom template-instantiation


【解决方案1】:

不能在头文件中默认析构函数和移动操作符

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

虽然用唯一指针实现 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;

【讨论】:

  • "解决方案只是在源文件中默认它们。"我知道这个解决方案,并找到它的样板
  • @NikitaPetrenko 样板?您在实现文件中最后为析构函数添加一行以使其工作。将其与您的提案进行比较。
  • @TedLyngmo 我的提案只为整个图书馆写了一次,渐近更好:)
  • @NikitaPetrenko 但是您的解决方案不是需要调用MakePimplPtr 的样板吗?我没有看到样板的减少。
  • @NikitaPetrenko 你用最少的样板代码来换取运行时的惩罚?我对这个想法非常怀疑。无论如何,pimpl 模式需要大量样板文件。如果您想要减少样板,您应该完全跳过 pimpl 模式。否则,MyClass::~MyClass() = default; 几乎没有任何样板。您仍然可以在头文件中定义移动操作;只有析构函数需要在 cpp 文件中。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-11-18
  • 2017-09-09
  • 2012-02-19
  • 2016-05-05
  • 2011-06-15
  • 2014-08-30
  • 1970-01-01
相关资源
最近更新 更多