Провел пару простых экспериментов с модулями C++20 и получил странные результаты.
Эксперименты проводились под Windows с VS2022 и VS2026 (обновления от мая 2026-го) и ArchLinux с GCC 16.1 и clang 22.1.
Ожидаемые мной результаты (т.е. отсутствие проблем компиляции/линковки) получились только с clang 22.1 и libc++. А вот с GCC и VC++ случились какие-то проблемы, которые мне сложно объяснить.
Во всех случаях сборка осуществлялась через CMake и Ninja.
Исходные коды описанных ниже тестовых программ можно найти в этом репозитории.
Эксперимент первый (case_001 из упомянутого репозитория). Очень простой модуль. Декларация в файле hello.ixx:
|
module; #include <iostream> #include <typeinfo> export module hello; export namespace hello { void greet(); template<typename T> struct helper { static void a(); static void b(); }; template<typename T> void helper<T>::a() { std::cout << "helper<" << typeid(T).name() << ">::a()" << std::endl; } template<typename T> void helper<T>::b() { std::cout << "helper<" << typeid(T).name() << ">::a()" << std::endl; } } // export namespace hello |
Реализация в файле hello.cpp:
|
module; #include <iostream> module hello; namespace hello { void greet() { std::cout << "hello, world!" << std::endl; } } /* namespace hello */ |
Но мне кажется, что ключевой момент в том, что шаблон класса helper реализован прямо в hello.ixx.
Описание библиотеки hello для CMake:
|
cmake_minimum_required(VERSION 3.28) project(hello CXX) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED YES) add_library(hello) target_sources(hello PUBLIC FILE_SET CXX_MODULES FILES hello.ixx PRIVATE hello.cxx ) |
Используется это в очень простой программе:
|
import hello; int main() { hello::greet(); hello::helper<int>::a(); hello::helper<long>::b(); } |
И здесь засада №1. В clang-е все OK, как я и ожидаю.
А вот для GCC и MSVC++ ошибка компиляции -- оказывается, в методах helper::a и helper::b не определен символ type_info.
Решается эта проблема добавлением в тестовую программу `#include <typeinfo>` для MSVC++. Тогда как для GCC нужен еще и `#include <iostream>`
Но здесь я перестаю понимать суть модулей. Ведь типа модуль -- это самодостаточная сущность. Если в модуле был сделан нужный набор include-ов, то в месте, где импортированный из модуля метод/класс/функция используется, уже никаких дополнительных include не нужно.
Такое ощущение, что GCC/VC++ для шаблонов в модуле просто сохраняют какое-то частично разобранное представление, которое просто 1-в-1 переносят в место использования шаблона и окончательный парсинг/анализ делают уже только в месте инстанцирования. Поэтому т.к. в тестовой программе не подключения заголовочного файла typeinfo, то нет и определения символа type_info.
Тогда как clang делает более глубокий и честный разбор тела шаблона и с символом type_info он определяется уже при компиляции самого модуля (либо же он умудряется дополнять код тестовой программы нужными include-ами автоматически -- типа того, что если в определении модуля использовался `#include <typeinfo>`, то этот же include нужно неявно подставить и в то место, где совершен импорта модуля).
Случай второй (case_003 из упомянутого выше репозитория).
Здесь я пытался сделать хитрый финт ушами. Иногда у меня бывают очень объемные шаблоны классов, которые я разбиваю на несколько исходных файлов: один заголовочный файл с определением шаблона, но без реализации, + несколько заголовочных файлов с реализациями внутренностей шаблона.
На простых .hpp-файлах это работает без проблем. А вот как сделать подобное на модулях?
После некоторого выкуривания бамбука пришло такое "решение".
Есть отдельный module partition unit (не знаю как лучше данный термин на русский язык перевести) с определением шаблона класса (файл helper.decl.ixx):
|
export module hello:helper.decl; export namespace hello { template<typename T> struct helper { static void a(); static void b(); }; } /* namespace hello */ |
Есть два отдельных module partition unit-а с реализацией методов a и b. В файле helper.impl.a.ixx:
|
module; #include <iostream> #include <typeinfo> export module hello:helper.impl.a; import :helper.decl; export namespace hello { template<typename T> void helper<T>::a() { std::cout << "helper<" << typeid(T).name() << ">::a()" << std::endl; } } /* namespace hello */ |
И в файле helper.impl.b.ixx:
|
module; #include <iostream> #include <typeinfo> export module hello:helper.impl.b; import :helper.decl; export namespace hello { template<typename T> void helper<T>::b() { std::cout << "helper<" << typeid(T).name() << ">::a()" << std::endl; } } /* namespace hello */ |
Все это собирается затем в primary module interface unit:
|
export module hello; export import :helper.decl; export import :helper.impl.a; export import :helper.impl.b; export namespace hello { void greet(); } // export namespace hello |
Описывается в CMake так:
|
cmake_minimum_required(VERSION 3.28) project(hello CXX) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED YES) add_library(hello) target_sources(hello PUBLIC FILE_SET CXX_MODULES FILES hello.ixx helper.decl.ixx helper.impl.a.ixx helper.impl.b.ixx PRIVATE hello.cxx ) |
И в случае с clang-ом все OK. Однако.
Для GCC опять таки требуется, чтобы в тестовой программе были сделаны необходимые include. Т.е. вот так OK:
|
#include <iostream> #include <typelist> import hello; int main() { hello::greet(); hello::helper<int>::a(); hello::helper<long>::b(); } |
А без include -- ошибка компиляции.
Тогда как для MSVC++ делать include в тестовой программе не нужно, оно компилируется, но зато возникает ошибка линковки:
cmake --build .
[1/1] Linking CXX executable bin\app.exe
FAILED: [code=4294967295] bin/app.exe
C:\WINDOWS\system32\cmd.exe /C "cd . && "C:\Program Files\Microsoft Visual Studio\18\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" -E vs_link_exe --msvc-ver=1951 --intdir=app\CMakeFiles\app.dir --rc=C:\PROGRA~2\WI3CF2~1\10\bin\100261~1.0\x64\rc.exe --mt=C:\PROGRA~2\WI3CF2~1\10\bin\100261~1.0\x64\mt.exe --manifests -- C:\PROGRA~1\MICROS~2\18\COMMUN~1\VC\Tools\MSVC\1451~1.362\bin\Hostx64\x64\link.exe /nologo app\CMakeFiles\app.dir\main.cpp.obj /out:bin\app.exe /implib:lib\app.lib /pdb:bin\app.pdb /version:0.0 /machine:x64 /INCREMENTAL:NO /subsystem:console lib\hello.lib kernel32.lib user32.lib gdi32.lib winspool.lib shell32.lib ole32.lib oleaut32.lib uuid.lib comdlg32.lib advapi32.lib && cd ."
LINK: command "C:\PROGRA~1\MICROS~2\18\COMMUN~1\VC\Tools\MSVC\1451~1.362\bin\Hostx64\x64\link.exe /nologo app\CMakeFiles\app.dir\main.cpp.obj /out:bin\app.exe /implib:lib\app.lib /pdb:bin\app.pdb /version:0.0 /machine:x64 /INCREMENTAL:NO /subsystem:console lib\hello.lib kernel32.lib user32.lib gdi32.lib winspool.lib shell32.lib ole32.lib oleaut32.lib uuid.lib comdlg32.lib advapi32.lib /MANIFEST:EMBED,ID=1" failed (exit code 1120) with the following output:
main.cpp.obj : error LNK2019: ссылка на неразрешенный внешний символ "public: static void __cdecl hello::helper<int>::a(void)" (?a@?$helper@H@hello@@SAXXZ::) в функции main.
main.cpp.obj : error LNK2019: ссылка на неразрешенный внешний символ "public: static void __cdecl hello::helper<long>::b(void)" (?b@?$helper@J@hello@@SAXXZ::) в функции main.
bin\app.exe : fatal error LNK1120: неразрешенных внешних элементов: 2
ninja: build stopped: subcommand failed.
И здесь я еще в большем недоумении: может это и не вина MSVC++, а проблема в CMake?
Однако впечатление, что под видом модулей комитет очень и очень сильно поднасрал C++ разработчикам, только усилилось.
PS. В очередной раз офигел от документации к CMake. Вроде как там целый раздел посвящен поддержке модулей из C++20. Но в этом разделе я не увидел ни одного примера того, как же модули нужно декларировать в CMakeLists.txt. Пришлось гуглить и потом таскать к себе куски из разных мест.
PPS. Что еще сильно не понравилось, так это то, что с какого-то бодуна стали внедрять новые расширения для module units. Вроде как в мире MS используют .ixx, тогда как в мире clang -- .cppm. Вот нафига и, главное, зачем? Расширения для файлов в C++ном проекте должны быть такими, чтобы можно было в grep одну простую маску задать и все. Поэтому .hpp/.cpp или .hxx/.cxx -- это хорошо, а вот всякие извраты с .H/.C, .hh/.cc и прочим -- фтопку. И если разработчикам навяжут какое-то новое расширение для module units, то удобство C++а лично для меня снизится еще сильнее.
Комментариев нет:
Отправить комментарий