Идиома Pimpl как базовый класс шаблона

В настоящее время я изучаю идиому pimpl, и есть очень хорошие руководства по ее реализации (например, здесь). Но я никогда не видел, чтобы он был реализован как базовый класс шаблона, например:

#ifndef PIMPL_H
#define PIMPL_H

template <class T>
class Pimpl
{
public:
    explicit Pimpl();
    explicit Pimpl(T *ptr);
    virtual ~Pimpl() = 0;

    Pimpl(const Pimpl<T> &other);
    Pimpl &operator=(const Pimpl<T> &other);

protected:
    T *d_ptr;
};

template<class T>
Pimpl<T>::Pimpl() : d_ptr(new T)
{

}

template<class T>
Pimpl<T>::Pimpl(T *ptr) : d_ptr(ptr)
{

}

template<class T>
Pimpl<T>::~Pimpl()
{
    delete d_ptr;
    d_ptr = 0;
}

template<class T>
Pimpl<T>::Pimpl(const Pimpl<T> &other) : d_ptr(new T(*other.d_ptr))
{

}

template<class T>
Pimpl<T> &Pimpl<T>::operator=(const Pimpl<T> &other)
{
    if (this != &other) {
        delete d_ptr;
        d_ptr = new T(*other.d_ptr);
    }

    return *this;
}

#endif // PIMPL_H

Который затем можно было бы использовать в любом классе, который вам нравится pimpl:

#ifndef OBJECT_H
#define OBJECT_H

#include "pimpl.h"

class ObjectPrivate;

class Object : public Pimpl<ObjectPrivate>
{
public:
    Object();
    virtual ~Object();

    /* ... */
};

#endif // OBJECT_H

В настоящее время я использую его в небольшом примерном проекте (сборке в качестве разделяемой библиотеки), и единственная проблема, с которой я столкнулся, заключалась в том, что MSVC предупреждает об отсутствии деструктора для ObjectPrivate (см. C4150). Это предупреждение возникает только потому, что ObjectPrivate объявлен заранее и поэтому не виден оператору удаления в Pimpl::~Pimpl() во время компиляции.

Кто-нибудь видит какие-либо проблемы с этим подходом? :-)


Итак, теперь есть окончательная версия на основе приведенного ниже обсуждения на GitHub (большое спасибо StoryTeller). репозиторий также содержит простой пример использования.


person 0x2648    schedule 11.05.2017    source источник
comment
Избегайте идентификаторов, начинающихся с двойного нижнего подчеркивания. Одно подчеркивание в области класса допустимо.   -  person StoryTeller - Unslander Monica    schedule 22.05.2017
comment
Спасибо за подсказку! Ответ и пример актуальны.   -  person 0x2648    schedule 22.05.2017
comment
Я также должен отметить, что ваш переход на статическое приведение немного меняет семантику. Живой пример, который я предоставил, использует частное наследование (идеально подходит для миксина, IMO). Статическое приведение не удастся, а приведение в стиле C – нет. Это единственное допустимое использование приведения в стиле c. Как у вас есть, пользователь вашего класса должен будет добавить шаблон, который объявляет Pimpl другом.   -  person StoryTeller - Unslander Monica    schedule 22.05.2017
comment
Итак, в настоящее время у меня возникают проблемы с кодом из вашего живого примера при компиляции с помощью MinGW 5.3.0 32bit. Я помещаю все в отдельные файлы (pimpl.h, object_p.h, object.h/.cpp, main.cpp). Если я компилирую с частным наследованием, unmake и clone недоступны в пределах _unmake и _clone. Если я компилирую с открытым наследованием, компилятор жалуется на недопустимое использование класса неполного типа ObjectPrivate в пределах clone. Он также предупреждает о том, что p в unmake имеет неполный тип.   -  person 0x2648    schedule 22.05.2017
comment
Я уже рассматривал проблему доступности в моем предыдущем комментарии. А по поводу неполного типа обратитесь к первому абзацу после примера в моем посте. Поместите определения объекта c'tor, d'tor и оператора присваивания только в object.cpp. Они не могут быть встроенными   -  person StoryTeller - Unslander Monica    schedule 22.05.2017
comment
Для облегчения обсуждения я добавил пример кода на github. Текущая версия использует версию pimpl из вашего примера (и также должна следовать всему, что вы упомянули). Я не могу скомпилировать с теми же ошибками, что и выше.   -  person 0x2648    schedule 22.05.2017
comment
Не могли бы вы пошутить и заменить шаблон pimpl на std::unique_ptr<ObjectPrivate>? Или протестировать свой проект с помощью GCC и/или clang? Я сильно подозреваю, что это проблема с MinGW. Элементы шаблонов не создаются до тех пор, пока они не будут использованы (согласно стандарту C++). Вот почему рекомендации по реализации. Ваш проект в порядке с первого взгляда.   -  person StoryTeller - Unslander Monica    schedule 22.05.2017
comment
Использование std::unique_ptr<ObjectPrivate> d_ptr вместо наследования Pimpl прекрасно работает (если вы не возражаете, что объект больше нельзя копировать). Я смог протестировать реализацию по умолчанию только с MSVC 2015, которая ведет себя точно так же, как MinGW.   -  person 0x2648    schedule 22.05.2017
comment
Ну, я еще раз внимательно посмотрел на это. Ваш класс Object не объявляет operator=, поэтому компилятор генерирует один встроенный. Поэтому создается и вызывается определение Pimpl::operator=, в результате чего клон пытается скопировать неполный тип. Так что этот проект не учел мой исходный комментарий   -  person StoryTeller - Unslander Monica    schedule 22.05.2017
comment
Верное направление! Ошибка была вызвана отсутствием конструктора копирования в Object (см. коммит 5f4310c). Теперь все компилируется без ошибок/предупреждений.   -  person 0x2648    schedule 22.05.2017


Ответы (3)


Да, есть несколько проблем, как я это вижу.

  1. Ваш класс по сути является миксином. Речь идет не о динамическом полиморфизме, поэтому никто никогда не будет вызывать удаление для указателя на Pimpl<ObjectPrivate>. Отбросьте виртуальный деструктор. Это вводит накладные расходы, которые никогда не потребуются. То, что вам нужно, это только статический полиморфизм.

  2. Вы выделяете объект с помощью new и освобождаете с помощью delete. Я не буду использовать ваш шаблон, потому что эта схема распределения не всегда подходит для моих приложений. Вы должны дать возможность настроить схему распределения, чтобы сделать ваш класс действительно полезным.

  3. Ваш оператор присваивания не является безопасным для исключений. Если конструктор для T сбрасывает, вы теряете ранее сохраненные данные. ИМО, в этом случае лучше использовать copy and swap идиома.

Решение (1) и (2) состоит в том, чтобы добавить больше параметров шаблона, где первый для CRTP. Это позволит вам передавать операции, которые вы не знаете, как выполнять, в класс, который наследует ваш миксин. Он может переопределить их, определив свои собственные make, unmake и clone. И все они будут связаны статически.

template <class Handle, class Impl>
class Pimpl
{
private:
    Impl* _make() const
    { return ((Handle const*)this)->make(); }

    void _unmake(Impl *p) const
    { ((Handle const*)this)->unmake(p); }

    Impl* _clone(Impl *p) const
    { return ((Handle const*)this)->clone(p); }

    void swap(Pimpl &other) {
        Impl *temp = d_ptr;
        d_ptr = other.d_ptr;
        other.d_ptr = temp;
    }

public:
    explicit Pimpl();
            ~Pimpl();

    Pimpl(const Pimpl &other);
    Pimpl &operator=(const Pimpl &other);

    // fall-backs
    static Impl* make()          { return new Impl; }
    static void  unmake(Impl* p) { delete p; }
    static Impl* clone(Impl* p)  { return new Impl(*p); }

protected:

    Impl *d_ptr;
};

template<class Handle, class Impl>
Pimpl<Handle, Impl>::Pimpl() :
  d_ptr(_make())
{

}

template<class Handle, class Impl>
Pimpl<Handle, Impl>::~Pimpl()
{
    _unmake(d_ptr);
    d_ptr = 0;
}

template<class Handle, class Impl>
Pimpl<Handle, Impl>::Pimpl(const Pimpl &other) :
  d_ptr(_clone(other.d_ptr))
{

}

template<class Handle, class Impl>
Pimpl<Handle, Impl> &Pimpl<Handle, Impl>::operator=(const Pimpl &other)
{
    Pimpl copy(other);
    swap(copy);

    return *this;
}

Живой пример

Теперь ваш заголовок может компилироваться чисто. Пока деструктор для Object не определен встроенным. Когда он встроен, компилятор должен создать экземпляр деструктора шаблона везде, где включен object.h.

Если он определен в файле cpp после определения ObjectPrivate, то при создании экземпляра ~Pimpl будет отображаться полное определение частных частей.

Дополнительные идеи по улучшению:

  1. Сделайте специальные элементы защищенными. В конце концов, их должен вызывать только производный класс Handle.

  2. Добавить поддержку семантики перемещения.

person StoryTeller - Unslander Monica    schedule 11.05.2017
comment
Мне нравится, что вы много отвечаете (и извините за долгое время ответа, мне пришлось прочитать некоторые вещи, которые вы упомянули). Единственная проблема заключается в том, что с make() можно вызвать только один конструктор (по умолчанию или специализированный) для создания ObjectPrivate. У меня была такая же проблема раньше, и я решил ее, добавив Pimpl(T *ptr), чтобы напрямую установить d_ptr. Кроме того, реализуя конструктор копирования в ObjectPrivate, вы можете управлять поведением new T(*other.d_ptr). Так вам действительно нужны make() и clone()? - person 0x2648; 18.05.2017
comment
@ 0x2648 - Позволить пользователю устанавливать указатель - это, на мой взгляд, беспорядок. Поскольку ваш деструктор всегда вызывает удаление, пользователь должен заботиться о деталях вашей реализации. Вот почему я представил эти функции для начала. Статические make/clone/unmake являются запасными вариантами только в том случае, когда унаследованный класс не хочет делать ничего лучше, чем создавать/удалять. Их можно статически переопределить (см. пример здесь), чтобы поддерживать любую схему распределения. Даже не нужно быть статичным. - person StoryTeller - Unslander Monica; 19.05.2017
comment
Я вижу вашу точку зрения. Поэтому я приму ваш пост в качестве ответа и добавлю финальную реализацию к моему вопросу. :-) - person 0x2648; 19.05.2017

Но я никогда не видел, чтобы он был реализован как базовый класс шаблона.

Это сделал Владимир Батов: https://github.com/yet-another-user/pimpl

Кто-нибудь видит какие-либо проблемы с этим подходом?

Вам нужно серьезно отнестись к предупреждению. Если ваш ObjectPrivate на самом деле имеет нетривиальный деструктор (что так же просто, как содержать член std::string), у вас неопределенное поведение, и деструктор, вероятно, не будет вызван.

Обычно это говорит о том, что по какой-то причине деструктор создается не в том месте. Убедитесь, что все определения всех конструкторов и деструкторов производного класса размещены после полного определения ObjectPrivate. Это включает в себя неявные конструкторы копирования и перемещения, которые, вероятно, вызывают предупреждение в вашем примере кода. Да, это означает, что вы должны явно объявить эти специальные функции (и, как следствие, также операторы копирования и перемещения, если они вам нужны), но, по крайней мере, вы можете использовать определение по умолчанию.

Я не знаю, есть ли такая же проблема в библиотеке Влада.

Кроме того, обнуление указателей в деструкторе бессмысленно и, вероятно, будет просто оптимизировано некоторыми современными компиляторами.

person Sebastian Redl    schedule 11.05.2017
comment
Владимир сделал это с правильным планом, основанным на политике. Очень очень хорошо. Спасибо за ссылку. +1 - person StoryTeller - Unslander Monica; 11.05.2017

Современная версия, которую я использую:


///////////////////////////////
// Header File
template <typename impl_t>
class Pimpl {
  public:
    Pimpl() = default;
    virtual ~Pimpl() = default;
    Pimpl(std::shared_ptr<impl_t> handle) : handle(handle) {}

    std::shared_ptr<impl_t>
    get_handle() const {
      return handle;
    }
  protected:
    std::shared_ptr<impl_t> handle;
};

class object_impl;
class object : public Pimpl<object_impl> {
/* whatever constructors you want*/
public:
  object(int x);
}

///////////////////////////////
// Cpp File
class object_impl {
public:
  object_impl(int x) : x_(x) {}
private:
  int x_;
}

object::object(int x) : Pimpl(std::make_shared<object_impl>(x)) {}
person WilderField    schedule 29.09.2020