Unique_ptr, объявление pimpl/forward и полное определение
Я уже проверил вопросы здесь и здесь, но до сих пор не могу понять, что не так.
Это код вызова:
#include "lib.h"
using namespace lib;
int
main(const int argc, const char *argv[])
{
return 0;
}
Это код lib:
#ifndef lib_h
#define lib_h
#include <string>
#include <vector>
#include <memory>
namespace lib
{
class Foo_impl;
class Foo
{
public:
Foo();
~Foo();
private:
Foo(const Foo&);
Foo& operator=(const Foo&);
std::unique_ptr<Foo_impl> m_impl = nullptr;
friend class Foo_impl;
};
} // namespace
#endif
clang++ дает мне эту ошибку:
Недействительное приложение 'sizeof' для неполного типа 'lib:: Foo_impl'
примечание: при создании функции-члена 'std:: default_delete:: operator()' запрошен
Вы можете видеть, что я уже специально объявил Foo destructor. Что еще мне здесь не хватает?
Ответы
Ответ 1
Реализация Foo_impl
должна быть завершена до создания экземпляра, требуемого в std::unique_ptr<Foo_impl> m_impl = nullptr
.
Оставив объявленный тип (но не инициализированный), исправит ошибку (std::unique_ptr<Foo_impl> m_impl;
), вам тогда потребуется инициализировать ее позже в коде.
Ошибка, которую вы видите, - это реализация метода, используемого для проверки этого; неполный тип. В принципе, sizeof
приведет к ошибке с типами, которые только объявлены вперед (т.е. Отсутствие определения при использовании в этой точке кода/компиляции).
Возможное исправление здесь будет выглядеть следующим образом:
class Foo_impl;
class Foo
{
// redacted
public:
Foo();
~Foo();
private:
Foo(const Foo&);
Foo& operator=(const Foo&);
std::unique_ptr<Foo_impl> m_impl;// = nullptr;
};
class Foo_impl {
// ...
};
Foo::Foo() : m_impl(nullptr)
{
}
Почему требуется полный тип?
В экземпляре через = nullptr
используется инициализация копирования и требуется объявить конструктор и деструктор (для unique_ptr<Foo_impl>
). Деструктор требует функцию делетера unique_ptr
, которая по умолчанию вызывает delete
на указателе на Foo_impl
, поэтому для этого требуется деструктор Foo_impl
, а деструктор Foo_impl
не объявлен в неполном type (компилятор не знает, как это выглядит). См. Howard answer об этом.
Ключевым моментом здесь является то, что вызов delete
по неполному типу приводит к поведению undefined (§ 5.3.5/5) и, следовательно, явно проверяется в реализации unique_ptr
.
Другой альтернативой этой ситуации может быть использование прямой инициализации следующим образом:
std::unique_ptr<Foo_impl> m_impl { nullptr };
Похоже, что существуют некоторые дискуссии о инициализаторе элементов нестатических данных (NSDMI) << → и является ли это контекстом, который требует определения члена, по крайней мере для clang (и, возможно, gcc), это, кажется, такой контекст.
Ответ 2
Заявление:
std::unique_ptr<Foo_impl> m_impl = nullptr;
вызывает инициализацию копирования. Это имеет ту же семантику, что и:
std::unique_ptr<Foo_impl> m_impl = std::unique_ptr<Foo_impl>(nullptr);
т.е. он создает временную prvalue. Это временное присвоение должно быть разрушено. И этот деструктор должен видеть полный тип Foo_impl
. Даже если исключить конструкцию prvalue и move, компилятор должен вести себя "как если бы".
Вместо этого вы можете использовать прямую инициализацию, а деструктор unique_ptr
больше не понадобится на данный момент:
std::unique_ptr<Foo_impl> m_impl{nullptr};
Обновление
Casey указывает, что gcc-4.9 в настоящее время создает ~unique_ptr()
даже для формы с прямой инициализацией. Однако в моих тестах clang не делает. Я не знаю, что могут сделать другие компиляторы. Я считаю, что в этом отношении clang соответствует, по крайней мере, с последними отчетами о дефектах ядра, которые были учтены.
Ответ 3
Заменить
std::unique_ptr<Foo_impl> m_impl = nullptr;
с
std::unique_ptr<Foo_impl> m_impl;
чтобы исправить ошибку.
Ответ 4
N3936 [temp.inst]/2 состояния:
Если член шаблона класса или шаблон-член явно не создан или явно не специализирован, специализация этого элемента неявно создается, когда специализация ссылается в контексте, который требует определения члена; в частности, инициализация (и любые связанные с ней побочные эффекты) статического члена данных не происходит, если сам статический член данных не используется таким образом, чтобы требовалось определение члена статического данных.
Таким образом, этот вопрос действительно сводится к тому, является ли объявление с нестационарным инициализатором элемента данных (NSDMI) "контекстом, требующим определения члена", в отношении деструктора этого типа элемента. Хотя ясно, что объявления конструктора типов немедленно необходимы для определения того, является ли NSDMI подходящим типом для инициализации элемента, я бы сказал, что определения конструктора/деструктора требуются только конструктором/деструктором охватывающего типа и что реализации не соответствуют требованиям.
Тем не менее, существует несколько проблем с семантикой NSDMI, которые в настоящее время рассматриваются основной группой языков:
чтобы он не удивлялся, что здесь есть путаница.