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

Вопрос или проблема

У меня есть несколько классов, связанных с играми, таких как “StateManager”, “Window”, “EventManager” и так далее.

Эти классы находятся в отдельных модулях. Модуль “Events” содержит класс EventManager, модуль “States” содержит класс StateManager и связанные с ним классы, класс Window находится в отдельном модуле Window и так далее.

Всегда бывают случаи, когда одной из этих систем нужен доступ к какой-либо функциональности или состоянию одной из других систем (например, StateManager может сообщить EventManager, что текущий тип состояния изменился). Чтобы разрешить этот доступ и обмен между всеми этими различными системами, в “предмодульные” времена я использовал легковесный объект SharedContext, который просто хранит некоторые указатели на все соответствующие классы.

#pragma once

class Window;
class EventManager;
class TextureManager;
class GUI_Manager;

struct SharedContext{
    Window* m_window = nullptr;
    EventManager* m_eventManager = nullptr;
    TextureManager* m_textureManager = nullptr;
    GUI_Manager* m_gui_manager = nullptr;
}; 

Этот объект SharedContext создается один раз, загружает все ссылки на уже существующие объекты в свои переменные-члены и затем передается, так что все объекты, получающие SharedContext, могут получить доступ к прикрепленным классам, таким как Window, EventManager, TextureManager, GUI_Manager и т.д.

Этот подход совершенно несовместим с модулями. Основная проблема в том, что я не могу объявлять имя класса после его объявления в модуле. Поэтому предварительные декларации здесь неправильно оформлены, и код не будет компилироваться, поскольку все предварительно объявленные классы уже существуют в модулях.

SharedContext логически не принадлежит ни одному модулю. Но сделать его модулем (чтобы я мог экспортировать предварительно объявленные классы) тоже не работает:

Следующий минимальный пример выдает ошибку “error: reference to ‘EventManager’ is ambiguous”; примечание: кандидаты: ‘class EventManager@SharedContext’ … примечание: ‘class EventManager@Events’, используя GCC 14.2:

//SharedContext.cppm
export module SharedContext;
export class EventManager;
export struct SharedContext{
    EventManager* m_eventManager = nullptr;
};
//Events.cppm
export module Events;
export class EventManager{

};
//main.cpp
import Events;
import SharedContext;
int main() {

    EventManager manager;
    SharedContext context;
    context.m_eventManager = &manager;

    return 0;
}

Импорт всех модулей вместо предварительного объявления необходимых классов в файле SharedContext может привести к круговым зависимостям, потому что, когда я импортирую модуль “States”, чтобы иметь возможность создать указатель на объект StateManager, импортируются и многие другие классы, которые являются частью модуля “States”. Поэтому, когда только один из этих классов нуждается в SharedContext, я создаю круговую зависимость (SharedContext импортирует модуль “States”, который в одной из своих модульных частей может импортировать SharedContext).

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

Ответ или решение

Проблема: Предварительное объявление классов для использования указателей на объекты

Ваша задача заключается в взаимодействии нескольких классов разных модулей, таких как StateManager, Window и EventManager, без создания циклических зависимостей и корректного управления областью видимости. Как показывают ваши примеры, использование предварительных объявлений классов в рамках модулей приводит к трудностям с компиляцией, из-за особенностей дизайна модульной системы языка.

Понимание модулей и предварительных объявлений

В модульной архитектуре C++ запрещается повторное объявление классов, что создает ограничения в плане предварительных объявлений. Это значит, что как только класс объявлен в одном модуле, вы не можете использовать его имя в качестве предварительного объявления в других модулях. Это причина, по которой ваш подход с SharedContext, как вы уже заметили, не подходит.

Решение на основе полиморфизма и интерфейсов

Одним из возможных решений этой проблемы может стать использование абстрактного интерфейса в качестве отправной точки для всех классов, связанные с контекстом. Вместо явного указания, что каждый модуль использует конкретные классы, вы можете определить базовый класс, который будет реализован остальными модулями. Давайте рассмотрим, как это может выглядеть.

Шаг 1: Определите интерфейсы

Создайте общий интерфейс для классов, который будет использоваться в вашем SharedContext.

// IEventManager.h
#pragma once
class IEventManager {
public:
    virtual void handleEvent() = 0;
    virtual ~IEventManager() = default; // виртуальный деструктор
};

// ITextureManager.h
#pragma once
class ITextureManager {
public:
    virtual void loadTexture() = 0;
    virtual ~ITextureManager() = default;
};

// Аналогично создайте интерфейсы для других классов.
Шаг 2: Имплементация интерфейсов в ваших модулях

В каждом модуле вы реализуете интерфейс:

// Events.cppm
export module Events;
import IEventManager;

export class EventManager : public IEventManager {
public:
    void handleEvent() override {
        // Реализация
    }
};

// Аналогично для других классов.
Шаг 3: Обновите SharedContext

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

// SharedContext.cppm
export module SharedContext;
import IEventManager;
import ITextureManager;

export struct SharedContext {
    IEventManager* m_eventManager = nullptr;
    ITextureManager* m_textureManager = nullptr;
    // И другие управляющие структуры
};
Шаг 4: Основной модуль

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

// main.cpp
import Events;
import SharedContext;

int main() {
    EventManager manager;
    SharedContext context;
    context.m_eventManager = &manager; // Полиморфное присвоение указателя на интерфейс

    return 0;
}

Заключение

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

Приобретая уверенность в своих архитектурных решениях, вы сможете более эффективно управлять сложной иерархией классов, сохраняя высокий уровень абстракции без ущерба для производительности.

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

Оцените материал
Добавить комментарий

Капча загружается...