C++ и циклические зависимости

108
04 июня 2021, 00:50

Я хочу понять, существует ли какой-то единый способ лечения циклических зависимостей?

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

Самым неприятным в этой ситуации является то, что в большинстве случаев компилятор выдает тонну уведомлений. При этом, уведомления указывают в неверные места, и только опыт помогает более или менее быстро понять, что причиной является циклическая зависимость.

К тому же, многие постоянно забывают, что #pragma once защищает от множественного включения, а не от циклических зависимостей.

К примеру, есть сцена с боем, есть бой, бой содержит ссылку на родительскую сцену:

// BattleScene.hpp
class BattleScene
{
    private:
    Battle battle;
};
// Battle.hpp
class Battle
{
    private:
    BattleScene &parrent;
};

Каков общий способ решения такого вопроса?

Answer 1

Для того, чтобы компилятор не ругался - достаточно использовать предварительное объявление:

class BattleScene;
class Battle
{
private:
    BattleScene &parent;
};

Замечу, что объявленный, но ещё не определенный класс нельзя использовать по значению, только по ссылке или по указателю.

Однако, с циклическими ссылками есть и вторая проблема: циклические структуры данных невозможно тривиально скопировать или перенести.

Так, если вы попытаетесь сделать вот так:

BattleScene copy = someOtherScene;

то окажется, что copy.battle.parent указывает не на copy, а на someOtherScene! Поэтому не забывайте запрещать копирование и перемещение!

class BattleScene;
class Battle
{
private:
    BattleScene &parent;
    Battle(const Battle&) = delete;
    Battle(Battle&&) = delete;
    Battle& operator=(const Battle&) = delete;
    Battle& operator=(Battle&&) = delete;
};

Ещё одна проблема с циклическими структурами - возможная потеря константности. Так, если у вас есть переменная const BattleScene scene, то вы можете взять scene.battle.parent - и получить неконстантную ссылку на scene!

Кроме всего этого, лишние ссылки банально неэффективны по памяти.

Поэтому предлагаю вам рассмотреть альтернативу - передачу родительского объекта параметром в методы дочернего:

class BattleScene;
class Battle
{
public:
    void Foo(BattleScene& parent);
};
class BattleScene
{
private:
    Battle battle;
public:
    void Bar()
    {
        battle.Foo(*this);
    }
};

Если вам требуется куда-то передавать ссылку на Battle отдельно - можно сделать обёртку:

class BattleScene;
class Battle
{
public:
    void Foo(BattleScene& parent);
};
class BattleRef
{
    BattleScene& scene;
    Battle& battle;
public:
    BattleRef(BattleScene& scene, Battle& battle) : scene(scene), battle(battle) { }
    void Foo()
    {
        battle.Foo(scene);
    }
};
Answer 2
// BattleScene.hpp
#include "Battle.hpp"
// Battle.hpp
class BattleScene;
class Battle
{
 private:
  BattleScene &parrent;
};
Answer 3

Универсальный способ лечения циклических зависимостей можно создать, но только сначала нужно понять порядок объявлений и зависимостей между ними.

Самый первый простой элемент это предварительное объявление типа. Потом объявление класса с их элементами и методами. Только без объявления методов класса внутри него! Это может привести к невозможности компиляции. Дальше эти самые инлайн функции. И потом определения простых статических методов.

Допустим есть два класса, которе содержат ссылку друг на друга и методы с аргументом типа другого класса. Вот пример как всё нужно правильно объявлять :

A.hpp :

# pragma once
// предварительное объявление класса B
class B ;
class A {
public :
  A ( B & ) ;
  B & theB();
  // аргументом по значению тоже можно
  void  Argument_B_by_value ( B );
private :
  B & b ;
} ;

B.hpp :

# pragma once
// предварительное объявление класса A
class A ;
class B {
public :
  B ( A & ) ;
  A & theA();
  // аргументом по значению тоже можно
  void  Argument_A_by_value ( A );
private :
  A & a ;
} ;

Ai.hpp :

// порядок не важен
# include "A.hpp"
# include "B.hpp"
inline B & A :: theB ( ) {
  return b ; }
inline void  A :: Argument_B_by_value ( B ) {
  }

Bi.hpp :

// порядок не важен
# include "B.hpp"
# include "A.hpp"
inline A & B :: theA ( ) {
  return a ; }
inline void  B :: Argument_A_by_value ( A ) {
  }

A.cpp :

// порядок не важен
# include "A.hpp"
# include "B.hpp"
// порядок не важен
# include "Ai.hpp"
# include "Bi.hpp"
A :: A ( B & x ) : b { x } { }

B.cpp :

// порядок не важен
# include "B.hpp"
# include "A.hpp"
// порядок не важен
# include "Bi.hpp"
# include "Ai.hpp"
B :: B ( A & x ) : a { x } { }

Получается три уровня с разными именами файлов :

  1. Предварительное объявление других типов. Определение класса.

  2. Инлайн методы.

  3. Статические методы.

Второй уровень включает хедеры первого уровня. Третий уровень включает хедеры первого и второго уровня.

READ ALSO
CodeBlocks не ставит автоматические табы

CodeBlocks не ставит автоматические табы

CodeBlocks на Ubuntu 1804 не форматирует фигурные скобки как в Windows версии

84
Разместить время в таблице

Разместить время в таблице

Подскажите пожалуйста, как сделать что бы время было по центру border-right?

103
Почему не работает в хроме?

Почему не работает в хроме?

Есть сайт: http://demoinfinity-web

347
Увеличить время для hover`a

Увеличить время для hover`a

Есть код, как увеличить время возврата в первоначальное положение, те

81