std :: unique_ptr з неповним типом не збирається


203

Я використовую ідіому pimpl з std::unique_ptr:

class window {
  window(const rectangle& rect);

private:
  class window_impl; // defined elsewhere
  std::unique_ptr<window_impl> impl_; // won't compile
};

Однак я отримую помилку компіляції щодо використання неповного типу в рядку 304 у <memory>:

Недійсне застосування " sizeof" до неповного типу " uixx::window::window_impl"

Наскільки я знаю, він std::unique_ptrповинен мати можливість використовувати неповний тип. Це помилка в libc ++ чи я роблю тут щось не так?


Посилання посилання на вимоги повноти: stackoverflow.com/a/6089065/576911
Howard Hinnant

1
Помпа часто будується і з тих пір не змінюється. Зазвичай я використовую std :: shared_ptr <const window_impl>
mfnx

Супутнє: Я дуже хотів би знати, чому це працює в MSVC, і як не допустити його роботи (щоб я не порушував збірки колег з GCC).
Лен

Відповіді:


260

Ось кілька прикладів std::unique_ptrіз неповними типами. Проблема полягає в руйнуванні.

Якщо ви використовуєте pimpl з unique_ptr, вам потрібно оголосити деструктор:

class foo
{ 
    class impl;
    std::unique_ptr<impl> impl_;

public:
    foo(); // You may need a def. constructor to be defined elsewhere

    ~foo(); // Implement (with {}, or with = default;) where impl is complete
};

оскільки в іншому випадку компілятор генерує типовий, і для цього йому потрібно повне оголошення foo::impl.

Якщо у вас є конструктори шаблонів, то вас накрутили, навіть якщо ви не конструюєте impl_член:

template <typename T>
foo::foo(T bar) 
{
    // Here the compiler needs to know how to
    // destroy impl_ in case an exception is
    // thrown !
}

У області простору імен використання unique_ptrтакож не буде працювати:

class impl;
std::unique_ptr<impl> impl_;

оскільки компілятор повинен знати тут, як знищити цей статичний об’єкт тривалості. Обхід:

class impl;
struct ptr_impl : std::unique_ptr<impl>
{
    ~ptr_impl(); // Implement (empty body) elsewhere
} impl_;

3
Я вважаю, що ваше перше рішення (додавання foo destructor) дозволяє самому оголошенню класу збирати, але оголошення об'єкта цього типу де завгодно призводить до початкової помилки ("недійсне застосування 'sizeof' ...").
Джефф Трулл

38
відмінна відповідь, просто зазначити; ми все ще можемо використовувати конструктор / деструктор за замовчуванням, розмістивши, наприклад, foo::~foo() = default;у файлі src
збираємося

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

2
Чи можете ви пояснити, як це діятиме в одних випадках, а не в інших? Я використав ідіому pimpl з унікальним_ptr та класом без деструктора, а в іншому проекті мій код не вдасться компілювати із згаданою помилкою ОП.
Цікаво

1
Здається, якщо для файлу заголовка класу зі стилем c ++ 11 за замовчуванням встановлено значення {default_ptr} {nullptr}, повне оголошення також потрібно з вищевказаної причини.
feirainy

53

Як зазначав Олександр К. , проблема зводиться до того window, що деструктор неявно визначається в місцях, де тип window_implще є неповним. Окрім його рішень, ще одне вирішення, яке я використав, - оголосити функціонера Deleter у заголовку:

// Foo.h

class FooImpl;
struct FooImplDeleter
{
  void operator()(FooImpl *p);
};

class Foo
{
...
private:
  std::unique_ptr<FooImpl, FooImplDeleter> impl_;
};

// Foo.cpp

...
void FooImplDeleter::operator()(FooImpl *p)
{
  delete p;
}

Зауважте, що використання спеціальної функції «Видалення» виключає використання std::make_unique(доступне на C ++ 14), про що вже говорилося тут .


6
Що стосується мене, це правильне рішення. Це не унікально для використання pimpl-idiom, це загальна проблема із використанням std :: unique_ptr з неповними класами. Делетер за замовчуванням, який використовується std :: unique_ptr <X>, намагається зробити "видалити X", чого він не може зробити, якщо X - пряма декларація. Вказавши функцію видалення, ви можете помістити цю функцію у вихідний файл, де клас X повністю визначений. Інші вихідні файли можуть використовувати std :: unique_ptr <X, DeleterFunc>, хоча X - це лише пряма заява, якщо вони пов'язані з вихідним файлом, що містить DeleterFunc.
притулок

1
Це хороший спосіб вирішення, коли ви повинні мати вбудоване визначення функції, яке створює екземпляр типу "Foo" (наприклад, статичний метод "getInstance", на який посилаються конструктор і деструктор), і ви не хочете переміщувати їх у файл реалізації як пропонує @ adspx5.
GameSalutes

20

використовувати спеціальний делетер

Проблема полягає в тому, що він unique_ptr<T>повинен викликати деструктора T::~T()у власному деструкторі, його операторі призначення присвоєння руху та unique_ptr::reset()функції члена (лише). Однак їх потрібно викликати (неявно або явно) у кількох ситуаціях PIMPL (вже у деструкторі зовнішнього класу та операторі призначення присвоєння).

Як вже зазначалося в іншу відповідь, один з способів уникнути цього, щоб перемістити всі операції, що вимагають unique_ptr::~unique_ptr(), unique_ptr::operator=(unique_ptr&&)іunique_ptr::reset() в вихідний файл , в якому фактично визначено клас Pimpl помічник.

Однак це досить незручно і певною мірою заперечує саму точку pimpl idoim. Набагато більш чисте рішення, яке дозволяє уникнути всього, що полягає у використанні користувальницького делетера, і лише перемістити його визначення у вихідний файл, де живе клас помічника прищів. Ось простий приклад:

// file.h
class foo
{
  struct pimpl;
  struct pimpl_deleter { void operator()(pimpl*) const; };
  std::unique_ptr<pimpl,pimpl_deleter> m_pimpl;
public:
  foo(some data);
  foo(foo&&) = default;             // no need to define this in file.cc
  foo&operator=(foo&&) = default;   // no need to define this in file.cc
//foo::~foo()          auto-generated: no need to define this in file.cc
};

// file.cc
struct foo::pimpl
{
  // lots of complicated code
};
void foo::pimpl_deleter::operator()(foo::pimpl*ptr) const { delete ptr; }

Замість окремого класу делетерів ви також можете використовувати вільну функцію або staticчлен fooу поєднанні з лямбда:

class foo {
  struct pimpl;
  static void delete_pimpl(pimpl*);
  std::unique_ptr<pimpl,[](pimpl*ptr){delete_pimpl(ptr);}> m_pimpl;
};

15

Можливо, у вас є деякі функціональні органи в .h-файлі в класі, який використовує неповний тип.

Переконайтеся, що у вашому .h для вікна класу у вас є лише декларація функції. Усі функціональні органи для вікна повинні бути у файлі .cpp. І для window_impl також ...

До речі, ви повинні явно додати декларацію деструктора для класу Windows у свій .h файл.

Але ви НЕ МОЖЕТЕ помістити порожній корпус dtor у файл заголовка:

class window {
    virtual ~window() {};
  }

Повинно бути лише декларацією:

  class window {
    virtual ~window();
  }

Це було і моє рішення. Шлях більш лаконічний. Просто замовте свій конструктор / деструктор у заголовку та визначено у файлі cpp.
Кріс Морнес

2

Щоб додати відповіді інших про користувацький делетер, у нашу внутрішню "бібліотеку утиліт" я додав помічник заголовка для реалізації цієї загальної моделі (std::unique_ptr неповного типу, відомого лише деякому з ТУ, наприклад, щоб уникнути тривалого часу компіляції або надати просто непрозора ручка для клієнтів).

Він надає загальні риштування цього шаблону: користувацький клас делетерів, який викликає функцію делетора, визначений зовні, тип псевдоніму для unique_ptrцього класу видалення та макрос для оголошення функції делетора в TU, який має повне визначення тип. Я думаю, що це має певну загальну корисність, тому ось це:

#ifndef CZU_UNIQUE_OPAQUE_HPP
#define CZU_UNIQUE_OPAQUE_HPP
#include <memory>

/**
    Helper to define a `std::unique_ptr` that works just with a forward
    declaration

    The "regular" `std::unique_ptr<T>` requires the full definition of `T` to be
    available, as it has to emit calls to `delete` in every TU that may use it.

    A workaround to this problem is to have a `std::unique_ptr` with a custom
    deleter, which is defined in a TU that knows the full definition of `T`.

    This header standardizes and generalizes this trick. The usage is quite
    simple:

    - everywhere you would have used `std::unique_ptr<T>`, use
      `czu::unique_opaque<T>`; it will work just fine with `T` being a forward
      declaration;
    - in a TU that knows the full definition of `T`, at top level invoke the
      macro `CZU_DEFINE_OPAQUE_DELETER`; it will define the custom deleter used
      by `czu::unique_opaque<T>`
*/

namespace czu {
template<typename T>
struct opaque_deleter {
    void operator()(T *it) {
        void opaque_deleter_hook(T *);
        opaque_deleter_hook(it);
    }
};

template<typename T>
using unique_opaque = std::unique_ptr<T, opaque_deleter<T>>;
}

/// Call at top level in a C++ file to enable type %T to be used in an %unique_opaque<T>
#define CZU_DEFINE_OPAQUE_DELETER(T) namespace czu { void opaque_deleter_hook(T *it) { delete it; } }

#endif

1

Можливо, це не найкраще рішення, але іноді ви можете використовувати натомість shared_ptr . Якщо, звичайно, це надмірна кількість, але ... що стосується unique_ptr, я, можливо, зачекаю ще 10 років, поки виробники стандартів C ++ вирішать використовувати лямбда як делетер.

Ще одна сторона. Згідно з вашим кодом, може статися, що на етапі знищення window_impl буде неповним. Це може бути причиною невизначеної поведінки. Дивіться це: Чому, дійсно, видалення незавершеного типу є невизначеною поведінкою?

Отже, якщо можливо, я б визначив дуже базовий об'єкт для всіх ваших об'єктів, з віртуальним деструктором. І ти майже хороший. Вам просто слід пам’ятати, що система викличе віртуальний деструктор для вашого вказівника, тому ви повинні визначити його для кожного предка. Ви також повинні визначити базовий клас у розділі спадкування як віртуальний (детальніше див. Це ).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.