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, которые в настоящее время рассматриваются основной группой языков:

чтобы он не удивлялся, что здесь есть путаница.