STL-дружественный класс pImpl? - PullRequest
2 голосов
/ 08 января 2010

Я поддерживаю проект, создание которого может занять значительное время, поэтому стараюсь уменьшить зависимости, где это возможно. Некоторые из классов могут использовать, если идиома 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_;
}

Ответы [ 3 ]

3 голосов
/ 08 января 2010

Вам следует рассмотреть возможность использования копирования и замены для назначения, если возможно, что Foo или Bar могут выдать, когда они копируются. Не видя определения этих классов, невозможно сказать, могут ли они или нет. Не видя их опубликованный интерфейс, невозможно сказать, будут ли они в будущем меняться на это без вашего ведома.

Как говорит jalf, использование auto_ptr немного опасно. Он не ведет себя так, как вы хотите при копировании или назначении. Вкратце, я не думаю, что ваш код когда-либо позволяет копировать или назначать элемент impl_, так что, вероятно, все в порядке.

Если вы можете использовать scoped_ptr, то компилятор выполнит эту непростую работу за вас, чтобы убедиться, что он никогда не был неправильно изменен. const может быть заманчиво, но тогда вы не можете поменяться.

2 голосов
/ 08 января 2010

Есть несколько проблем с 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); }

Не так много, учитывая буст (особенно для проблем приведения), но есть некоторые тонкости:

  • семантика правильной копии (т. Е. Глубокая)
  • правильное распространение констант

Вам все еще нужно написать "Dreaded 3". но, по крайней мере, вы можете относиться к этому с семантическим значением.


РЕДАКТИРОВАТЬ : Подстегнутый Фрерихом Раабе, вот ленивая версия, когда написание «Большой тройки» (теперь «четверки») становится проблемой.

Идея состоит в том, чтобы "захватить" информацию, где доступен полный тип, и использовать абстрактный интерфейс, чтобы сделать ее управляемой.

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); }
0 голосов
/ 25 июля 2012

Я боролся с тем же вопросом. Вот что я думаю, ответ:

Вы можете делать то, что предлагаете, при условии, что вы определяете операторы копирования и присваивания, чтобы делать разумные вещи.

Важно понимать, что контейнеры 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);

(или что-то подобное - невозможно запомнить все параметры шаблона). Если бы 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 - неправильный выбор для хранения и вам нужно использовать что-то еще, вероятно, шаблон повышения.

Редактировать : Я думаю, что вышеупомянутый конструктор копирования и оператор присваивания безопасны для исключений , пока ~Impl не вызывает исключение . В любом случае это всегда должно относиться к вашему коду.

...