Почему построение std:: unique_ptr для компиляции неполного типа?

код:

#include <memory>

struct Data;
std::unique_ptr<Data> make_me();

int main()
{
    std::unique_ptr<Data> m = make_me();
    return 0;
}

Что, конечно, не удается:

In file included from <source>:1:
In file included from /opt/compiler-explorer/gcc-7.1.0/lib/gcc/x86_64-linux-gnu/7.1.0/../../../../include/c++/7.1.0/memory:80:
/opt/compiler-explorer/gcc-7.1.0/include/c++/7.1.0/bits/unique_ptr.h:76:16: error: invalid application of 'sizeof' to an incomplete type 'Data'
        static_assert(sizeof(_Tp)>0,
                      ^~~~~~~~~~~
/opt/compiler-explorer/gcc-7.1.0/include/c++/7.1.0/bits/unique_ptr.h:268:4: note: in instantiation of member function 'std::default_delete<Data>::operator()' requested here
          get_deleter()(__ptr);
          ^
8 : <source>:8:31: note: in instantiation of member function 'std::unique_ptr<Data, std::default_delete<Data> >::~unique_ptr' requested here
    std::unique_ptr<Data> m = make_me();
                              ^
3 : <source>:3:8: note: forward declaration of 'Data'
struct Data;
       ^
1 error generated.
Compiler returned: 1

Но добавление ниже строки в конце выше кода компилируется отлично:

struct Data {};

Мой вопрос в том, почему этот код компилируется и работает, когда Data объявляется после момента создания std:: unique_ptr? По-видимому, оба случая должны терпеть неудачу с той же/подобной ошибкой.

Весь пример на godbolt: https://godbolt.org/g/FQqxwN

Ответы

Ответ 1

Это работает, потому что точка создания template находится после определения Data. Из стандарта:

[temp.point]

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

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

Обратите внимание, что это, вероятно, плохо сформировано (NDR) из-за последнего предложения в цитате. Я недостаточно уверен, чтобы сказать, что это, безусловно, плохо сформировано или нет.

Ответ 2

Если вы читаете немного ближе, проблема заключается в удалении содержащегося объекта Data.

get_deleter()(__ptr)

часть - это большой намек.

Что происходит здесь, так это то, что уникальный объект-указатель m выходит за пределы области действия в конце функции main, поэтому данные, указывающие на необходимость удаления. Однако, поскольку деструктор не имеет деструктора, он не может обработать его.

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

auto null_deleter = [](Data*){ /* Do nothing */ };
...
std::unique_ptr<Data, decltype(null_deleter)> m = make_me();

Конечно, если вы хотите фактически удалить данные, то либо определите структуру, либо измените делектор, чтобы он delete указатель (что в любом случае требует полного определения структуры, но тогда делетер может быть определен в другая единица траннасляции, вероятно, такая же, где make_me определена).