1

众所周知,它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_为静态成员不是一个解决方案,至少因为它需要在多线程情况下进行同步。

4

1 回答 1

0

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

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

虽然如何用唯一指针实现 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;
于 2019-08-07T23:50:32.957 回答