mimik написав:А, що за причини? Можна детальніше або де про це можна почитати?
Ну, почнімо з того, що delete для вказівника на неповний тип - UB.
Деструктор за замовчуванням ~Test() генерує виклик деструктора ~std::unique_ptr<Implementation>, який містить delete для неповного типу Implementation.
Тепер, чому з = default це працює:
http://eel.is/c++draft/class.dtor#10 написав:A destructor that is defaulted and not defined as deleted is implicitly defined when it is odr-used or when it is explicitly defaulted after its first declaration.
Тобто, якщо у вас дефолтний деструктор, то він буде визначений лише при використанні ODR, тобто при знищенні об'єкта Test. У вашому фрагменті коду жоден об’єкт такого типу ніколи не знищується, і, отже, програма компілюється - оскільки той хто видаляє, а це unique_ptr насправді ніде не викликається. Він може бути викликаний лише деструктором Test, який не визначений у вашому випадку.
Коли ви зробите деструктор, як user-defined (тобто додасте йому тіло {}), то він визначиться на місці, і компілятор видасть своє "фе", оскільки ви не можете знищити об’єкт unique_ptr неповного типу.
До речі, оголошення деструктора, але не визначення (~Test();), не дасть помилки компіляції з тієї ж причини.
-------------------------------------------
Але це лише наслідок, а не причина.
Мені вже ліньки ритись в макулатурі стандарті, але як ви знаєте, формально "точкою визначення" такого деструктора буде самий кінець одиниці трансляції. Тобто для того, щоб код скомпілювати, не обов'язково надавати повне визначення Implementation саме вище за кодом. Достатньо надати його де завгодно в тих одиницях трансляції, що включають визначення Test. І, якщо підправити ваш код, згідно слів вище, то все запрацює:
class Implementation;
class Test {
public:
Test() = default;
virtual ~Test() = default;
private:
std::unique_ptr<Implementation> m_pImpl;
};
int main() {
Test t;
}
class Implementation {};
І саме тому, такий код працює валідно теж:
class Implementation;
template <typename T> void foo();
int main() {
foo<Implementation>();
}
class Implementation {};
template <typename T> void foo() {
T x;
}
// Ось це компілятор добавить в кінець файлу неявно.
// template<>
// void foo<Implementation>() {
// Implementation x = Implementation();
// }
І зроблено це спеціально для того, щоб дати компіляторам свободу просто ні про що не турбуючись інстанціювати всі шаблони в кінці файлу. Але при цьому потрібно, щоб в усіх точках інстанціювання, реальних і гіпотетичних, шаблон мав одну і ту ж інтерпретацію. В іншому випадку: ill-formed, no diagnostic required.