Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?

Smart pointer z typem, który jest zadeklarowany ale nie zdefiniowany

Ostatnio zmodyfikowano 2022-08-17 22:14
Autor Wiadomość
Kyriet
Temat założony przez niniejszego użytkownika
Smart pointer z typem, który jest zadeklarowany ale nie zdefiniowany
» 2022-08-13 23:42:05
Cześć, nie rozumiem zachowania std::unique_ptr w moim kodzie. Niżej podaję Minimalny Reprodukujący Przykład:

C/C++
// main.cpp
#include "Number.hpp"

int main()
{
   
return 0;
}
C/C++
// Number.hpp
#pragma once

#include "Helper.hpp"

struct Number { };
C/C++
// Helper.hpp
#pragma once

#include <memory>

struct Number;

struct Helper
{
   
std::unique_ptr < Number > ptr;
};
C/C++
// Helper.cpp
#include "Helper.hpp"

Proszę nie oceniać sensowności tego circular dependency. Kod, który podałem powyżej kompiluje się. Oznacza to, że forward-deklaracja
struct Number;
 w pliku Helper.hpp wystarczyła.

Teraz moje pytanie 1) Dlaczego dodanie konstruktora kontruktora (innego niż
= default
) powoduje błąd kompilacji?

C/C++
// Helper.hpp
#pragma once

#include <memory>

struct Number;

struct Helper
{
   
Helper() { }
   
std::unique_ptr < Number > ptr;
};

memory(3128,1): error C2027: use of undefined type 'Number'
memory(3128,25): error C2338: static_assert failed: 'can't delete an incomplete type'
memory(3129,9): warning C4150: deletion of pointer to incomplete type 'Number'; no destructor called

Oczywiście przed założeniem tego tematu zrobiłem research i dowiedziałem się, że mój smart pointer używa
std::default_delete
 co oznacza, że potrzebuje znać pełną definicję klasy
Number
 w destruktorze mojej klasy
Helper
. Błąd kompilacji można naprawić definiując destruktor poza nagłówkiem, w innej jednostce translacji, gdzie można już bezpiecznie zaincludować pełną definicję Number.hpp. Zmiany takie jak poniżej:

C/C++
// Helper.hpp
#pragma once

#include <memory>

struct Number;

struct Helper
{
   
Helper() { }
   
~Helper();
   
std::unique_ptr < Number > ptr;
};
C/C++
#include "Helper.hpp"

#include "Number.hpp"          
// Trzeba dołączyć definicję struct Number
Helper::~Helper() = default;

Okej, naprawiłem problem, ale nie rozumiem co się tutaj właściwie zadziało. Wiem, że dodanie user-defined konstruktora sprawiło, że moja klasa przestała być trivial. Jednak nadal nie wiem jaki to ma związek z unique_ptr :-/
P-179602
DejaVu
» 2022-08-14 00:47:25
Sprawdź czy jak implementację konstruktora dasz do Helper.cpp czy zadziała.
P-179604
Kyriet
Temat założony przez niniejszego użytkownika
» 2022-08-14 02:09:16
Nope, implementacja konstruktora w Helper.cpp bez destruktora się nie kompiluje. Ten sam powód co wyżej ._.
P-179606
pekfos
» 2022-08-14 13:17:49
Twój przykład nie jest minimalny, bo niepotrzebnie jest podzielony na N plików.
C/C++
#include <memory>

struct Number;

struct Helper
{
   
//Helper() { }
   
std::unique_ptr < Number > ptr;
};

// struct Number { };

int main()
{
   
new Helper;
   
return 0;
}
Jak nie postawisz konstruktora samemu, to "działa", ale przy próbie utworzenia obiektu i tak wymusisz utworzenie domyślnego konstruktora więc problem się pojawia. Konstruktor może również wywoływać destruktory dla podobiektów klasy w przypadku wystąpienia błędu. (https://eel.is/c++draft/except.ctor#3).
P-179608
Kyriet
Temat założony przez niniejszego użytkownika
» 2022-08-15 12:46:52
Dzięki za odpowiedź, nie wiedziałem o tym, że z konstruktora mogą się zawołać destruktory podobiektów. Ale w takim razie dlaczego definicja destruktora
struct Helper
 w innej jednostce translacji naprawiła błąd podczas gdy konstruktor nadal pozostał w pliku nagłówkowym? Przecież gdyby mój konstruktor
Helper() { }
 chciał uruchomić destruktor
std::unique_ptr < Number >
, to nadal nie powinien być w stanie tego zrobić, bo widział tylko deklarację
Number
. Chyba, że konstruktor w przypadku błędu wywołuje destruktor całego
Helper
, wtedy miałoby to sens, dlaczego to działa.

Kolejna kwestia, to to, że konstruktor zdefiniowany tak:
Helper() noexcept { }
 też powoduje błąd. Czyli pomimo
noexcept
 kompilator nadal generuje w konstruktorze kod destrukcji
std::unique_ptr < Number >
? Moim zdaniem to trochę bez sensu.

Ale w każdym razie wygląda na to, że definiowanie konstruktorów / destruktorów w nagłówku to proszenie się o problemy w przyszłości. Ehhh... skomplikowany jest ten język 😄
P-179610
DejaVu
» 2022-08-15 12:50:02
Chodzi to, że w momencie gdy definiujesz 'implementację' konstruktora/metody, to musisz znać już 'jak wyglądają typy wszystkich składowych'. Skoro definiujesz konstruktor wewnątrz klasy, a nie w pliku *.cpp oraz masz 'forward' deklaracje to doprowadzasz do sytuacji, że nie da się wygenerować poprawnego kodu, bo nie znasz definicji typu wszystkich zmiennych.
P-179611
pekfos
» 2022-08-15 13:50:08
noexcept znaczy tylko tyle, że wyjątek nie może wylecieć z wywołania tak zadeklarowanej funkcji, ale wciąż może być wyrzucony wewnątrz. Gdyby miało dojść do propagacji na zewnątrz, to program zostanie zakończony.

Ale w takim razie dlaczego definicja destruktora
struct Helper
 w innej jednostce translacji naprawiła błąd podczas gdy konstruktor nadal pozostał w pliku nagłówkowym?
A naprawiła? Coś takiego dalej się nie kompiluje, przynajmniej na GCC.
C/C++
#include <memory>

struct Number;

struct Helper
{
   
Helper() { } // "required from here"
   
~Helper();
   
std::unique_ptr < Number > ptr;
};
In file included from D:/TDM-GCC-64/lib/gcc/x86_64-w64-mingw32/9.2.0/include/c++/memory:80,
                 from a.cpp:1:
D:/TDM-GCC-64/lib/gcc/x86_64-w64-mingw32/9.2.0/include/c++/bits/unique_ptr.h: In instantiation of 'void std::default_delete<_Tp>::operator()(_Tp*) const [with _Tp = Number]':
D:/TDM-GCC-64/lib/gcc/x86_64-w64-mingw32/9.2.0/include/c++/bits/unique_ptr.h:284:17:   required from 'std::unique_ptr<_Tp, _Dp>::~unique_ptr() [with _Tp = Number; _Dp = std::default_delete<Number>]'
a.cpp:7:14:   required from here
D:/TDM-GCC-64/lib/gcc/x86_64-w64-mingw32/9.2.0/include/c++/bits/unique_ptr.h:79:16: error: invalid application of 'sizeof' to incomplete type 'Number'
   79 |  static_assert(sizeof(_Tp)>0,
      |

Ale w każdym razie wygląda na to, że definiowanie konstruktorów / destruktorów w nagłówku to proszenie się o problemy w przyszłości. Ehhh... skomplikowany jest ten język 😄
Po prostu przykład jest bez sensu. Jakakolwiek realna implementacja też by miała te same problemy, przez to że Number jest niekompletne w tym kontekście. Tu zachowanie jest nieoczywiste tylko dlatego że nie widać co dokładnie kompilator robi.
P-179612
Kyriet
Temat założony przez niniejszego użytkownika
» 2022-08-16 22:41:28
Chodzi to, że w momencie gdy definiujesz 'implementację' konstruktora/metody, to musisz znać już 'jak wyglądają typy wszystkich składowych'

Ale jak destruktor jest zdefiniowany w innej jednostce translacji, to już nagle implementacja konstruktora nie musi znać 'jak wyglądają typy wszystkich składowych'? 😄

A naprawiła? Coś takiego dalej się nie kompiluje, przynajmniej na GCC.

Ano naprawiła. Na GCC, proszę: https://onlinegdb.com/r8sQPzGor
Możesz się pobawić tym kodem. Przenieś definicję destruktora z powrotem do definicji
Helper
 i zobacz jak nagle program się nie skompiluje. Albo zamień konstruktor na
Helper() = default
 i zobacz jak nagle destruktor przestaje być potrzebny i program się kompiluje bez niego.

Po prostu przykład jest bez sensu. Jakakolwiek realna implementacja też by miała te same problemy, przez to że Number jest niekompletne w tym kontekście.

Dlatego właśnie podałem mój przykład złożony z 3 plików. Jest on jak najbardziej z sensem i zderzyłem się z tą sytuacją w moim kodzie. Miałem cykliczną zależność: Klasa A includowała klasę B, klasa B includowała klasę C, a klasa C trzymała wskaźnik na klasę A (klasa A była forward-declared). Wszystko działało do czasu, gdy postanowiłem zamienić "wskaźnik" na std::unique_ptr. Nagle się nie skompilowało. Wtedy natrafiłem na wpis blogowy: https://ortogonal.github.io/cpp/forward-declaration-and-smart-pointers/, który pokazuje ten trick, że zdefiniowanie destruktora w innej jednostce translacji magiczne pozwala na stworzenie smart-pointera z forward zadeklarowanego typu, bez definicji. Autor wyjaśnia nawet dlaczego to działa. A mnie zastanawia (i to jest powód dla którego założyłem ten wątek na forum): dlaczego ten "trick" nie jest potrzebny, gdy konstruktor mojej klasy jest
= default
.

A tutaj przykład z "prawdziwego" projektu, gdzie występuje ten "problem": https://github.com/TheCherno/Hazel/blob/master/Hazel/src/Hazel/Scene/Components.h#L89
Autor w pliku Components.h w linii 87 robi forward-dekalrację
ScriptableEntity
. Plik ScriptableEntity.h includuje Entity.hpp. Plik Entity.h includuje Components.h. Czyli ta forward deklaracja na początku ratuje program przed cyklicznym includowaniem siebie nawzajem nagłówków. W kodzie z githuba, który podlinkowałem problem nie występuje, bo autor użył surowego wskaźnika
ScriptableEntity *
. Ja chciałem być "fancy" i zamienić to na
unique_ptr < ScriptableEntity >
. I wtedy napotkałem błąd kompilacji \(〇_o)/

Wyjaśnienia opisywane przez Was (za które bardzo dziękuję) mają częściowo sens. Chociaż moje dotychczasowe eksperymenty wskazują na taki wniosek: Zdefiniowanie własnego konstruktora w klasie
Helper
 sprawia, że (z nadal nieznanego mi powodu) automatycznie generowany destruktor tejże klasy potrzebuje nagle definicji
Number
, aby się skompilować. I dlatego destruktor ten należy przenieść gdzieś indziej (np. do Helper.cpp), i tam zaincludować definicję
Number
. Tak jak to zrobiłem na przykładzie 🙃
P-179614
« 1 » 2
  Strona 1 z 2 Następna strona