STL友好的pImpl类?

时间:2010-01-08 14:16:39

标签: c++ stl pimpl-idiom

我正在维护一个可能需要相当长时间才能构建的项目,因此我尝试尽可能减少依赖项。如果pImpl成语,我可以使用某些类,并且我想确保我正确地执行此操作,并且这些类将与STL(特别是容器)很好地协作。以下是我打算做的一个示例 - 看起来好吗?我使用std::auto_ptr作为实现指针 - 这是可以接受的吗?使用boost::shared_ptr会更好吗?

以下是SampleImpl类的一些代码,它使用名为FooBar的类:

// 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 个答案:

答案 0 :(得分:3)

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

正如jalf所说,使用auto_ptr有点危险。它的行为与您在复制或分配时的行为方式不同。快速浏览一下,我认为你的代码不允许复制或分配impl_成员,所以它可能没问题。

但是,如果你可以使用scoped_ptr,那么编译器将为你检查它从未被错误地修改过那么棘手的工作。 const可能很诱人,但是你不能交换。

答案 1 :(得分:2)

Pimpl存在一些问题。

首先,虽然不明显:如果使用Pimpl,则必须定义复制构造函数/赋值运算符和析构函数(现在称为“Dreaded 3”)

您可以通过使用正确的语义创建一个漂亮的模板类来简化这一过程。

问题在于,如果编译器为您定义了一个“Dreaded 3”,因为您使用了前向声明,它确实知道如何调用前向声明的对象的“Dreaded 3”...... / p>

最令人惊讶的是:它似乎在大多数情况下与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); }

没有多少考虑提升(特别是对于演员问题),但有一些细节:

  • 正确的复制语义(即深层)
  • 适当的const传播

你仍然要写“Dreaded 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;
};

使用它, true 编译防火墙:

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); }

答案 2 :(得分:0)

我一直在努力解决同样的问题。以下是我认为答案是:

只要您定义副本和赋值运算符以做合理的事情,您就可以执行您的建议。

了解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指向仅由一个auto_ptr拥有的内容。那么当你创建一个auto_ptr的副本时,哪一个现在拥有底层指针?旧的auto_ptr还是新的nullptr?哪一个负责清理底层对象?答案是所有权移动到副本,原始作为指向auto_ptr的指针。

缺少阻止在向量中使用的auto_ptr<T>(const auto_ptr<T>& other); 是复制构造函数,它对正在复制的东西进行const引用:

auto_ptr

(或类似的东西 - 不记得所有模板参数)。如果SampleImpl确实提供了这一点,并且您尝试在第一个示例的main()函数中使用上面的c类,则会崩溃,因为当您将auto_ptr推入向量,pimpl会将c的所有权转移到向量中的对象,c.Incr()将不再拥有它。因此,当您致电nullptr时,此过程会因 SampleImpl(const SampleImpl& other) : pimpl(new Impl(*(other.pimpl))) {} SampleImpl& operator=(const SampleImpl& other) { pimpl.reset(new Impl(*(other.pimpl))); return *this; } 取消引用时出现细分错误而崩溃。

所以你需要决定你的类的底层语义是什么。如果您仍然想要“复制一切”行为,那么您需要提供一个能够正确实现的复制构造函数:

~Impl

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

如果您决定想要此复制行为,则替代方法是让SampleImpl的副本共享基础Impl对象。在这种情况下,不再清楚(或甚至定义好)哪个SampleImpl对象拥有底层的Impl。如果所有权不明确属于一个地方,那么std :: auto_ptr是存储它的错误选择 你需要使用别的东西,可能是一个提升模板。

编辑:我认为上述复制构造函数和赋值运算符是异常安全的只要{{1}}不会抛出异常。无论如何,这应始终适用于您的代码。