Почему unique_ptr требует полного типа в конструкторе?

От Нужно ли std::unique_ptr знать полное определение T?, я знаю, что если класс A имеет член unique_ptr<T>, то T должен быть полным типом в деструкторе ~A(). Однако я столкнулся с ситуацией, когда конструктор A() также требует полного типа T, см. код ниже:

// a.h -------------------------------
#pragma once
#include <memory>
struct B;
struct A {
  A(); // <---
  ~A();
  std::unique_ptr<B> ptr;
};

// a.cpp -------------------------------
#include "a.h"
struct B {};
A::A() = default; // <---
A::~A() = default;

// main.cpp -------------------------------
#include "a.h"
int main() {A a;}

Если определение конструктора A::A() переместить в заголовок a.h, компилятор будет жаловаться error: invalid application of ‘sizeof’ to incomplete type ‘B’. Почему это происходит? Есть ли какой-нибудь справочный материал по этому поводу?

Кстати, я использую gcc-7.5.0 в Ubuntu 18.04 с включенным С++ 17.


Отредактируйте @463035818_is_not_a_number в комментариях. Полное сообщение об ошибке:

[1/2] Building CXX object CMakeFiles/t.dir/main.cpp.o
FAILED: CMakeFiles/t.dir/main.cpp.o 
/usr/bin/c++   -g -fdiagnostics-color=always -std=gnu++1z -MD -MT CMakeFiles/t.dir/main.cpp.o -MF CMakeFiles/t.dir/main.cpp.o.d -o CMakeFiles/t.dir/main.cpp.o -c /home/user/Tests/UniquePtrTest/main.cpp
In file included from /usr/include/c++/7/memory:80:0,
                 from /home/user/Tests/UniquePtrTest/a.h:2,
                 from /home/user/Tests/UniquePtrTest/main.cpp:1:
/usr/include/c++/7/bits/unique_ptr.h: In instantiation of ‘void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = B]’:
/usr/include/c++/7/bits/unique_ptr.h:263:17:   required from ‘std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = B; _Dp = std::default_delete<B>]’
/home/user/Tests/UniquePtrTest/a.h:5:3:   required from here
/usr/include/c++/7/bits/unique_ptr.h:76:22: error: invalid application of ‘sizeof’ to incomplete type ‘B’
  static_assert(sizeof(_Tp)>0,
                      ^
ninja: build stopped: subcommand failed.

Конструктор должен знать, как уничтожить все поля на случай, если в какой-то момент во время построения возникнет исключение.

Matteo Italia 10.02.2023 09:43

@MatteoItalia похоже, что ваше объяснение должно относиться к использованию того же A() = default в объявлении класса (в файле h). Но компилятор не выдает ошибки, если вы делаете это так.

wohlstad 10.02.2023 09:48

@wohlstad - Скомпилируйте как C++ 20, тогда вы не будете иметь дело с глупым агрегатом.

StoryTeller - Unslander Monica 10.02.2023 09:48

Кстати, #include в основном просто заменяет текст, поэтому должна быть возможность воспроизвести его с кодом в одном файле. Было бы проще воспроизводить для других

463035818_is_not_a_number 10.02.2023 09:50

@StoryTeller-Unslander Моника, я не понимаю. Я имел в виду определение A ctor как =default. Это работает в объявлении класса (A() = default;), но не снаружи, как A::A() = default;. В обоих случаях я удалил строку struct B {};.

wohlstad 10.02.2023 09:51

@wohlstad - Что следует за «скомпилировать как C++ 20»?

StoryTeller - Unslander Monica 10.02.2023 09:52

@StoryTeller-UnslanderMonica, вы упомянули «глупый агрегат». Я не понял, как это связано. Кстати, я компилирую как C++20.

wohlstad 10.02.2023 09:55

@wohlstad - Интересно. struct A { A() = default; }; — это совокупность до c++20, а не позже. Это повлияет на то, что на самом деле это не тело c'tor, на котором можно ошибиться. Я предполагаю, что в вашем тестировании есть что-то еще.

StoryTeller - Unslander Monica 10.02.2023 09:59

Теперь я понимаю ваше упоминание агрегата. Однако - мой тест минимален - похож на код OP, и я получаю такое же поведение, когда компилирую как C++ 17, так и C++ 20. Я использую VS2022 версии 17.2.5.

wohlstad 10.02.2023 10:04

@ 463035818_is_not_a_number Извините, я не могу воспроизвести его с кодом в одном файле. Не могли бы вы привести пример?

VictorBian 10.02.2023 10:09

Смотрите также stackoverflow.com/questions/71821115/…

Passer By 10.02.2023 10:11

У меня тоже не получилось воспроизвести. Признаюсь, я не понимаю, должно быть это как-то связано с тем, что A a; находится в другой единице перевода, я думаю, если вы удалите это из main, ошибка также исчезнет

463035818_is_not_a_number 10.02.2023 10:18

@ 463035818_is_not_a_number Да, разные единицы перевода имеют значение.

VictorBian 10.02.2023 10:27

хм, тогда я бы попросил вас включить в вопрос полное сообщение об ошибке. Кажется, что ошибка из main, но это не ясно из вопроса.

463035818_is_not_a_number 10.02.2023 10:29

@StoryTeller-UnslanderMonica Я разместил этот вопрос по поводу проблемы, о которой я упоминал выше: stackoverflow.com/questions/75409177/….

wohlstad 10.02.2023 10:33
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
15
162
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Проблема в том, что A::A() нужно знать, как уничтожить ptr на случай, если конструктор бросит вызов.

Пример:

#include <memory>
struct B {};

struct X{
    X(){throw 42;}
};

struct A {
  A() {}
  ~A() {};
  std::unique_ptr<B> ptr;
  X x;
};


int main() {
    A a;
}

генерирует:

A::A() [base object constructor]:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 24
        mov     QWORD PTR [rbp-24], rdi
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax
        call    std::unique_ptr<B, std::default_delete<B> >::unique_ptr<std::default_delete<B>, void>()
        mov     rax, QWORD PTR [rbp-24]
        add     rax, 8
        mov     rdi, rax
        call    X::X() [complete object constructor]
        jmp     .L6
        mov     rbx, rax
        mov     rax, QWORD PTR [rbp-24]
        mov     rdi, rax
        call    std::unique_ptr<B, std::default_delete<B> >::~unique_ptr() [complete object destructor]
        mov     rax, rbx
        mov     rdi, rax
        call    _Unwind_Resume

показывает вызов std::unique_ptr<B, std::default_delete<B> >::~unique_ptr().

Есть ли какой-нибудь справочный материал по этому поводу?

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

На практике не так много, так как вам нужно прочитать Стандарт, который определяет, какие функции/выражения требуют полного типа.

Конечно cppreference качественная и реально читабельная, хотя такого варианта использования я там не нашел.

В частности, этот вопрос упоминается в заметке 20.11.1.3.3 Деструктор [unique.ptr.single.dtor]

[Примечание 1: использование default_delete требует, чтобы T был полным типом. — примечание в конце]

Спасибо. Кажется, проблема в моем случае заключается в следующем: если A::A() определено в a.h, ctor не будет скомпилирован в a.o. Затем main.o компилирует A::A() из шапки, а B здесь неполный тип.

VictorBian 10.02.2023 11:06

@VictorBian Да, именно потому, что тогда определение встроено в заголовок с неполным B. Извините, надо было сказать это прямо. Если бы у вас было main внутри a.cpp, я думаю, это сработало бы.

Quimby 10.02.2023 11:15

Ошибка, которую вы видите, std::default_deleter защищает вас от неопределенного поведения.

Когда вы создаете экземпляр определения конструктора std::unique_ptr<B>::unique_ptr, также создается определение std::default_delete<B>::operator(). Внутри которого находится утверждение

static_assert(sizeof(B) > 0);

который проверяет неполные типы. Это предотвращает любое возможное удаление неполного типа, что является неопределенным поведением. См. также неполные типы с shared_ptr и unique_ptr.

Но почему перемещение определения A::A() в заголовок вызывает ошибку, а не в файле реализации?

Как оказалось, простое объявление члена std::unique_ptr<B> создает только объявление его конструктора, но не определение. Следовательно, если A::A() определено в файле реализации, определение std::default_delete<B>::operator() также создается только тогда, когда B является полным типом.

Спасибо за Ваш ответ. Я думаю, что вы и @Quimby вместе ответили на мой вопрос.

VictorBian 10.02.2023 11:09

Другие вопросы по теме

Auto open_flags = std::ios::binary; кажется, создает неправильный тип в MSVC. Это ошибка?
Проблема со списком инициализаторов в конструкторе
Почему в С++ есть это правило: явные определения создания экземпляров игнорируют спецификаторы доступа к членам: типы параметров и возвращаемые типы могут быть закрытыми
Модель памяти «получить-освободить» для двух последовательных атомарных операций
Стандарт С++ для смещений элементов стандартной структуры макета
Почему в этом примере используется «2.» вместо «2.0» (десятичная точка без десятичных знаков)?
Используйте лямбда через ссылку std::function, вы не можете изменить значение в лямбда
Виртуальный метод в С++ не высмеивается. Что мне здесь не хватает?
=default и =delete - это объявление функции или определение функции?
Почему попытка напечатать строки в кодировке Unicode с помощью cout приводит к ошибке компиляции в новых стандартах C++?