Когда компилятор создает деструктор std::unique_ptr<Foo>
, компилятор должен найти Foo::~Foo()
и вызвать его. Это означает, что Foo
должен быть полным типом в точке, где std::unique_ptr<Foo>
уничтожено.
Этот код в порядке:
struct Foo;
std::unique_ptr<Foo> create();
... до тех пор, пока вам не нужно вызывать деструктор std::unique_ptr<Foo>
! Для фабричной функции, которая возвращает std::unique_ptr
классу, этот класс должен быть завершенным типом. Вот как бы вы объявили фабрику:
#include "foo.hpp"
std::unique_ptr<Foo> create();
Вы, похоже, правильно реализуете pimpl с std::unique_ptr
. Вы должны определить A::~A()
в точке завершения B
(которая находится в файле cpp). Вы должны определить A::A()
в том же месте, потому что B
должен быть завершен, если вы хотите выделить память и вызвать ее конструктор.
Так что все в порядке:
// a.hpp
struct A {
A();
~A();
private:
struct B;
std::unique_ptr<B> b;
};
// a.cpp
struct A::B {
// ...
};
A::A()
: b{std::make_unique<B>()} {}
A::~A() = default;
Теперь давайте рассмотрим это (сделаем вид, что я не сделал b
приватным):
int main() {
A a;
auto b = std::move(a.b);
}
Что именно здесь происходит?
- Мы создаем
std::unique_ptr<B>
для инициализации b
.
b
- локальная переменная, которая означает, что ее деструктор будет вызываться в конце области действия.
B
должен быть полным типом, когда создается деструктор для std::unique_ptr<B>
.
B
является неполным типом, поэтому мы не можем уничтожить b
.
Хорошо, поэтому вы не можете передать std::unique_ptr<B>
, если B
является неполным типом. Это ограничение имеет смысл. pimpl означает «указатель на реализацию». Для внешнего кода не имеет смысла обращаться к реализации A
, поэтому A::b
должен быть закрытым. Если вам нужен доступ к A::b
, тогда это не прыщ, это что-то еще.
Если вы действительно должны получить доступ к A::b
, сохраняя определение B
скрытым, то есть несколько обходных путей.
std::shared_ptr<B>
. Это полиморфно удаляет объект, так что B
не обязательно должен быть полным типом, когда создается деструктор std::shared_ptr<B>
. Это не так быстро, как std::unique_ptr<B>
, и я лично предпочитаю избегать std::shared_ptr
, за исключением случаев, когда это абсолютно необходимо.
std::unique_ptr<B, void(*)(B *)>
. Аналогично тому, как std::shared_ptr<B>
удаляет объект. Указатель на функцию передается в конструкции, которая отвечает за удаление. Это может привести к ненужным переносам указателя на функцию.
std::unique_ptr<B, DeleteB>
. Самое быстрое решение. Тем не менее, это, вероятно, немного раздражает, если у вас есть несколько классов pimpl (но не совсем pimpl), потому что вы не можете определить шаблон. Вот как бы вы это сделали:
// a.hpp
struct DeleteB {
void operator()(B *) const noexcept;
};
// a.cpp
void DeleteB::operator()(B *b) const noexcept {
delete b;
}
Определение пользовательского удалителя, вероятно, является наилучшим вариантом, но на вашем месте я бы нашел способ избежать необходимости доступа к деталям реализации извне класса.