2

我正在维护一个可能需要花费大量时间来构建的项目,因此我正在尝试尽可能减少依赖关系。pImpl如果习语和我想确保我正确执行此操作并且这些类将与 STL(尤其是容器)很好地配合使用,则某些类可以使用。这是我计划做的一个示例 - 这看起来好吗? 我正在使用std::auto_ptr实现指针 - 这可以接受吗?使用 aboost::shared_ptr会更好吗?

以下是使用名为and的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_;
}
4

3 回答 3

3

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

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

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

于 2010-01-08T14:47:19.770 回答
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;
};

并使用它,一个真正的编译防火墙:

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); }
于 2010-01-08T17:22:16.077 回答
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它要求它存储的东西提供一个不修改它正在复制的东西的复制构造函数。

Anauto_ptr指向完全属于 one 的东西auto_ptr。因此,当您创建 的副本时auto_ptr,现在哪个拥有底层指针?旧auto_ptr的还是新的?哪个负责清理底层对象?答案是所有权转移到副本,而原件作为指向nullptr.

阻止它在向量中auto_ptr使用的缺失是复制构造函数对被复制的事物进行 const 引用:

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

(或类似的东西 - 不记得所有的模板参数)。如果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; }

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

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

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

于 2012-07-25T12:20:27.090 回答