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
Говард Хиннант,
1
С тех пор прыщ часто строится и не изменяется. Я обычно использую std :: shared_ptr <const window_impl>
mfnx
Связанный: я очень хотел бы знать, почему это работает в MSVC, и как препятствовать тому, чтобы это работало (так, чтобы я не ломал компиляции моих коллег GCC).
Лен

Ответы:

258

Вот несколько примеров 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 ) позволяет самому объявлению класса компилироваться, но объявление объекта этого типа в любом месте приводит к исходной ошибке («недопустимое применение sizeof» ... »).
Джефф Трулл
38
отличный ответ, просто на заметку; мы все еще можем использовать конструктор / деструктор по умолчанию, поместив, например, foo::~foo() = default;в файл src
сборка
2
Один из способов жить с конструкторами шаблонов - объявлять, но не определять конструктор в теле класса, определять его где-то, где будет видно полное определение impl, и явно создавать все необходимые экземпляры там.
энобайрам
2
Не могли бы вы объяснить, как это будет работать в некоторых случаях, а в других - нет? Я использовал идиому pimpl с unique_ptr и классом без деструктора, и в другом проекте мой код не компилируется с упомянутой ошибкой OP.
Любопытно
1
Кажется, если значение по умолчанию для unique_ptr установлено в {nullptr} в заголовочном файле класса со стилем c ++ 11, полное объявление также необходимо по вышеуказанной причине.
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;
}

Обратите внимание, что использование пользовательской функции Deleter исключает использование 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 помощник.

Тем не менее, это довольно неудобно и в некоторой степени противоречит самой сути проблемы. Гораздо более чистое решение, позволяющее избежать всего, что заключается в использовании пользовательского средства удаления и только перемещать его определение в исходный файл, где живет класс помощника pimple . Вот простой пример:

// 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();
  }
adspx5
источник
Это было и мое решение. Гораздо лаконичнее. Просто объявите ваш конструктор / деструктор в заголовке и определите в файле cpp.
Крис Морнесс
2

Чтобы добавить ответы других о пользовательском удалителе, в нашей внутренней «библиотеке утилит» я добавил вспомогательный заголовок для реализации этого общего шаблона (std::unique_ptr неполного типа, известного только некоторым TU, например, чтобы избежать длительного времени компиляции или обеспечить просто непрозрачная ручка для клиентов).

Он предоставляет общие условия для этого шаблона: пользовательский класс средства удаления, который вызывает внешнюю определенную функцию средства удаления, псевдоним типа для 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
Matteo Italia
источник
1

Может быть не лучшим решением, но иногда вы можете использовать shared_ptr . Если, конечно, это немного излишне, но ... что касается unique_ptr, я подожду еще 10 лет, пока разработчики стандартов C ++ не решат использовать лямбду в качестве средства удаления.

Другая сторона. По вашему коду может случиться так, что на этапе уничтожения window_impl будет неполным. Это может быть причиной неопределенного поведения. Посмотрите на это: почему действительно удаление неполного типа является неопределенным поведением?

Поэтому, если возможно, я бы определил очень базовый объект для всех ваших объектов с помощью виртуального деструктора. И ты почти хорош. Вы просто должны помнить, что система будет вызывать виртуальный деструктор для вашего указателя, поэтому вы должны определить его для каждого предка. Вам также следует определить базовый класс в разделе наследования как виртуальный ( подробности см. В этом разделе ).

Степан Дятьковский
источник