Форвардные объявления в модулях 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;
линия. Дайте мне знать, если знаете, почему, а еще лучше, если у вас есть ссылка на видео/статью/книгу, где объясняется, как все это работает и почему все это происходит.