Pull to refresh

Умный указатель для Pimpl

Reading time6 min
Views15K

Pimpl (pointer to implementation, указатель на имплементацию) — полезная идиома, распространенная в языке C++. У этой идиомы есть несколько положительных сторон, однако, в этой статье она рассматривается только как средство уменьшения зависимостей времени компиляции. Более подробно о самой идиоме можно посмотреть, например, здесь, здесь и здесь. Эта статья посвящена тому какой умный указатель использовать при работе с Pimpl и зачем он нужен.


Рассмотрим различные варианты реализации Pimpl:


Голый указатель


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


Пример использования:


// widget.h
class Widget {
public:
    Widget();
    ~Widget();
//...
private:
    struct Impl;
    Impl* d_;
};
// widget.cpp
struct Widget::Impl { /*...*/ };
Widget::Widget(): d_(new Impl) {}
Widget::~Widget() { delete d_; }

Плюсы:


  • не нужно никаких дополнительных сущностей

Минусы:


  • необходимость явно удалять указатель (возможная утечка памяти о которой никто не скажет)
  • небезопасно относительно исключений (при возникновении исключения в конструкторе после создания Impl произойдет утечка памяти) — в целом, это главная причина, почему нужно использовать умный указатель.

Использование std::auto_ptr


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


Пример использования:


// widget.h
class Widget {
// ... как раньше
    struct Impl;
    std::auto_ptr<Impl> d_;
};
// widget.cpp
struct Widget::Impl { /*...*/ };
Widget::Widget(): d_(new Impl) {}
Widget::~Widget() {}

auto_ptr, как и другие умные указатели из стандартной библиотеки, берет на себя ответственность за управление временем жизни указателя. С помощью идиомы RAII auto_ptr позволяет работать с Pimpl безопасно относительно исключений, так как при возникновении исключения вызывется его деструктор, который освобождает память.


Несмотря на автоматическое освобождение памяти, auto_ptr имеет очень опасное свойство при работе с Pimpl. При выполнении данного кода, на удивление многих, произойдет утечка памяти без каких либо предупреждений:


// widget.h
class Widget {
public:
    Widget();
//... отсутствует деструктор
private:
    struct Impl;
    std::auto_ptr<Impl> d_;
};
// widget.cpp
struct Widget::Impl { /*...*/ };
Widget::Widget(): d_(new Impl) {}

Это связано с тем, что auto_ptr будет удалять неполный класс. Более подробно с данной проблемой можно ознакомиться здесь. Так как эта проблема относится не только к auto_ptr настоятельно рекомендуется ознакомиться и разобраться с этим вопросом. Краткое решение проблемы в этой ситуации — явное объявление и определение деструктора.


Плюсы:


  • безопасен относительно исключений

Минусы:


  • запрещен
  • возможна утечка памяти при удалении неполного класса


Использование std::unique_ptr


В C++11 появилась семантика перемещения (move semantic), которая позволила заменить auto_ptr на умный указатель с ожидаемым поведением unique_ptr.


Пример использования:


// widget.h
class Widget {
// ... как раньше
    struct Impl;
    std::unique_ptr<Impl> d_;
};
// widget.cpp
struct Widget::Impl { /*...*/ };
Widget::Widget(): d_(std::make_unique<Impl>()) {}
Widget::~Widget() {}

unique_ptr решает проблему удаления неполного класса при проверке на полноту на этапе компиляции. Теперь молча удалять неполный класс не получится.


Однако для решения поставленной задачи unique_ptr все еще имеет недостаток, заключающийся в том, что он имеет семантику обычного указателя. Рассмотрим пример:


// widget.h
class Widget {
public:
// ... как раньше
    void foo() const; // <- константный метод
private:
    struct Impl;
    std::unique_ptr<Impl> d_;
};
// widget.cpp
struct Widget::Impl { int i = 0; };
Widget::Widget(): d_(std::make_unique<Impl>()) {}
Widget::~Widget() {}

void Widget::foo() const {
    d_->i = 42; // <- изменение данных внутри константного метода
}

В подавляющем большинстве случаев компиляция такого кода нежелательна.


Несмотря на то, что в идиоме Pimpl используется указатель, данные, на который он указывает, имеют семантику принадлежности исходному классу. С точки зрения логической константности все данные, в том числе данные Impl, в константных методах должны быть константными.


Плюсы:


  • защита от утечек памяти

Минусы:


  • нарушение логической константности


Использование std::unique_ptr с propagate_const


В экспериментальной библиотеке есть обертка для указателей propagate_const, которая позволяет исправить логическую константность.


Пример использования:


// widget.h
class Widget {
// ... как раньше
    struct Impl;
    std::experimental::propagate_const<std::unique_ptr<Impl>> d_;
};
// widget.cpp
struct Widget::Impl { /*...*/ };
Widget::Widget(): d_(std::make_unique<Impl>()) {}
Widget::~Widget() {}

Теперь код из предыдущего примера будет вызывать ошибки компиляции.


Кажется это близко к полноценному решению проблемы, однако, есть еще один небольшой момент.
При написании конструктора надо всегда явно создавать Impl. Это не кажется большой проблемой, так как, скорее всего, ошибка проявится при первом обращении к классу во время выполнения.


Плюсы:


  • соблюдение логической константности

Минусы:


  • возможность забыть создать Impl в конструкторе
  • propagate_const пока не является частью стандарта


Использование PimplPtr


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


  • безопасность относительно исключений
  • защита от удаления неполного класса
  • соблюдение логической константности
  • защита от несозданного Impl

Первые два пункта можно реализовать с помощью unique_ptr:


template<class T>
class PimplPtr {
public:   
   using ElementType = typename std::unique_ptr<T>::element_type;
 // ...
private:
   std::unique_ptr<T> p_; // <- Должен быть неконстантный для семантики перемещения
};

Третий пункт можно было бы реализовать с помощью propagate_const, но, так как его пока нет в стандарте, можно легко реализовать методы доступа к указателю самостоятельно:


    const ElementType* get() const noexcept { return p_.get(); }
    const ElementType* operator->() const noexcept { return get(); }
    const ElementType& operator*() const noexcept { return *get(); }
    explicit operator const ElementType*() const noexcept { return get(); }

    ElementType* get() noexcept { return p_.get(); }
    ElementType* operator->() noexcept { return get(); }
    ElementType& operator*() noexcept { return *get(); }
    explicit operator ElementType*() noexcept { return get(); }

Для выполнения четвертого пункта нужно реализовать конструктор по умолчанию, который будет создавать Impl:


   PimplPtr(): p_(std::make_unique<T>()) {}

Если у Impl нет конструктора по умолчанию, то компилятор скажет об этом, и пользователю потребуется другой конструктор:


    explicit PimplPtr(std::unique_ptr<T>&& p) noexcept: p_(std::move(p)) {}

Для большей ясности, возможно, стоит добавить статические проверки в конструкторе и деструкторе:


   PimplPtr(): p_(std::make_unique<T>()) {
       static_assert(sizeof(T) > 0, "Probably, you forgot to declare constructor explicitly");
   }

   ~PimplPtr() {
       static_assert(sizeof(T) > 0, "Probably, you forgot to declare destructor explicitly");
   }

И, чтобы сохранить семантику перемещения, надо добавить соответствующие конструктор и оператор:


   PimplPtr(PimplPtr&&) noexcept = default;
   PimplPtr& operator =(PimplPtr&&) noexcept = default;

Весь код целиком:


namespace utils {

template<class T>
class PimplPtr {
public:   
   using ElementType = typename std::unique_ptr<T>::element_type;

   PimplPtr(): p_(std::make_unique<T>()) {
       static_assert(sizeof(T) > 0, "Probably, you forgot to declare constructor explicitly");
   }
   explicit PimplPtr(std::unique_ptr<T>&& p): p_(std::move(p)) {}

   PimplPtr(PimplPtr&&) noexcept = default;
   PimplPtr& operator =(PimplPtr&&) noexcept = default;

   ~PimplPtr() {
       static_assert(sizeof(T) > 0, "Probably, you forgot to declare destructor explicitly");
   }

    const ElementType* get() const noexcept { return p_.get(); }
    const ElementType* operator->() const noexcept { return get(); }
    const ElementType& operator*() const noexcept { return *get(); }
    explicit operator const ElementType*() const noexcept { return get(); }

    ElementType* get() noexcept { return p_.get(); }
    ElementType* operator->() noexcept { return get(); }
    ElementType& operator*() noexcept { return *get(); }
    explicit operator ElementType*() noexcept { return get(); }

private:
   std::unique_ptr<T> p_;
};

} // namespace utils

Пример использования:


// widget.h
class Widget {
// ... как раньше
    struct Impl;
    utils::PimplPtr<Impl> d_;
};
// widget.cpp
struct Widget::Impl { /*...*/ };
Widget::Widget() {}
Widget::~Widget() {}

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


» Исходный код

Tags:
Hubs:
+20
Comments10

Articles