От Нужно ли 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.
@MatteoItalia похоже, что ваше объяснение должно относиться к использованию того же A() = default в объявлении класса (в файле h). Но компилятор не выдает ошибки, если вы делаете это так.
@wohlstad - Скомпилируйте как C++ 20, тогда вы не будете иметь дело с глупым агрегатом.
Кстати, #include
в основном просто заменяет текст, поэтому должна быть возможность воспроизвести его с кодом в одном файле. Было бы проще воспроизводить для других
@StoryTeller-Unslander Моника, я не понимаю. Я имел в виду определение A
ctor как =default
. Это работает в объявлении класса (A() = default;
), но не снаружи, как A::A() = default;
. В обоих случаях я удалил строку struct B {};
.
@wohlstad - Что следует за «скомпилировать как C++ 20»?
@StoryTeller-UnslanderMonica, вы упомянули «глупый агрегат». Я не понял, как это связано. Кстати, я компилирую как C++20.
@wohlstad - Интересно. struct A { A() = default; };
— это совокупность до c++20, а не позже. Это повлияет на то, что на самом деле это не тело c'tor, на котором можно ошибиться. Я предполагаю, что в вашем тестировании есть что-то еще.
Теперь я понимаю ваше упоминание агрегата. Однако - мой тест минимален - похож на код OP, и я получаю такое же поведение, когда компилирую как C++ 17, так и C++ 20. Я использую VS2022 версии 17.2.5.
@ 463035818_is_not_a_number Извините, я не могу воспроизвести его с кодом в одном файле. Не могли бы вы привести пример?
Смотрите также stackoverflow.com/questions/71821115/…
У меня тоже не получилось воспроизвести. Признаюсь, я не понимаю, должно быть это как-то связано с тем, что A a;
находится в другой единице перевода, я думаю, если вы удалите это из main
, ошибка также исчезнет
@ 463035818_is_not_a_number Да, разные единицы перевода имеют значение.
хм, тогда я бы попросил вас включить в вопрос полное сообщение об ошибке. Кажется, что ошибка из main, но это не ясно из вопроса.
@StoryTeller-UnslanderMonica Я разместил этот вопрос по поводу проблемы, о которой я упоминал выше: stackoverflow.com/questions/75409177/….
Проблема в том, что 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 Да, именно потому, что тогда определение встроено в заголовок с неполным B
. Извините, надо было сказать это прямо. Если бы у вас было main
внутри a.cpp
, я думаю, это сработало бы.
Ошибка, которую вы видите, 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 вместе ответили на мой вопрос.
Конструктор должен знать, как уничтожить все поля на случай, если в какой-то момент во время построения возникнет исключение.