我正在维护一个可能需要相当长时间才能构建的项目,因此我尝试尽可能减少依赖项。如果pImpl
成语,我可以使用某些类,并且我想确保我正确地执行此操作,并且这些类将与STL(特别是容器)很好地协作。以下是我打算做的一个示例 - 看起来好吗?我使用std::auto_ptr
作为实现指针 - 这是可以接受的吗?使用boost::shared_ptr
会更好吗?
以下是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_;
}
答案 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); }
没有多少考虑提升(特别是对于演员问题),但有一些细节:
你仍然要写“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}}不会抛出异常。无论如何,这应始终适用于您的代码。