unique_ptr и прямое объявление: правильный способ кодирования фабричной функции - PullRequest
3 голосов
/ 20 апреля 2019

Недавно узнал, умные ptrs, я пытаюсь написать фабричную функцию, которая возвращает unique_ptrs.Прочитав несколько статей о том, как поместить время создания вместе с явно определенными ctor и dtor в один и тот же файл cpp, я подумал, что смогу сделать это:

// factory.hpp

struct Foo;

std::unique_ptr<Foo> create();
// foo.cpp

struct Foo {
    Foo();
    ~Foo();
    Foo(const Foo &);
    Foo(Foo &&);
};

std::unique_ptr<Foo> create() {
    return make_unique<Foo>();
}
#include "factory.hpp"


int main() {
    auto r = create();
    return 0;
}

Но я получаюОшибка неполного типа.Затем, после нескольких часов веб-поиска и экспериментов, я понимаю, что даже не могу этого сделать:

Вот классическая идиома Pimpl unique_ptr.

// A.hpp

struct B;

struct A {
    A();
    ~A();
    unique_ptr<B> b;
};
// A.cpp

struct B {};

A::A() = default;

A::~A() = default;

#include "A.hpp"


int main() {
    A a;   // this is fine since we are doing the Pimpl correctly.

    // Now, I can't do this.
    auto b = std::move(a.b);   // <--- Can't do this.

    return 0;
}

Дляради обсуждения, пожалуйста, игнорируйте тот факт, что строка std::move не имеет смысла.Я получил ту же ошибку неполного типа.

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

  1. Убирать неполный тип - это УБ.Вот почему запрещено создавать unique_ptrs с незавершенными типами с использованием средств удаления по умолчанию.
  2. Если я использую пользовательские средства удаления, я должен быть в состоянии это сделать.
  3. Я предполагаю, так как используюв моем случае, по умолчанию, я не могу сделать это по какой-то причине, я не совсем уверен в этом.

Явное определение функций создания и уничтожения должно помочь.Но для меня это безобразно.Например, удалители по умолчанию в моем случае.С другой стороны, мне кажется, что я не могу использовать лямбду для разрушителя, так как тип лямбды известен только компилятору, и я не могу сделать объявление фабричной функции с помощью decltype.

Итак, мои вопросы:

  1. В чем причина этого сбоя?
  2. Как правильно написать фабричные функции, которые возвращают unique_ptrs?

Пожалуйста, исправьтеЯ, если что-то, что я сказал, неправильно.Любые указатели будут оценены.

1 Ответ

2 голосов
/ 20 апреля 2019

Когда компилятор создает деструктор 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);
}

Что именно здесь происходит?

  1. Мы создаем std::unique_ptr<B> для инициализации b.
  2. b - локальная переменная, которая означает, что ее деструктор будет вызываться в конце области действия.
  3. B должен быть полным типом, когда создается деструктор для std::unique_ptr<B>.
  4. 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;
}

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

...