Форвардные объявления в модулях C++ (MSVC)

В последнее время я экспериментировал с реализацией модулей, предоставленных MSVC, и столкнулся с интересным сценарием. У меня есть два класса, которые имеют взаимную зависимость в своих интерфейсах, что означает, что мне придется использовать предварительные объявления, чтобы заставить его компилироваться. Следующий код показывает пример:

Module interface

export module FooBar;

export namespace FooBar {
    class Bar;

    class Foo {
    public:
        Bar createBar();
    };

    class Bar {
    public:
        Foo createFoo();
    };
}

Module implementation

module FooBar;

namespace FooBar {
    Bar Foo::createBar() {
        return Bar();
    }

    Foo Bar::createFoo() {
        return Foo();
    }
}

Теперь я хотел бы разделить эти два класса на их собственные модули с именем Foo а также Bar, Однако каждый модуль должен импортировать другой, так как их интерфейсы зависят друг от друга. И в соответствии с предложением модулей в настоящее время импорт кругового интерфейса не допускается. В этой статье предлагается использовать proclaimed ownership декларации, но кажется, что это еще не реализовано в реализации модулей MSVC.

Поэтому я прав, полагая, что эта ситуация в настоящее время не может быть решена с использованием текущей реализации, как это предусмотрено MSVC? Или есть какая-то альтернатива, которую мне не хватает? В этом сценарии ситуация довольно тривиальна, однако я столкнулся с этой проблемой при модульной библиотеке, которая имеет много классов, имеющих такие зависимости. Я понимаю, что циклические зависимости часто являются признаком плохого дизайна, однако в некоторых случаях они неизбежны или трудны для рефакторинга.

2 ответа

Вы можете создать третий модуль, который экспортирует только предварительные объявления для каждого из ваших классов (может быть много классов).
Затем вы импортируете этот модуль в оба (или все) свои модули, где он предоставляет предварительные объявления, необходимые для реализации каждого модуля.

К сожалению, MSVC все еще имеет (сегодня это версия 16.7) проблемы с модулями; хотя этот подход работает, вы часто получаете совершенно дикие сообщения об ошибках; например, "невозможно преобразовать MyClass* в MyClass* - преобразование не предусмотрено (этот пример происходит, когда вы напрямую добавляете прямую декалацию к одному и тому же классу в несколько модулей; компилятор считает их разными животными).
Другая проблема - если вы забыли импортировать все требуемых модулей, сообщение об ошибке либо сильно вводит в заблуждение ("в этом классе нет такого метода"), либо компилятор прерывает работу с внутренней ошибкой.

Не ожидайте слишком многого, пока они не закончат свою работу.

Я только что нашел 2 решения этой проблемы.

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

Итак, допустим, у нас есть 2 модуля:MyType1.ixx:

      export module MyType1;

namespace MyNamespace {
    class MyType2;

    export class MyType1 {
    public:
        MyType1(MyType2* MyType2);
    private:
        MyType2* MyType2Intance{};
    };
}

Модуль1.cpp:

      module MyType1;

namespace MyNamespace {
    MyType1::MyType1(MyType2* MyType2) {
        MyType2Intance = MyType2;
    };
}

МойТип2.ixx:

      export module MyType2;

namespace MyNamespace {
    export class MyType2 {
    public:
    };
}

Затем, если вы попытаетесь создать экземпляр MyType1 в модуле Main.ixx:

      export module Main;

import MyType1;
import MyType2;

using namespace MyNamespace;

int main() {
    MyType2* MyType2Ptr = nullptr;
    MyType1 MyType1(MyType2Ptr);
}

Вы получитеError C2665 'MyNamespace::MyType1::MyType1': no overloaded function could convert all the argument types. Что совершенно странно, поскольку информации достаточно, чтобы все решить. Нам не нужен импорт ни в .ixx, ни в .cpp файле модуля MyType1 для простой ссылки на указатель. Даже если мы действительно использовали MyType2 в файле MyType1.cpp, нам нужно будет импортировать только MyType2 в файл .cpp, и нам не нужно будет импортировать MyType2 в файл MyType1.ixx.

Итак, что же делать, чтобы исправить эту досадную нелогичную ошибку, которой вообще не должно быть?

Одним из решений является экспорт заранее объявленного класса MyType2 в MyType1.ixx:

      export module MyType1;

namespace MyNamespace {
    export class MyType2;

    export class MyType1 {
    public:
        MyType1(MyType2* MyType2);
    private:
        MyType2* MyType2Intance{};
    };
}

Другое решение — заранее объявить класс MyType2 передexport module MyType1линия:

      namespace MyNamespace {
    class MyType2;
}

export module MyType1;

namespace MyNamespace {
    export class MyType1 {
    public:
        MyType1(MyType2* MyType2);
    private:
        MyType2* MyType2Intance{};
    };
}

И если вы используете MyType2 в файле реализации модуля MyType2, вам, конечно, придется его правильно импортировать.

Я бы показал работающий пример компиляции на godbolt или чем-то еще, но я не думаю, что там есть способ создавать подобные многофайловые решения.

Почему это работает, я действительно не знаю, поскольку сам только начинаю с модулей. Вчера Visual Studio предупредила меня в журнале вывода сборки, что строка объявления модуля после экспорта, вероятно, ошибочна (но решение скомпилировано нормально) и что мне следует переместить#include <Windows.h>перед этой строкой. Я сделал это, это сработало, и это означает, что в файле интерфейса модуля есть несколько разделов: до строки и, по крайней мере, после.

Кроме того, почему первое решение с экспортом предварительного объявления исправляет ошибку компиляции? Главный модуль импортирует оба модуля, поэтому у него нет проблем с наличием всей необходимой информации.

Я сделал несколько руководств по модулям, например, от Microsoft: https://learn.microsoft.com/en-us/cpp/cpp/tutorial-named-modules-cpp?view=msvc-170 .

Но я не думаю, что кто-либо из них объяснил эту особенность предварительного объявления, а также то, чем #include заголовки отличаются до и послеexport module MyModule;линия. Дайте мне знать, если знаете, почему, а еще лучше, если у вас есть ссылка на видео/статью/книгу, где объясняется, как все это работает и почему все это происходит.

Другие вопросы по тегам