Referencje cykliczne - jak poprawić i skompilować

0

Mam co następuje:
Ubuntu Linux 20.04.2
GCC w wersji 9.3.0
Qt w wersji 5.12.8
Qt Creator w wersji 4.11.0

Chce zrobić w C++ dokładnie to samo, co w Java i w C# można zrobić bez żadnego problemu.
Chciałbym mieć klasę A, w której jest referencja do obiektu klasy B oraz klasę B, w której jest referencja do obiektu klasy A.

W efekcie mam taką klasę A (plik *.h i *.cpp):

#ifndef TESTCLASSA_H
#define TESTCLASSA_H

#include "testclassb.h"

class TestClassA
{
public:
    TestClassA();
    TestClassB * RefB;
    int I;
    void foo();
    void bar();
};

#endif // TESTCLASSA_H
#ifndef TESTCLASSB_H
#define TESTCLASSB_H

#include "testclassa.h"

class TestClassB
{
public:
    TestClassB();
    TestClassA * RefA;
    int I;
    void foo();
    void bar();
};

#endif // TESTCLASSB_H

Klasa B jest analogiczna do klasy A:

#include "testclassa.h"

TestClassA::TestClassA()
{
    I = 0;
    RefB = nullptr;
}

void TestClassA::foo()
{
    I++;
}

void TestClassA::bar()
{
    RefB->foo();
}
#include "testclassb.h"

TestClassB::TestClassB()
{
    I = 0;
    RefA = nullptr;
}

void TestClassB::foo()
{
    I++;
}

void TestClassB::bar()
{
    RefA->foo();
}

W całej aplikacji jest też MainWindow zdefiniowany nastepująco:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QDebug>
#include "testclassa.h"
#include "testclassb.h"

using namespace std::chrono;


QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    TestClassA * TestClassA_;
    TestClassB * TestClassB_;
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    TestClassA_ = new TestClassA();
    TestClassB_ = new TestClassB();
    TestClassA_->RefB = TestClassB_;
    TestClassB_->RefA = TestClassA_;
}

MainWindow::~MainWindow()
{
    delete TestClassA_;
    delete TestClassB_;
    delete ui;
}

To, co powyżej to możliwie najprostszy sposób zobrazowania problemu, nie chodzi o rozważanie słuszności takiej architektury docelowego projektu. w C# nie ma z tym problemu. Natomiast tutaj, jak próbuję to skompilować, to przy linii TestClass * RefA w klasie TestClassA pokazuje się ‘TestClassA’ does not name a type; did you mean ‘TestClassB’. W drugiej klasie dokładnie to samo. W samym IDE, jak podążam za typem (klawisz F2, przejście do deklaracji pola lub typu), to IDE widzi definicje klas nawzajem, czyli jest prawidłowo.

Skompilowanie tego jako C++20 nie rozwiązuje problemu. Dopiero, jak w jednej z klas usunę linię #include "testclassb.h" lub #include "testclassa.h" z pliku *.h i oczywiście wszystko, co pochodzi z tego pliku, to program się kompiluje, jednak, co oczywiste nie ma funkcjonalności, którą ma mieć.

W jaki sposób rozwiązać ten problem zachowując architekturę dwóch klas z referencją wzajemną? Czy to tylko kwestia dodatkowej deklaracji, konfiguracji IDE, konfiguracji kompilatora, czy coś jeszcze innego?

Co do samych referencji, to oczywiście nie muszą być cykliczne, można może na przykład tak, ale problem ciągle pozostaje:

TestClassA TestClassA1 = new TestClassA();
TestClassB TestClassB1 = new TestClassB();
TestClassA TestClassA2 = new TestClassA();
TestClassB TestClassB2 = new TestClassB();
TestClassA TestClassA3 = new TestClassA();
TestClassB TestClassB3 = new TestClassB();

TestClassA1->RefB = TestClassB1;
TestClassB1->RefA = TestClassA2;
TestClassA2->RefB = TestClassB2;
TestClassB2->RefA = TestClassA3;
TestClassA3->RefB = TestClassB3;
6

Jak mówisz o C++ to staraj się zachowywać poprawne nazewnictwo. Wskaźnik, referencja, obiekt - to różne rzeczy, i nazywasz je błędnie.

Jeśli chodzi o sam problem: potrzebujesz forward deklaracji: https://en.cppreference.com/w/cpp/language/class - w ten sposób powiesz kompilatorowi "takie coś istnieje i jest to klasa".

struct foo;
void work_on_foo(foo*);

0

Wygląda na to, że dopisanie "deklaracji naprzód" w plikach nagłówkowych rozwiązuje problem.

#ifndef TESTCLASSA_H
#define TESTCLASSA_H

#include "testclassb.h"

class TestClassB;

class TestClassA
{
public:
    TestClassA();
    TestClassB * RefB;
    int I;
    void foo();
    void bar();
};

#endif // TESTCLASSA_H
#ifndef TESTCLASSB_H
#define TESTCLASSB_H

#include "testclassa.h"

class TestClassA;

class TestClassB
{
public:
    TestClassB();
    TestClassA * RefA;
    int I;
    void foo();
    void bar();
};

#endif // TESTCLASSB_H

Co do nazewnictwa, to moim zdaniem jedyna różnica między wskaźnikiem a referencji jest taka, że pod wskaźnikiem jest adres, może być adres pusty (null), adres obiektu albo tez przypadkowy adres, a referencja jest albo pusta, abo wskazuje obiekt (bez możliwości odczytu adresu). Czy dobrze myślę, ze, co nazwałem referencją to tak naprawdę jest to wskaźnik? Idea wydaje się być ta sama, jedynie trzeba samemu pilnować, żeby wskaźnik był albo pusty, albo na adres obiektu.

1

Referecja nigdy nie będzie pusta, przynajmniej ta ze znakiem & w C++.
Wskaźnik jest "rebindable", tzn. posiada operator przypisania, ergo po jego stworzeniu możesz pokazać tym samym wskaźnikiem na różne obiekty, referencja - nie, ona ma jedynie konstruktor kopiujący (ma-nie ma...zachowuje się w taki sposób). Ponadto nie zrobisz wektora (czy ogólniej: kontenera) referencji, wskaźników już tak. Referencyjnym odpowiednikiem wskaźnika (jeśli można to tak nazywać, bo wszystko co mówię nie jest raczej tak ujęte w standardzie, to jest moja własna interpretacja, żeby mi to w głowie grało ;)) będzie std::reference_wrapper

EDIT: referencja nie ma swojego adresu, ale można wziąć adres jej docelowego obiektu, w ten sposób implementuje się std::addressof, który ma za zadanie wziąć faktyczny adres obiektu w momencie kiedy operator & jest przeładowany:

Foo foo;
Foo* bar { reinterpret_cast<Foo*>( &const_cast<char&> ( reinterpret_cast<const volatile char&> (foo) ) ) };
0

Czyli referencja w C++, a referencja w .NET lub w Java ma różne znaczenia, prawda?

W C# tworzenie obiektu wygląda tak:

Foo foo = new Foo();
Foo bar;

Spotkałem się z tym, ze w C# foo i bar to referencje, pierwsza jest na obiekt, druga jest pusta (wskazuje null). Nie da się ani uzyskać adresu, ani wskazać na coś innego niż obiekt lub null. Odnośnie Java mówi się wprost, że w Java nie ma wskaźników, a tworzenie i niszczenie obiektów przebiega tak samo. W C# zasadniczo też nie ma wskaźników, jednakże są one możliwe, a kod je zawierający należy objąć blokiem unsafe i pozwolić na kompilację takiego kodu w ustawieniach.

1

Tak i nie. Są podobieństwa, ale powiedziałbym że w Javie ta referencja to taki miks wskaźnika i C++owej referencji. @vpiotr Ty chyba w dżawce też piszesz, możesz się wypowiedzieć?
EDIT: ale w skrócie: koncepcyjnie te rzeczy są podobne, w C++ bardzo często pod spodem referencji i tak siedzi wskaźnik (chociaż nie musi).

2
alagner napisał(a):

Tak i nie. Są podobieństwa, ale powiedziałbym że w Javie ta referencja to taki miks wskaźnika i C++owej referencji. @vpiotr Ty chyba w dżawce też piszesz, możesz się wypowiedzieć?

EDIT: ale w skrócie: koncepcyjnie te rzeczy są podobne, w C++ bardzo często pod spodem referencji i tak siedzi wskaźnik (chociaż nie musi).

To ja się wymądrzę. :)

W Javie to raczej są wskaźniki (mówiąc po cplusplusowemu) -- bo mogą być puste i są 'rebindable' -- podstawienie x = y w Javie działa jak w x = y w C++ dla typów wskaźnikowych, nie referencyjnych. W C# chyba podobnie jak w Javie...

andrzejlisek napisał(a):

Co do nazewnictwa, to moim zdaniem jedyna różnica między wskaźnikiem a referencji jest taka [...]

Ja bym powiedział, że w C++ referencja to po prostu inna nazwa konkretnego obiektu (i nie ma co się wdawać w filozoficzne rozważania czym to jest :)), a wskaźnik to adres takiego obiektu... Takie myslenie u mnie wiele rozwiązało. Patrzenie na inne języki jest na nic, bo mają swoje nazewnictwo, nie do końca zbieżne z nazewnictwem C++.

4

@alagner: te pojęcia zależą od języka:

  • referencja w C++ to taki alias do istniejącego obiektu, gdzieś pod spodem reprezentowany jako jego adres, nie powinien być pusty
  • wskaźnik w C++ to adres komórki pamięci, niekoniecznie obiektu, inkrementowany zgodnie z reprezentowanym typem, może być pusty (NULL, null_ptr)
  • inteligentny wskaźnik w C++ (unique_ptr, shared_ptr) to taki zarządzany adres obiektu, który może być pusty ale który ma też możliwość śledzenia obiektu
  • referencja (ta podstawowa) w Javie ma najbliżej do inteligentnego wskaźnika w C++, przy czym są jeszcze referencje "specjalne" - SoftReference, WeakReference
  • różnica między referencją w Javie a inteligentnym wskaźnikiem w C++ to przede wszystkim moment kiedy obiekt jest zwalniany - w C++ natychmiast po wyjściu poza zakres dostępności obiektu i kod za to odpowiedzialny jest wkompilowany w program, w Javie - w bliżej nieokreślonej przyszłości po wyjściu poza zakres i kod za to odpowiedzialny jest w GC
  • referencja z OP (zwana również "odwołaniem") występuje w każdym języku i w wersji cyklicznej (A->B, B->A) nie jest zalecana
3

Offtopic: Nieważne jaki język Java/C#/C++ cykliczne zależności między symbolami są poważnym błędem architektonicznym.

1 użytkowników online, w tym zalogowanych: 0, gości: 1