代码之家  ›  专栏  ›  技术社区  ›  Rob

stl友好的pimpl类?

  •  2
  • Rob  · 技术社区  · 15 年前

    我正在维护一个需要花费大量时间来构建的项目,因此我正在尽可能地减少依赖性。如果 pImpl 成语,我想确保我做得正确,并且这些类可以很好地使用STL(尤其是容器)。下面是我计划做的一个示例-看起来可以吗?我正在使用 std::auto_ptr 对于实现指针-这可以接受吗?将使用 boost::shared_ptr 更好的主意?

    这是A的代码 SampleImpl 使用调用的类的类 Foo Bar :

    // SampleImpl.h
    #ifndef SAMPLEIMPL_H
    #define SAMPLEIMPL_H
    
    #include <memory>
    
    // Forward references
    class Foo;
    class Bar;
    
    class SampleImpl
    {
    public:
        // Default constructor
        SampleImpl();
        // Full constructor
        SampleImpl(const Foo& foo, const Bar& bar);
        // Copy constructor
        SampleImpl(const SampleImpl& SampleImpl);
        // Required for std::auto_ptr?
        ~SampleImpl();
        // Assignment operator
        SampleImpl& operator=(const SampleImpl& rhs);
        // Equality operator
        bool operator==(const SampleImpl& rhs) const;
        // Inequality operator
        bool operator!=(const SampleImpl& rhs) const;
    
        // Accessors
        Foo foo() const;
        Bar bar() const;
    
    private:
        // Implementation forward reference
        struct Impl;
        // Implementation ptr
        std::auto_ptr<Impl> impl_;
    };
    
    #endif // SAMPLEIMPL_H
    
    // SampleImpl.cpp
    #include "SampleImpl.h"
    #include "Foo.h"
    #include "Bar.h"
    
    // Implementation definition
    struct SampleImpl::Impl
    {
        Foo foo_;
        Bar bar_;
    
        // Default constructor
        Impl()
        {
        }
    
        // Full constructor
        Impl(const Foo& foo, const Bar& bar) :
            foo_(foo),
            bar_(bar)
        {
        }
    };
    
    SampleImpl::SampleImpl() :
        impl_(new Impl)
    {
    }
    
    SampleImpl::SampleImpl(const Foo& foo, const Bar& bar) :
        impl_(new Impl(foo, bar))
    {
    }
    
    SampleImpl::SampleImpl(const SampleImpl& sample) :
        impl_(new Impl(*sample.impl_))
    {
    }
    
    SampleImpl& SampleImpl::operator=(const SampleImpl& rhs)
    {
        if (this != &rhs)
        {
            *impl_ = *rhs.impl_;
        }
        return *this;
    }
    
    bool SampleImpl::operator==(const SampleImpl& rhs) const
    {
        return  impl_->foo_ == rhs.impl_->foo_ &&
            impl_->bar_ == rhs.impl_->bar_;
    }
    
    bool SampleImpl::operator!=(const SampleImpl& rhs) const
    {
        return !(*this == rhs);
    }
    
    SampleImpl::~SampleImpl()
    {
    }
    
    Foo SampleImpl::foo() const
    {
        return impl_->foo_;
    }
    
    Bar SampleImpl::bar() const
    {
        return impl_->bar_;
    }
    
    3 回复  |  直到 12 年前
        1
  •  3
  •   Steve Jessop    15 年前

    如果FOO或BAR可能在复制时抛出,则应考虑使用复制和交换分配。如果看不到这些类的定义,就不可能说出它们是否可以。如果没有看到他们发布的界面,就不可能说他们将来是否会这样做,而你没有意识到。

    正如jalf所说,使用auto-ptr有点危险。它在复制或分配时不符合您的要求。快速看一下,我认为您的代码从未允许复制或分配IMPL成员,所以这可能是正常的。

    但是,如果您可以使用作用域指针,那么编译器将为您做一些棘手的工作,检查它是否没有被错误地修改过。 const 可能很诱人,但你不能交换。

        2
  •  2
  •   Matthieu M.    12 年前

    PIMPL有几个问题。

    首先,尽管不明显:如果使用PIMPL,则必须定义复制构造函数/赋值运算符和析构函数(现在称为“可怕的3”)。

    您可以通过使用适当的语义创建一个好的模板类来缓解这一问题。

    问题是,如果编译器开始为您定义“可怕的3”中的一个,因为您已经使用了forward声明,它知道如何调用所声明的对象forward的“可怕的3”…

    最令人惊讶的是:它似乎与 std::auto_ptr 大多数时候,但是你会有意想不到的内存泄漏,因为 delete 不起作用。但是,如果使用自定义模板类,编译器会抱怨它找不到所需的操作符(至少,这是我在GCC 3.4.2中的经验)。

    作为奖励,我自己的PIMPL类:

    template <class T>
    class pimpl
    {
    public:
      /**
       * Types
       */
      typedef const T const_value;
      typedef T* pointer;
      typedef const T* const_pointer;
      typedef T& reference;
      typedef const T& const_reference;
    
      /**
       * Gang of Four
       */
      pimpl() : m_value(new T) {}
      explicit pimpl(const_reference v) : m_value(new T(v)) {}
    
      pimpl(const pimpl& rhs) : m_value(new T(*(rhs.m_value))) {}
    
      pimpl& operator=(const pimpl& rhs)
      {
        pimpl tmp(rhs);
        swap(tmp);
        return *this;
      } // operator=
    
      ~pimpl() { delete m_value; }
    
      void swap(pimpl& rhs)
      {
        pointer temp(rhs.m_value);
        rhs.m_value = m_value;
        m_value = temp;
      } // swap
    
      /**
       * Data access
       */
      pointer get() { return m_value; }
      const_pointer get() const { return m_value; }
    
      reference operator*() { return *m_value; }
      const_reference operator*() const { return *m_value; }
    
      pointer operator->() { return m_value; }
      const_pointer operator->() const { return m_value; }
    
    private:
      pointer m_value;
    }; // class pimpl<T>
    
    // Swap
    template <class T>
    void swap(pimpl<T>& lhs, pimpl<T>& rhs) { lhs.swap(rhs); }
    

    考虑不多(尤其是演员阵容问题),但有一些细节:

    • 正确的复制语义(即深度)
    • 适当的常数传播

    你还得写“可怕的3”。但至少你可以用价值语义来处理它。


    编辑 :在Frerich Raabe的鼓励下,这是一个懒惰的版本,当写三大(现在是四大)是一个麻烦。

    其思想是“捕获”完整类型可用的信息,并使用抽象接口使其可操作。

    struct Holder {
        virtual ~Holder() {}
        virtual Holder* clone() const = 0;
    };
    
    template <typename T>
    struct HolderT: Holder {
        HolderT(): _value() {}
        HolderT(T const& t): _value(t) {}
    
        virtual HolderT* clone() const { return new HolderT(*this); }
        T _value;
    };
    

    用这个,一个 编译防火墙:

    template <typename T>
    class pimpl {
    public:
        /// Types
        typedef T value;
        typedef T const const_value;
        typedef T* pointer;
        typedef T const* const_pointer;
        typedef T& reference;
        typedef T const& const_reference;
    
        /// Gang of Five (and swap)
        pimpl(): _holder(new HolderT<T>()), _p(this->from_holder()) {}
    
        pimpl(const_reference t): _holder(new HolderT<T>(t)), _p(this->from_holder()) {}
    
        pimpl(pimpl const& other): _holder(other->_holder->clone()),
                                   _p(this->from_holder())
        {}
    
        pimpl(pimpl&& other) = default;
    
        pimpl& operator=(pimpl t) { this->swap(t); return *this; }
    
        ~pimpl() = default;
    
        void swap(pimpl& other) {
            using std::swap;
            swap(_holder, other._holder);
            swap(_p, other._p)
        }
    
        /// Accessors
        pointer get() { return _p; }
        const_pointer get() const { return _p; }
    
        reference operator*() { return *_p; }
        const_reference operator*() const { return *_p; }
    
        pointer operator->() { return _p; }
        const_pointer operator->() const { return _p; }
    
    private:
        T* from_holder() { return &static_cast< HolderT<T>& >(*_holder)._value; }
    
        std::unique_ptr<Holder> _holder;
        T* _p;           // local cache, not strictly necessary but avoids indirections
    }; // class pimpl<T>
    
    template <typename T>
    void swap(pimpl<T>& left, pimpl<T>& right) { left.swap(right); }
    
        3
  •  0
  •   Tom    12 年前

    我一直在同一个问题上挣扎。我认为答案是:

    只要定义了复制和分配操作符来执行明智的操作,就可以执行您建议的操作。

    重要的是要理解STL容器创建了事物的副本。所以:

    class Sample {
    public:
        Sample() : m_Int(5) {}
        void Incr() { m_Int++; }
        void Print() { std::cout << m_Int << std::endl; }
    private:
        int m_Int;
    };
    
    std::vector<Sample> v;
    Sample c;
    v.push_back(c);
    c.Incr();
    c.Print();
    v[0].Print();
    

    其输出为:

    6
    5
    

    也就是说,向量存储了一个C的副本,而不是C本身。

    因此,当您将它重写为PIMPL类时,您会得到:

    class SampleImpl {
    public:
        SampleImpl() : pimpl(new Impl()) {}
        void Incr() { pimpl->m_Int++; }
        void Print() { std::cout << m_Int << std::endl; }
    private:
        struct Impl {
            int m_Int;
            Impl() : m_Int(5) {}
        };
        std::auto_ptr<Impl> pimpl;
    };
    

    注意,为了简洁起见,我对pimpl习惯用法做了一些修改。如果您尝试将其推送到一个向量中,它仍会尝试创建 SampleImpl 类。但这不起作用,因为 std::vector 要求它存储的内容提供一个复制构造函数, 不修改它正在复制的内容 .

    auto_ptr 指向某个完全属于一个人的东西 自动PTR . 所以当你创建一个 自动PTR ,哪个现在拥有底层指针?老年人 自动PTR 还是新的?哪个负责清理底层对象?答案是所有权转移到副本,而原件则作为指向 nullptr .

    什么 自动PTR 缺少可防止其在向量中使用的是复制构造函数,该构造函数对正在复制的对象采用常量引用:

    auto_ptr<T>(const auto_ptr<T>& other);
    

    (或类似的内容-无法记住所有模板参数)。如果 自动PTR 提供了这个,并且您尝试使用 采样干扰 班以上在 main() 从第一个例子来看,它会崩溃,因为当你按下 c 在矢量中, 自动节拍 将转让 pimpl 到矢量中的对象 C 将不再拥有它。所以当你打电话的时候 c.Incr() ,进程将崩溃,并在 努尔普特 撤销引用。

    所以您需要决定类的底层语义是什么。如果仍然需要“复制所有内容”行为,则需要提供一个正确实现该行为的复制构造函数:

        SampleImpl(const SampleImpl& other) : pimpl(new Impl(*(other.pimpl))) {}
        SampleImpl& operator=(const SampleImpl& other) { pimpl.reset(new Impl(*(other.pimpl))); return *this; }
    

    现在,当您尝试获取一个sampleimpl的副本时,您还将获得其impl结构的副本,该结构由copy sampleimpl拥有。如果您将一个拥有大量私有数据成员并且在STL容器中使用的对象转换为PIMPL类,那么这可能是您想要的,因为它提供了与原始对象相同的语义。但是请注意,将对象推送到一个向量中会慢得多,因为现在复制对象时涉及到动态内存分配。

    如果你决定 不要 如果需要这种复制行为,那么另一种方法是让sampleimpl的副本共享底层的impl对象。在这种情况下,不再清楚(甚至是定义良好的)sampleimpl对象拥有底层impl。如果所有权不明确地属于一个地方,那么std::auto-ptr是存储它的错误选择。 你需要使用其他东西,可能是一个增强模板。

    编辑 :我认为上面的复制构造函数和赋值运算符是异常安全的。 只要 ~Impl 不会引发异常 . 不管怎样,您的代码应该总是这样。