Комментарии 10
Я бы всё-же не приводил "не очень хороший" код в статье для новичков: в первом примере, раз вы явно говорите об деструкторе, то можно бы и явно прописать реализацию конструкторов/операторов присваивания, следуя хорошему тону — The rule of three/five/zero. auto_ptr
я бы выбросил вообще — всё по тому же поводу — deprecated — можно просто ссылочку дать — так для истории и действительно интересующихся. Про unique_ptr
, возможно, нужно было бы рассказать, почему деструктор нужно реализовывать в файле-реализации (а что с move-семантикой на этот счёт ?) — но это отклонение от темы — зачем нужна своя реализация PimplPtr
-а?
Если убрать из статью вводную, то причина у вас, получается одна — "нарушение логической константности" (а точнее 2ве: "propagate_const
пока не является частью стандарта"). Хорошо, но — код из статьи — не компилируется.
По поводу реализации:
static_assert
— не нужен — это сделает за вас вызов конструктора вmake_unique
.constexpr
— зачем? Он сдесь только мешает код читать и нет случая, когда он нужен был бы.- 2й конструктор, который
explicit
(кстати, а почему? почему и не первый ?), наверное, должен вызывать первый (Delegating constructor) — хотя бы для того, чтобы логика в конструкторах совпадала - зачем в деструкторе делать тот-же
statis_assert
, когда в конструкторе — она уже есть ? - почему здесь
= default
используется, а в примерах выше, в файле-реализации — нет ?:Widget::~Widget() = default
- хорошим тоном является использование одной и той же функции — основной — для реализации других ф-й, которые дублируют код для удобства, т.е. — реализация операторов должна использовать эталонный
get()
Спасибо за статью. Извините за, возможно, резкий тон. Хорошего вам дня ^_^
По поводу "не очень хорошего" кода, я с вами согласен, так как это имеет прямое отношение к рассматриваемому вопросу. Можно было показать в чем заключается проблема, и то, что unique_ptr по умолчанию запрещает попасть в неприятную ситуацию. И тут же описать использование move семантики.
Рассказать, почему деструктор нужно реализовывать в файле-реализации я думаю не стоит, так как это не совсем относиться к рассматриваемой теме, одна ссылка для ознакомления приведена в статье.
“не компилируется” — поправил.
“то причина у вас, получается одна” — еще как минимум одна причина — конструктор по умолчанию, я часто забывал написать явное создание Impl, после чего несколько минут искал ошибку.
По поводу реализации:
- static_assert действительно можно опустить, но я его использую для более приятного сообщения об ошибке.
- constexpr — пожалуй, вы правы.
- Если вызывать конструктор из конструкторы, то можно было бы из первого вызывать второй (но не наоборот), но не думаю что это было бы лучше. ?? explicit
- static_assert нужен как в конструкторе так и деструкторе — сообщения об ошибке отличаются, они указывают предполагаемую причину ошибку, если забыли конструктор, ты будет первое сообщение, если забыли деструктор — второе.
- возможно "= default" было бы лучше
- Согласен с вами, исправил исходный код.
Спасибо за комментарий.
Спасибо за ответ.
если забыли конструктор, ты будет первое сообщение, если забыли деструктор — второе.
Я немного не понял вас. Вот моя логика: sizeof(T)
— приведёт к ошибке компиляции, если компилятор не видет определения типа T
, т.е., T
— неполный тип. sizeof(T)
никак не зависит от того определён для user defined типа конструктор или деструктор. Это означает, что sizeof(T) > 0
всегда — для нашого случая — нужен только для того, чтобы выдать пользователю ошибку во время компиляции о том, что он забыл определить указанный класс (тип) до места его использования (кстати, поскольку sizeof(T)
никогда не может быть 0м — то можно просто писать sizeof(T)
).
Дальше: у вас, по сути, таких места 2: конструктор и деструктор. Ставим вопрос — может ли пользователь написать такое использование PimplPtr<Impl>
, чтобы Impl
был, например, определён до вызова конструктора PimplPtr<Impl>
и, одновременно, не определён при вызове деструктора PimplPtr<Impl>
? (или наоборот). Ответ — да, может:
template<typename T>
struct PimplPtr
{
//
};
// Header
//
struct UserType
{
struct Impl;
PimplPtr<Impl> _impl;
UserType()
// Используем неполный тип `Impl`
: _impl{}
{
}
~UserType();
};
// Source
//
struct UserType::Impl
{
};
// Используем уже определённый тип `Impl`
UserType::~UserType() = default;
// Header
//
struct UserType
{
struct Impl;
PimplPtr<Impl> _impl;
UserType();
~UserType()
{
// Используем неполный тип `Impl`
}
};
// Source
//
struct UserType::Impl
{
};
// Используем уже определённый тип `Impl`
UserType::UserType()
: _impl{}
{
}
Т.е., да — я вас обманул в деструкторе — для приведённого второго случая — такой assert
будет полезен, прошу прощения. Но текст сообщения, всё же, немного неправильный.
По поводу делегирующего конструктора — я снова вас обманул — имел в виду, что 1й конструктор должен вызывать 2й, т.е., для вашего кода, это что-то типа:
PimplPtr(): PimplPtr(std::make_unique<T>()) {}
explicit PimplPtr(std::unique_ptr<T>&& p) noexcept: p_(std::move(p)) {}
И ещё — поскольку у вас в публичном классе есть функция:
PimplPtr(std::unique_ptr<T>&& p)
То это означает, что я могу сделать так:
PimplPtr<int>{std::unique_ptr<int>{nullptr}};
либо, более неявно — через какую-то фабрику:
PimplPtr<T>{make_T()};
где make_T()
вернёт нулевой указатель.
Т.е. не хватает проверки времени выполнения инварианта указатель не nullptr
— я бы добавил assert()
в конструктор и ф-и get()
.
И, моё предвзятое мнение — в мире плюсов — для таких штук как умные указатели — пытаются убежать от неявных опеаторов преобразования: т.е. — пометить бы ещё operator ElementType()
как explicit
...
И… и я бы написал using ElementType = T
— меньше кода и проще читается…
Извините, я увлёкся — простите за, возможно, нелепую критику — ухожу от клавиатуры :)
Спасибо
"Но текст сообщения, всё же, немного неправильный" — что вы имеете ввиду? Я рассматриваю это сообщение как небольшую подсказку, чтобы быстрее вспомнить что может быть не так. Если человек не понимает о чем речь, то ему наверняка придется идти на какой-то ресурс и искать более подробную информацию.
"Т.е. не хватает проверки времени выполнения инварианта указатель не nullptr" — полностью согласен
"operator ElementType() как explicit..." — скорее всего это правильно, но в данном случае возможно это не важно
Не соглашусь с auto_ptr, не все мы живем в идеальном мире, приходится пользоваться и старыми компиляторами.
Переход на новые не всегда возможно, иногда даже вообще невозможно
del
2) Константные методы можно реализовать в интерфейсе.
Не совсем понял про деструкторы. Если вы говорите про класс widget, то деструктор необходим для нормальной работы Pimpl, если же говорите про класс PimplPtr, то, я думаю, нет никаких проблем явно написать конструкторы и операторы присваивания.
Второе совсем не понял, константные методы где? В классе widget? Зачем их реализовывать через интерфейс?
А может подскажете, в каком направлении искать/читать, как принято реализовывать модульность, несколько подобную этой идиоме pimpl, но в случае, когда в рантайме необходимо выбирать, какая конкретно из разных реализаций одного интерфейса выбирается. Различные реализации для различных входных данных. Каждая реализация в своей dll/so с одинаковыми названиями функций. Я правильно понимаю, что подобная задача возникает в медиаплеерах, когда открывается файл, и в зависимости от его формата подключаются разные декодеры потока? Но сформулировать правильный вопрос гуглу не смог, к сожалению.
Если вы хотите выбирать реализацию в рантайме, то, скорее всего, вам нужна некоторая фабрика и, возможно, шаблон проектирования мост.
Класть каждую реализацию в отдельную динамической библиотеку мне не приходилось, но, как я понимаю, система плагинов QtCreator устроена подобным образом, так что можете посмотреть в эту сторону.
“Я правильно понимаю, что подобная задача возникает в медиаплеерах” — возможно, не сталкивался.
Умный указатель для Pimpl