Pimpl - Почему make_unique можно вызвать на неполном типе?

Почему компиляция make_unique компилируется? Не make_unqiue требует, чтобы его аргумент шаблона был полным типом?

struct F;
int main()
{
  std::make_unique<F>();
}

struct F {};

Вопрос, который был задан из моей "проблемы" с моей реализацией PIMPL:

Я понимаю, почему деструктор должен быть объявлен и определен пользователем внутри файла cpp для класса реализации (PIMPL).

Но перемещение конструктора класса, содержащего pimpl-, еще компилируется.

class Object
{};

class CachedObjectFactory
{
public:
  CachedObjectFactory();

  ~CachedObjectFactory();
  std::shared_ptr<Object> create(int id) const;

private:
  struct CacheImpl;
  std::unique_ptr<CacheImpl> pImpl;
};

Теперь файл cpp:

// constructor with make_unique on incompete type ?
CachedObjectFactory::CachedObjectFactory()
  : pImpl(std::make_unique<CacheImpl>())
{}

struct CachedObjectFactory::CacheImpl
{
  std::map<int, std::shared_ptr<Object>> idToObjects;
};

//deferred destructor
CachedObjectFactory::~CachedObjectFactory() = default;

Может кто-нибудь объяснить, почему это компилируется? Почему существует разница между строительством и разрушением? Если создание деструктора и создание экземпляра default_deleter является проблемой, почему создание make_unique не является проблемой?

Ответ 1

make_unique имеет несколько точек инстанцирования: конец единицы перевода также является точкой инстанцирования. То, что вы видите, это компилятор, make_unique экземпляр make_unique после CacheImpl/F Составителям разрешено это делать. Ваш код плохо сформирован, если вы полагаетесь на него, и компиляторы не обязаны обнаруживать ошибку.

14.6.4.1 Точка инстанцирования [temp.point]

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

Ответ 2

Причина, по которой этот компилятор находится здесь, в [temp.point] ¶8:

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

Обратите внимание на окончание этой цитаты, так как мы перейдем к ней в приведенном ниже редактировании, но на данный момент, что происходит на практике по фрагменту OP, компилятор использует дополнительно рассмотренную точку создания make_unique() которая помещается в конец блок перевода, так что он будет иметь определения, которые отсутствуют в исходной точке использования в коде. Это разрешено в соответствии с этим пунктом из спецификации.

Обратите внимание, что это больше не компилируется:

struct Foo; int main(){ std::make_unique<Foo>(); } struct Foo { ~Foo() = delete; };

Как и в случае, компилятор не пропустит точку инстанцирования, он только отбрасывает его с точки зрения того, в какой точке блока перевода он использует для генерации кода для шаблона.


Изменение: Наконец, кажется, что даже если у вас есть несколько точек создания, это не означает, что поведение определяется, если определение отличается между этими точками. Обратите внимание на последнее предложение в приведенной выше цитате, согласно которой это различие определено Правилом одного определения. Это взято прямо из моего комментария к ответу @hvd, который осведомил это здесь: см. Здесь в правиле одного определения:

Каждая программа должна содержать ровно одно определение каждой не-встроенной функции или переменной, которая не используется в этой программе вне отбрасываемого оператора; не требуется диагностика. Определение может явно отображаться в программе, оно может быть найдено в стандартной или определяемой пользователем библиотеке или...

И так, в случае OP, это, очевидно, разница между двумя точками создания, в том, что, как отметил сам @hvd, первый из них имеет неполный тип, а второй - нет. Действительно, эта разница составляет два разных определения, и поэтому очень мало сомнений в том, что эта программа плохо сформирована.