【问题标题】:STL-friendly pImpl class?STL 友好的 pImpl 类?
【发布时间】:2010-01-08 14:16:39
【问题描述】:

我正在维护一个可能需要相当长的时间来构建的项目,因此我试图尽可能减少依赖关系。如果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_;
}

【问题讨论】:

  • boost::scoped_ptr 几乎就是为此目的而创建的。它比auto_ptr 更合适,因为它根本不允许更改所有权。
  • @Rob 你的评论很好,我觉得我不得不删除我的答案;)
  • @Andreas,你最初是对的,我正在评论 Rob 的评论。如果Foo的构造函数在new Foo()的构造过程中抛出异常,删除对象不是你的责任,由语言保证内存会被释放。
  • @Idan K 不,实际上我错了;)SampleImpl 的内存将被删除,但SampleImpl 的构造函数分配的内存不会被删除,除非可以找到内存的释放函数(即类似于auto_ptr 的析构函数)。有关详细信息,请参阅标准 5.3.4/17。

标签: c++ stl pimpl-idiom


【解决方案1】:

如果 Foo 或 Bar 在被复制时可能会抛出,您应该考虑使用复制和交换进行分配。如果没有看到这些类的定义,就无法说它们是否可以。如果没有看到他们发布的界面,就无法说他们将来是否会改变,而您没有意识到。

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

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

【讨论】:

  • 如果我使用 boost::scoped_ptr 我可以从我的班级中删除空析构函数吗?
  • 实际上,我没有注意到空的析构函数。它在当前代码中实现了什么 - 它与根本没有析构函数并让编译器生成默认值不一样吗?我猜 3 的规则说你需要实现析构函数,但实际上在这种情况下我认为规则是错误的。默认析构函数很好,因为您实际上并没有克隆 operator= 中的任何资源,只是修改了您已经拥有的资源。
  • boost::scoped_ptr 除非我提供析构函数(我得到'使用未定义类型'和'删除指向不完整类型'SampleImpl :: Impl'的指针;没有调用析构函数'错误。但是,boost::shared_ptr 不需要析构函数。
  • @Steve。如果没有析构函数,如果我使用boost::scoped_ptr,则会出现编译错误。我在std::auto_ptr 那里有它,因为我确定我曾经读过它是必需的(可能是迈耶斯。)
  • 啊,我现在知道为什么 shared_ptr 不同了,因为 shared_ptr 没有在其析构函数中引用有效负载的析构函数,而是在其构造函数中引用它。 auto_ptr 和 scoped_ptr 没有可配置的释放器,因此它们在其析构函数中引用了有效负载的析构函数。 stackoverflow.com/questions/311166/…
【解决方案2】:

Pimpl 存在一些问题。

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

您可以通过创建一个具有适当语义的漂亮模板类来缓解这种情况。

问题在于,如果编译器设置为您定义“Dreaded 3”之一,因为您使用了前向声明,它确实知道如何调用前向声明的对象的“Dreaded 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); }

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

  • 适当的复制语义(即深度)
  • 正确的 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); }

【讨论】:

  • 我认为您的 pimpl 模板非常简洁 - 我从来没有想过为此有一个模板,以便我可以轻松地拥有值语义隐藏私有实现无需编写样板代码。我似乎记得在 StackOverflow 上发布的所有代码都在公共域中 - 这段代码也是如此吗?我可能想在我正在从事的商业项目中使用它......
  • @FrerichRaabe:是的,请随意使用它,如果它爆炸了不要怪我。 (根据经验,我建议将delete 替换为boost::checked_delete)。请注意,它并没有真正隐藏实现细节(T 的定义必须可见)。我还有另一个(更复杂的版本),它重用shared_ptr 的原理来捕获删除函数。
  • 明白 - 但是,T 的定义必须可见是什么意思? pimpl 仅包含一个指针,因此前向声明就足够了。请参阅ideone.com/rgFi0 以获取演示我如何使用您的模板的小示例(注意Private 结构是前向声明的)。 C 不需要“三巨头”真是太好了。 :-)
  • 嗯,也许我的代码正是boost::checked_delete会触发编译错误的情况...
  • @FrerichRaabe:不幸的是,将delete 应用于不完整的类型(例如,前向声明)是未定义的行为。大多数情况下,这意味着不会调用析构函数,如果它不是 POD,那就太糟糕了。因此,按原样呈现的课程只是一个不完整的 PIMPL。它保留了 ABI(大小是固定的),但它不提供编译防火墙保证(单独)。您仍然必须在完整类型可见的地方定义三巨头....
【解决方案3】:

我一直在为同样的问题而苦苦挣扎。以下是我认为的答案:

您可以按照您的建议做,只要您定义复制和赋值运算符来做明智的事情。

了解 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 的缺失会阻止其在向量中的使用,即复制构造函数对被复制的内容进行 const 引用:

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

(或类似的东西 - 无法记住所有模板参数)。如果auto_ptr 确实提供了这个,并且您尝试在第一个示例中的main() 函数中使用上面的SampleImpl 类,它会崩溃,因为当您将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; }

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

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-04-15
    • 1970-01-01
    • 2023-04-01
    • 2023-03-21
    • 2011-11-13
    相关资源
    最近更新 更多