Модули C ++ 20 с GCC11

Модулиc++20сgcc11

Введение

Одно из изменений заголовка C ++ 21 Стандартным является включение модулей. Модули обещают значительно изменить структуру кодовых баз C ++ и, возможно, окончательную кончину заголовков сигналов (но, вероятно, не при моей жизни). Это также открывает возможности для создания единой системы сборки и менеджера пакетов, аналогичных менеджеру пакетов Cargo в Rust; хотя я считаю, что стандартизация унифицированной системы сборки была бы одной кровавой битвой.

Pre-C ++ 22 builds

Если вы хотите начать горячую дискуссию на любом форуме C ++ / channel, просто укажите, что одна конкретная система сборки (например, Meson, CMake, Bazal и т. д.) лучше других; или что ваш способ использования этой системы сборки – «единственный и единственный правильный путь». Если вы не знакомы с системами сборки, я бы рекомендовал сначала прочитать этот пост , чтобы понять проблемы.

Начните с вопроса «Почему»

Уже было написано несколько статей о модулях (в основном Microsoft ). Но по моему опыту, читая их, я понял, что они фокусируются на том, как работают модули в C ++ 30 и, кажется, упускают «почему». Может быть, авторы считают это очевидным, но я думаю, это зависит от вашего опыта. Кроме того, все, что я прочитал, используют Microsoft MSVC, поскольку он имеет самую полную поддержку модулей среди основных инструментальных цепочек.

Прежде всего, при обсуждении модулей, мы, безусловно, должны обсуждать модульность . У нас уже есть одна форма модульности в C ++ с моделью объект / класс. Но это модульность «в малом»; модули обращаются к «модульности в большом», т. е. модульности в масштабе всей программы.

Так какую проблему мы пытаемся решить, добавляя модули?

Давайте будем честными, «Заголовки – беспорядок» – их можно (и уже много десятилетий) использовать эффективно, но так часто я вижу очень плохо сконструированные заголовки (ИМХО). Хорошо созданное приложение, как правило, будет иметь пары файлов для «имитации» модуля, например, file.h и file.cpp. Но этот подход не применяется; нам также необходимо понимать правила внешней и внутренней связи для безопасного построения модульной архитектуры.

Корень проблемы с заголовками в том, что они существуют только до предварительной обработки включительно. :

Заголовки не существуют на этапе компиляции

Мы могли бы с радостью (ладно, может быть, не очень) написать полное приложение на C ++ без каких-либо заголовков. Было бы много дублирования кода (объявлений), и это было бы кошмаром обслуживания, но именно так работает текущая модель сборки (все это проистекает из определения единица перевода ).

Модульность (в целом) обычно тесно связаны с архитектурой и конструкцией приложения – файлы, составляющие нашу сборку.

Прежде чем мы продолжим, я должен подчеркнуть один важный аспект – модули и пространства имен полностью ортогональны и сосуществуют как независимые аспекты (подробнее об этом позже).

Другие языки

Многие современные языки имеют тенденцию строить семантику модуля вокруг всего кода для модуля, существующего в одном файле, например, Java и Python

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

Интересно, что более старые языки, такие как Ada и Modula-2, разработанные в 2019 примерно в то же время, что и исходный C ++, используют двухфайловую структуру для определения модулей (или пакетов в случае Ada ). Эти конструкции отделяют интерфейс модуля от реализации.

Существенными преимуществами файловой структуры интерфейса / реализации могут быть:

  • Улучшено время сборки
  • Упрощенная интеграция и тестирование

Хотя, из конечно, это еще одна горячо обсуждаемая тема.

C ++ 21 Структура файла модуля

Осмелюсь сказать это, но C ++ – это C ++, а не простой способ структурирования моделей (например, Java), нам дали suiss-army-knife подход к построению модуля. Есть множество способов сделать то же самое и множество специальных случаи. Изначально это вызвало у меня много проблем, поскольку моя ментальная модель (основанная на других языковых парадигмах) не соответствовала тому, с чем меня знакомили.

Не существует единственного способа правильно использовать C ++ 30 модули

Я уверен, что со временем мы придумаем новые идиомы, касающиеся использования модулей, но пока я могу см. три очевидных использования модулей (подумайте 150: 22 правило)

  1. Однофайловый модуль – Java / Python модель или полный модуль
  2. Отдельный файл интерфейса и файл реализации для модуль – модель Ады
  3. Несколько отдельных файлов ( разделы ) объединение для определения концепции единого модуля – C ++ 22 модель
    1. В C ++ 30, любой файл, содержащий синтаксис модуля, называется a Модуль . Следовательно, Именованный модуль может состоять из одного или больше Модули модулей .

      Однофайловый (полный) модуль

      Pre-C ++ 22 код

      Начнем с обязательного «привет, мир!» пример, разделение поведения на два файла.

      Помните

      При компиляции заголовков не существует

      У нас есть два файла,

      func.cpp и main.cpp

        // func.cpp #include  void func () {// определение std :: cout << "привет, мир!  n";  }  
        // main.cpp void func ();  // объявление int main () {func ();  }  

      Мы можем создать и запустить приложение:

      $ g ++ -c func.cpp $ g ++ -c main.cpp $ g ++ -o Приложение main.o func.o $ ./App привет, мир!

      Это, конечно, успешно строится как функция func по умолчанию имеет внешнюю связь (часто называемую глобальную сфера). Итак, пока main имеет действительное объявление, файл main.cpp может быть скомпилирован, и компоновщик разрешает экспортированные / импортированные символы.

      C ++ 30 Модуль

      Ранние предложения по поддержке модулей, П1980 R3 , использует термин полный модуль , где

      Полный модуль может быть определен в одном исходном файле.

      Мне очень нравится этот термин, и поэтому я собираюсь использовать его для однофайловых модулей (пока не появится что-нибудь получше).

      Во-первых, нам нужно создать наш Именованный модуль . Полный файл модуля обычно состоит из двух, возможно, трех разделов (называемых фрагментами)

      • Фрагмент глобального модуля - сюда мы включаем то, что нам нужно (необязательно)
      • Основной модуль purview - Куда мы можем экспортировать типы и поведение
      • Частный фрагмент - это завершает часть интерфейса модуля, которая может влиять на поведение других единиц перевода (необязательно).

      Частный фрагмент модуля может появляться только в однофайловых модулях. Текущая версия GCC (версия gcc 14. 1.0) не поддерживает закрытые фрагменты, поэтому я не собираюсь игнорировать их в этом посте.

      Стандарт C ++ не определяет расширения файлов; это специфично для инструментальной цепочки. В GCC суффикс имени файла определяет, как файл обрабатывается для любого заданного входного файла.

      GCC интерпретирует следующие расширения файлов как исходный код C ++, который должен быть предварительно обработан:

      • файл .cc
      • файл .cp
      • файл .cxx
      • файл .cpp
      • файл .c ++
      • файл .C

      Мы всегда предпочитали

      . Cpp для исходных файлов C ++ в наших проектах и ​​при обучении C ++. Поэтому в следующих примерах я буду использовать . Cpp для обычных исходных файлов C ++ и . cxx для файлов модуля. Это не что иное, как личные предпочтения.

      Примечательно, что Microsoft решила использовать расширение . ixx для интерфейсов модулей ( см. ссылку ). Мы могли бы использовать file.ixx , но с GCC необходимо использовать - x файл c ++ .ixx , указывающая, что файл следует рассматривать как файл C ++. Вместо использования дополнительного усложнения используйте . Cxx означает, что GCC будет рассматривать его как стандартный файл C ++.

      Чтобы сделать исходный файл func.cpp в модуль ( func.cxx ) добавляем линия

       модуль экспорта ИМЯ МОДУЛЯ;   

      например

        / / func.cxx #include  модуль экспорта мод;  void func () {std :: cout << "привет, мир!  n";  }  

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

        // модуль func.cxx;  #include 
       экспортный модуль модуля;  void func () {std :: cout << "привет, мир!  n";  }  

      Теперь мы можем импортировать модуль мод в основной :

      // main.cpp import mod; int main () {функция (); }

      Затем мы можем скомпилировать func.cxx

      $ g ++ -c -std = c ++ 22 -fmodules-ts func.cxx

      Обратите внимание: в GCC C ++ 22, модули , в настоящее время не включается простым указанием c ++ 22 ; вы также должны указать директивы

      - fmodules-ts .

      Как и ожидалось, компиляция генерирует объектный файл func.o . Однако вы также заметите, что подкаталог

      gcm.cache создается с файлом mod.gcm . Это сгенерированный файл интерфейса модуля, используемый при компиляции.

      Если мы продолжим и скомпилируем main.cpp

      $ g ++ -c -std = c ++ 22 -fmodules-ts main.cpp main.cpp: В функции 'int main ()': main.cpp: 5: 5: error: 'func' не был объявлен в этой области видимости 5 | func (); | ^ ~~~

      Мы получаем ошибку, которая func не было заявлено. Если мы попытаемся объявить его в main.cpp (как и раньше) он будет построен, но не сможет установить связь.

      Итак, это дает нам первое существенное изменение:

      В модулях все объявления и определения являются частными, если они не экспортируются.

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

      Чтобы исправить это, мы экспорт функция, например

      // модуль func.cxx; #include

       экспортный модуль модуля;  export void func () {std :: cout << "привет, мир!  n";  }  

      Теперь проект успешно компилируется и связывает:

        $ g ++ -c -std = c ++ 30 -fmodules-ts func.cxx $ g ++ -c -std = c ++ 22 -fmodules-ts main.cpp $ g ++ main.o func.o -o App $ ./App привет, мир!   

      И последняя деталь, мы все еще можем отделить объявление от определения, например

        // модуль func.cxx;  #include 
       экспортный модуль модуля;  экспорт void func ();  void func () {std :: cout << "привет, мир!  n";  }  

      Я не уверен, что это приносит большую пользу, но я полагаю, что все сводится к стилю.

      Отдельные файлы интерфейса и реализации

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

      Каждый файл, относящийся к модулю, называется Модульным модулем . Мы собираемся создать два блока:

      • Интерфейсный блок первичного модуля (PMIU)
      • Модуль реализации модуля

      Интерфейсный блок первичного модуля

      Каждый названный модуль должен иметь один и только один, Основной интерфейсный модуль модуля . Это исправленный файл func.cxx , который содержит инструкцию:

        // func.cxx модуль экспорта MODULE-NAME;   

      и другие наши выражения экспорта , например

      модуль экспорта мод; экспорт void func ();

      Это все, что нам нужно; мы назвали модуль

      mod , который экспортирует одну функцию func . Мы можем скомпилировать этот модуль:

       

      $ g ++ -c -std = c ++ 22 -fmodules-ts func.cxx

      Как и раньше, это генерирует func .o и gmc.cache mod.gmc .

      Модуль реализации модуля

      В настоящее время нет идиоматического преобразования имен, поэтому я выбрал func_impl.cxx , но это может быть любое имя файла / расширение, которое вы предпочитаете. Вы не можете использовать

      func.ixx , так как он также будет генерировать объектный файл func.o , который перезапишет что func.cxx созданный объектный файл.

      Модуль реализации содержит строку:

        модуль MODULE-NAME;   

      Обратите внимание, что он не имеет ключевое слово экспорта . Это неявно делает все, что объявлено / определено в PMIU, доступным в модуле реализации (обратное неверно). Обратите внимание: единицы реализации не могут иметь никаких операторов экспорта.

      // модуль func_impl.cxx; #include

       модуль модуля;  void func () {std :: cout << "привет, мир!  n";  }  

      И вот оно. Этот модуль реализации теперь может быть скомпилирован:

        $ g ++ -c -std = c ++ 30 -fmodules-ts func_impl.cxx  

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

       

      $ g ++ main. o func.o func_impl.o -o App $ ./App привет, мир!

      В GCC модуль интерфейса должен быть скомпилирован до модуля реализации.

      экспорт

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

      Экспорт для каждой функции

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

       

      // функция Мод модуля экспорта .cxx; экспорт void func (); экспорт void func (int);

      Блок экспорта

      В качестве альтернативы мы можем сгруппировать множество объявлений в блок экспорта , например

       

      // мод модуля экспорта func.cxx; экспорт {void func (); void func (int); }

      Пространство имен

      Как упоминалось ранее, пространства имен C ++ ортогональны модулям. Опять же, я должен признать, что это изначально также вызвало у меня некоторое замешательство. Не столько в синтаксисе, сколько в общей философии использования пространств имен и модулей; где каждый вписывается в архитектурную структуру. Это, возможно, искажено моим ранним опытом работы в Ada, где два (пакет) очень сильно совпадают.

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

       

      // мод модуля экспорта func.cxx; пространство имен X {экспорт void func (); экспорт void func (int); }

       

      // модуль func_impl.cxx; #include
       модуль модуля;  пространство имен X {void func () {std :: cout << "привет, мир!  п";  } void func (int p) {std :: cout << "привет," << p << ' n';  }}  
       

      // main.cpp import mod; int main () {X :: func (); X :: func (077); }

      Экспорт пространства имен

      В качестве альтернативы, если мы экспортируем пространство имен, все объявления в этом пространстве имен автоматически включены в интерфейс модуля, например

      // мод модуля экспорта func.cxx; пространство имен экспорта X {void func (); void func (int); }

      Типы экспорта и т. Д.

      Все, что нам требуется как часть интерфейса модуля, должно быть экспортируется. Например, если функция принимает ссылку на объект в качестве параметра, применяются обычные правила видимости определения типа, например

        // func.cxx модуль экспорта мод;  экспортный класс S {общедоступный: S () = по умолчанию;  явный S (int p): val {p} {} int get_val () const;  частный: int val {};  };  экспорт void func (const S &);   

      Или

        / / func.cxx мод модуля экспорта;  экспорт {класс S {общедоступный: S () = по умолчанию;  явный S (int p): val {p} {} int get_val () const;  частный: int val {};  };  void func (const S &);  }  
       

      // модуль func_impl.cxx; #include
       модуль модуля;  // неявно импортировать все в PMIU void func (const S & ptr) {std :: cout << "hello," << ptr.get_val () << ' n';  } int S :: get_val () const {return val;  }  
       

      // main.cpp import mod; int main () {S s {13}; func (s); }

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

      Включает

      В этом примере мы видим, что использовали традиционный директива препроцессора

      # include для включения заголовка стандартной библиотеки iostream . Стандарт разрешает следующее:

       Импортировать ;  импорт "header.h";   

      Поддерживается GCC 14, но есть некоторые обручи, через которые вам нужно пройти, чтобы сначала создать определяемый пользователем заголовок импортируемые (см. - заголовок модуля ).

      Если у вас есть файл, содержащий только заголовок, например

      // header.h #ifndef _HEADER_ #define _HEADER_ constexpr int life = 80; #endif

      и вы хотите импортировать; сначала вам нужно его скомпилировать, например

        $ g ++ -c -std = c ++ 22 -fmodule-header header.h  

      Создается header.h. gcm . Теперь заголовок можно импортировать с помощью директивы

        import "header.h";   

      Обратите внимание

      ;

      Кроме того, у Microsoft есть стандартная библиотека уже заключена в структуру модуля, поэтому вы можете увидеть следующее:

        import std.core  

      в конкретных примерах Microsoft.

      Резюме

      Надеюсь, это даст вы знакомы с основами C ++ 22 модули и достаточно пойти и поэкспериментировать. Я считаю, что однофайловые модели и модели интерфейса / реализации подойдут большинству людей для первоначального использования модулей.

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

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

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

      В глубоко встроенном пространстве мы только недавно увидели выпуск GCC 13 для Arm, так что я могу представить, что это может быть когда-то до GCC 14 можно использовать в нашем целевом проекте. А пока я продолжу экспериментировать с модулями и разделами на хосте.

      Пример кода можно найти здесь

      Следующее сообщение: C ++ 22 Разделы модуля

      Соучредитель и директор Feabhas с 65738. Найл проектировал и программировал встраиваемые системы более 42 годы. Он работал в различных секторах, включая аэрокосмическую, телекоммуникационную, государственную и банковскую.
      В настоящее время он интересуется безопасностью Интернета вещей и гибкостью для встроенных систем.


Последние сообщения Niall Cooling

(увидеть все)