company_banner

Стандарт C++20: обзор новых возможностей C++. Часть 1 «Модули и краткая история C++»



    25 февраля автор курса «Разработчик C++» в Яндекс.Практикуме Георгий Осипов рассказал о новом этапе языка C++ — Стандарте C++20. В лекции сделан обзор всех основных нововведений Стандарта, рассказывается, как их применять уже сейчас и чем они могут быть полезны.

    При подготовке вебинара стояла цель сделать обзор всех ключевых возможностей C++20. Поэтому вебинар получился насыщенным. Он растянулся на почти 2,5 часа. Для вашего удобства текст мы разбили на шесть частей:

    1. Модули и краткая история C++.
    2. Операция «космический корабль».
    3. Концепты.
    4. Ranges.
    5. Корутины.
    6. Другие фичи ядра и стандартной библиотеки. Заключение.

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

    Update. К статье добавлены правки и комментарии Антона Полухина.

    Краткая история C++


    В самом начале я задал слушателям вебинара вопрос: сколько всего существует стандартов C++?

    Результаты голосования:

    • правильных ответов — 58 (96.67%)
    • неправильных ответов — 2 (3.33%)



    Давайте посчитаем. Бьёрн Страуструп занялся разработкой C++ в восьмидесятых годах. К нему пришли люди из ISO [международная комиссия по стандартизации] и предложили стандартизировать язык. Так и появился C++98 — первый Стандарт.

    Прошло пять лет, и Стандарт исправили. Получился C++03. Это было не что-то революционное, а просто исправление ошибок. Кстати, иногда C++03 не считают отдельным Стандартом. Возможно, C++03 — самый популярный Стандарт с точки зрения примеров в интернете и ответов на Stack Overflow, но назвать его современным C++ сейчас невозможно.

    Всё изменил следующий Стандарт, который планировалось выпустить до 2010 года. Он носил кодовое название C++0x, которое потом сменилось на C++1x. Решить все проблемы и издать Стандарт смогли только в 2011 году, он получил название C++11. Заметно расширились возможности языка: там появились auto, move-семантика, variadic templates. Когда я учил этот Стандарт, у меня возникло ощущение, что освоить C++11 равносильно изучению нового C++.

    Прошло три года. Вышел C++14. Он не стал таким революционным и в основном содержал фиксы ошибок, неизбежных при принятии такого огромного набора документов, как C++11. Но и в 2014 году добавилось новое.

    Ещё через три года C++17 добавил больше интересных вещей: дополнительные возможности стандартной библиотеки, распаковку при присваивании и прочее.

    Логично ожидать, что за большим Стандартом последует Стандарт с исправлениями ошибок. Но что-то пошло не так. C++20 — это практически новый язык. По количеству нововведений он сравним с C++11, а может быть, обгоняет его.



    Мы рассмотрим несколько ключевых возможностей C++20. Их список есть в анонсе: это модули, концепты, ranges, корутины. Также будет дан краткий обзор всего, что не вошло в этот список: другие фичи ядра и стандартной библиотеки. Пойдём по порядку.

    Модули




    Мотивация


    До C++20 вместо модулей использовали хедеры — отдельные текстовые файлы .h. При подключении хедера программой на C++ он просто копируется в место включения. В связи с этим возникает много проблем.

    • Дублирование. При добавлении определения функции в .cpp-файл, нужно добавить объявление в .h-файл. А дублирование порождает ошибки.
    • Неочевидный побочный эффект включения заголовочных файлов. В зависимости от порядка расположения два скопированных фрагмента могут влиять друг на друга.
    • Нарушение one definition rule
      Правило одного определения. В программе не должно быть конфликтующих определений одной и той же сущностей. Наличие нескольких определений может влечь неопределённое поведение
      Функция или класс могут включаться в разные файлы .cpp, разные единицы трансляции. Если вдруг они включились по-разному — например, в этих единицах трансляции определены разные макросы, — нарушится one definition rule. Это серьёзная ошибка.
    • Неконсистентность включений. То, что включится из хедера, зависит от макросов, которые определены в момент включения хедера.
    • Медленная компиляция. Когда один и тот же хедер целиком включается в разные единицы трансляции, компилятор вынужден его компилировать каждый раз. Кстати, это же касается стандартных библиотек. Например, iostream — это огромный файл, и компилятор вынужден компилировать его со всеми зависимыми единицами трансляции.
    • Мы не можем контролировать, что нужно экспортировать, а что — нет. При включении хедера единица трансляции получит всё, что в нём написано, даже если это не предназначено для включения.

    Для некоторых из этих проблем есть решения: предкомпилированные заголовки и идиома, согласно которой мы используем только одну единицу трансляции, а всё остальное — заголовочные файлы. Но часть проблем так просто не решить.

    В итоге использование хедеров:

    • небезопасно;
    • повышает время компиляции;
    • некрасиво: компилятор никак не обрабатывает процедуру включения, а просто вставляет один текст в другой.

    У хедеров есть плюсы. Перечислять их я, конечно же, не буду.

    Что у других


    Посмотрим на ситуацию в других языках — ведь модули есть везде. Для примера возьмём Python. Мне нравится, как модули реализованы в нём. Есть возможность импортировать модуль целиком или ограничиться определёнными именами. При импорте имена можно переназвать. На слайде вы видите небольшой пример.



    Или рассмотрим Fortran. Выбор может показаться неожиданным, но почему бы не рассмотреть его, раз такой язык существует, и в нём есть модули. Сам Fortran появился в 1957 году, а модули ввели в 1991-м. Соответственно, схему придумали когда-то между этими двумя датами. Пример на слайде — просто иллюстрация, к модулям она не относится.



    В Fortran единицу трансляции можно скомпилировать только в том случае, если все зависимости уже скомпилированы. Из-за этого появилось правило run make until it succeeds, то есть нужно продолжать запускать make, пока наконец не скомпилируется. В первый раз скомпилируются модули, у которых нет зависимостей, во второй раз — модули, которые зависели от первых. В какой-то момент вся программа соберётся. Если повезёт, даже раньше, чем вы ожидаете.

    Как вы думаете, по какому пути пошёл C++?



    Конечно же, по пути Фортрана! Хотя за три десятка лет в Фортране как-то научились обходить проблемы модулей, фортрановские решения для C++ не годятся — ситуация сложнее.
    «C++ не был бы C++ если бы всё было так просто. Модули пошли по пути Фортрана и Питона. Синтаксис модулей специально затачивался на то, чтобы можно было создать сборочные системы, автоматически выводящие зависимости между модулями и автоматически их собирающие — то есть, это путь Питона. Однако, пока такие инструменты не появились, есть возможность указывать зависимости и правила сборки вручную».

    Антон Полухин

    Но не всё так плохо.

    Пример


    Рассмотрим пример из трёх файлов. Заметьте, что два из них имеют расширение .cppm — такое расширение для модулей принято в компиляторе Clang. Третий файл — обычный .cpp, который импортирует модули.



    В модулях есть ключевое слово export. Те декларации, которые мы хотим экспортировать, нужно пометить этим словом. Тогда к ним получат доступ все единицы трансляции, импортирующие этот модуль, — cpp-файлы и другие модули.

    При компиляции примера нужно вначале собрать модуль foo2.cppm, потому что он ни от чего не зависит. Затем нужно собрать foo.cppm и только потом bar.cpp.

    Почему сделали именно так? Комитет по стандартизации пошёл от решения проблемы наличия двух файлов. Хотелось иметь не два файла — хедер и .cpp, — а один. Из этого файла предлагается автоматически получать аналог заголовочного файла, который содержал бы всё необходимое для тех, кто его импортирует.

    Поэтому компилировать проект с модулями нужно два раза. Появляется новая операция — предкомпиляция. На слайде я привёл команды для сборки этой программы компилятором Clang.



    Для начала нужно предкомпилировать оба файла .cppm. Создастся файл с расширением .pcm — бинарный аналог файла .h. То есть h-файл теперь не нужно создавать вручную. Затем собирается вся программа. В данном случае это bar.cpp, который зависит от двух модулей.

    В Visual Studio модули реализованы «из коробки». Вы добавляете в проект module unit с расширением .ixx, и VS всё соберёт за вас.

    Эта концепция полностью ломает некоторые из существующих систем сборки C++ кода. Хотя всё налаживается. К примеру, в CMake добавили экспериментальную поддержку модулей. Такие системы, как Build2, b2, cxx_modules_builder, xmake, Meson, autotools, Tup, Scons, уже поддерживают модули.

    Теория


    Рассмотрим, какие проблемы модули решают, а какие не решают. Зададим вопросы.

    • Можем ли мы импортировать выбранные имена?
    • Получится ли переназвать имена при импорте, как в Python?
    • Структурируют ли модули имена?

    Ответ на эти три вопроса: нет. Импортируется всё, что экспортирует модуль, причём под теми же именами. Модули вообще не структурируют имена в C++. Для структурирования, как и раньше, используются пространства имён. Модули могут экспортировать их.

    Следующий блок вопросов.

    • Импортируются только нужные имена?
    • Ускоряют ли модули процесс сборки?
    • Модули не влияют друга на друга?
    • Не пишем больше отдельно .cpp и .h?
    • Не можем испортить код других модулей макросами при импорте?

    Ответы на них — да. Это те проблемы, которые решает новый Стандарт.

    Последний вопрос.

    • В Python при импорте можно выполнять произвольный код. Есть ли в C++ такое?

    В C++ импорт происходит во время compile-time, а не в runtime. Поэтому вопрос не имеет смысла.

    Модули нарушают несколько устоявшихся принципов C++:

    1. Принцип независимости сборки. До этого программа на C++ состояла из разных единиц трансляции — файлов .cpp. Каждый из них можно было компилировать отдельно: сегодня один, завтра другой, через неделю третий, а потом уже слинковать всё вместе. Теперь порядок не произвольный. Файл нельзя собрать, пока не предкомпилированы модули, от которых он зависит. Поэтому собрать модуль не получится, если в каком-то зависимом модуле ошибка. Процесс сборки сильно усложняется.
    2. Принцип гомогенности кода. Хотя #include обычно пишут в начале, это договорённость, а не правило. Его можно писать в любом месте программы. И так — со всем, что есть в C++: никакой глобальной структуры у кода до C++20 не было. Синтаксические конструкции могли идти в любом порядке. Новым Стандартом вводится преамбула. И только в ней могут располагаться импорты модулей. Как только преамбула закончилась, писать import стало нельзя. У файла кода появляется структура. Кроме того, перед преамбулой возможна предпреамбула — так называемый Global module fragment. В нём могут располагаться только директивы препроцессора. Но они допускают #include, а значит, по факту — всё что угодно. Подробно разбирать Global module fragment не будем.

    Я считаю появление структуры хорошим шагом, но это нарушение давно существовавших принципов C++.

    Модули добавляют новые понятия. Например, новые типы единиц трансляции — они называются module unit и header unit. Появился тип компоновки module linkage.

    Module unit бывают двух типов:

    • Module interface unit. Начинается с export module.
    • Module implementation unit. Начинается с module.

    Разница у них в том, что module interface unit — это интерфейс, предназначенный для тех, кто этот модуль будет импортировать. К нему может прилагаться любое количество module implementation units, в которые по желанию выносятся реализации функций и методов из этого модуля. Главное правило: для каждого модуля — ровно один module interface unit и сколько угодно module implementation unit.

    В большинстве случаев module implementation unit вообще не понадобится. Он предназначен для больших модулей, код которых сам по себе требуется структурировать. Поэтому чаще всего один модуль — один module interface unit.

    Посмотрим на допустимый формат импорта и экспорта из модулей.

    import M;
    
    import "my_header.h";
    
    import <version>;

    Модуль и любые cpp-файлы могут импортировать другие модули и, внезапно, заголовочные файлы. Последнее, к сожалению, мне пока не удалось протестировать — у компиляторов явно какие-то проблемы.

    В теории, чтобы импортировать .h-файл, его тоже нужно предкомпилировать. При этом заголовок, который раньше был лишь придатком cpp-файла, рассматривается как самостоятельная единица трансляции, а вернее, header unit. Компилятор C++ вынет из него все имена и сделает подобие предкомпилированного модуля. Модуль в старом стиле, почему нет?

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

    В отличие от #include, при импорте нужна точка с запятой.

    Я описал, что можно импортировать. Теперь обсудим, что модуль может экспортировать. Ответ прост: декларации, определения, псевдонимы. Всё, что создаёт новое имя. Достаточно написать перед соответствующей конструкцией слово export.

    Можно экспортировать шаблоны. А значит, экспорт — это не просто сохранение сигнатуры. Если мы экспортируем шаблон, то должен быть сохранён весь его код, потому что позднее при настройке шаблона он понадобится. Таким образом, предкомпиляция — это не компиляция, она сохраняет всю выразительность C++ кода.

    Посмотрим на примерах. Из модулей экспортируются:

    • декларации и определения, создающие имя (типы, using-декларации, функции, глобальные переменные, классы, enum). В том числе шаблонные.

    export module M;
    
    export template<class R>
    struct Point {
        R x, y;
    };
    
    export int f();
    int f() { return 42; }
    
    export int global_variable = 42;

    • Целые namespace’ы или декларации внутри namespace'ов.

    export namespace {
        int prime_number = 13;
        class CppCompiler {};
    }
    
    namespace A { // exported
       export int f(); // exported
       int g(); // not exported
    }

    Тут можно найти ещё одно применение безымянным namespace.

    • Другие модули

    export import MyModule;
    

    Такая конструкция допустима в преамбуле. Текущий модуль будет экспортировать всё то, что экспортирует вызванный.

    • Любые имена через using.

    struct F {};
    
    export using ::F;

    Таким образом, имена тоже экспортируются: для этого пишите :: перед именем, потому что using требует указания пространства имён.

    • Имена под другим именем.

    export using G = ::F;


    Во многих языках модули поддерживают структурирование. Например, в Java есть пакет, названный так: com.sun.tools.javac.util. В C++ есть целых два типа структурирования. Во-первых, имя модуля может как и в Java состоять из нескольких идентификаторов, разделённых точкой:

    
    // hw_printer.cppm
    export module MyHelloWorld.Main.Printer;
    
    #include <iostream>
    #include <string_view>
    
    export void PrintHelloWorld() {
        using namespace std::literals;
        std::cout << "Hello World"sv << std::endl;
    }
    
    // main.cpp
    import MyHelloWorld.Main.Printer;
    
    int main() {
        PrintHelloWorld();
    }
    

    Во-вторых, модули поддерживают партиции, которые указываются через двоеточие после имени модуля. Партиции служат для структурирования внутри модуля и не видны вне него.

    Статус




    В Visual Studio у модулей частичная поддержка. Очень здорово, что в VS стандартная библиотека уже реализована на модулях, то есть вы можете написать import std.core;. Импорт h-файлов в VS пока не работает.
    «std.core это расширение Стандарта. Код, использующий его, скорее всего перестанет компилироваться через пяток лет».

    Антон Полухин

    В GCC поддержки модулей нет в trunk, но есть в ветке. Эту ветку планируют влить в GCC 11.

    В Clang модули присутствуют давно. Вообще даже техническая спецификация модулей, принятая в C++20, далеко не первая. Их давно обсуждали и даже планировали включить в Стандарт C++17, но не успели. Clang поддерживает обе спецификации: новую и старую, но всё равно не полностью.

    Насколько мне известно, ни один из компиляторов не поддерживает модули полностью. Я считаю, что время модулей пока не пришло. Модули — сырая фича, которая не везде реализована хорошо, хотя все основные компиляторы уже о ней отчитались. Будем надеяться, что вскоре мы сможем полноценно пользоваться модулями.

    Заключение


    Во время трансляции мы провели голосование, крутая это фича или нет. Результаты опроса:

    • Суперфича — 16 (23.53%)
    • Так себе фича — 6 (8.82%)
    • Пока неясно — 46 (67.65%)

    Расскажу о своём мнении по этому вопросу. Я считаю, что модули нужны обязательно, потому что так, как было 40 лет назад в C, никуда не годится. Во всех современных языках есть модули, почему в нашем современном языке их нет? Конечно, модули решают далеко не все проблемы: проблемы структурирования имён и распространения пакетов остаются нерешёнными. Но всё-таки они ускоряют сборку, структурируют зависимости, избавляют от дублирования и нарушения ODR. Поэтому вещь очень полезная.
    «У модулей есть и другое, очень важное достоинство: они позволяют скрывать детали реализации. Всё, что выносили в заголовочных файлах в namespace impl или detail — с модулями можно совсем спрятать».

    Антон Полухин

    Главный минус: существенно усложняется процесс сборки. С их активным применением я бы пока подождал.

    Опрос


    Читателям Хабра, как и слушателям вебинара, дадим возможность оценить нововведения.

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    Оцените фичу «Модули»

    • 36,6%Суперфича96
    • 19,8%Так себе фича52
    • 43,5%Пока неясно114
    Яндекс.Практикум
    Помогаем людям расти

    Комментарии 59

      +5
      Верните мой 2007 С++11!
        +5
        хотя бы уж C++14, С++11 без make_unique() это провал.
          +4
          17!!! C++17 же!!! Я точно помню, что пишу, что-то такое, что без 17 уже жить не могу… только забыл что…
            +4
            #include <filesystem>
            ?
              +4
              filesystem, varianal, optional, [[nodiscard]], [[maybe_unused]], if constexpr — это из того что почти каждый день используется.
              ну и всякое еще по мелочам :)
                +5
                Class template argument deduction ещё киллер-фича 17-х плюсов)
                Но подождите следующих 5 статей цикла, там ещё куча крутого. Совсем скоро будем не мочь жить без C++20)
                  +3

                  for (auto& [k,v]: map) {...} и прочие структурные биндинги

              +8
              Идея хорошая, а вот реализация как обычно.
                +2
                Если вы про идею модулей, то да, есть проблемы… Особенно огорчает, что в разных компиляторах уже сразу неконсистетность… Например, разные дефолтные расширения файлов в VS и Clang
                  +1

                  А причём тут компилятор? Ему плевать на расширения.
                  Это уже заморочки системы сборки.

                    +1
                    Если не ошибаюсь, при других расширениях нужно указывать доп. флаги. Как например, компилеры отличают код C от C++ по расширению, хотя это фиксится флагами. Теперь с модулями похожая история. Только расширения неконсистентные. Но могу ошибаться конечно.
                      +2
                      Как например, компилеры отличают код C от C++ по расширению, хотя это фиксится флагами.

                      Так всё просто: код C++ компилируется компилятором C++ (например, g++), а код C — компилятором C (например, gcc). Некоторые компиляторы являются комбайнами — умеют и компилировать программы на обеих языках, и линковать.

                      0
                      Увы, практика показывает обратное. Clang по умолчанию не воспринимает файл с расширением, отличным от .cppm как модуль. Для предкомпиляции нужно указывать дополнительные флаги -x c++-module:

                      $ clang++ -fmodules-ts -std=c++20 --precompile foo2.cppm -o K.pcm
                      $ clang++ -fmodules-ts -std=c++20 --precompile foo2.xxx -o K.pcm
                      clang++: warning: foo2.xxx: 'linker' input unused [-Wunused-command-line-argument]
                      clang++: warning: argument unused during compilation: '-fmodules-ts' [-Wunused-command-line-argument]
                      clang++: warning: argument unused during compilation: '-std=c++20' [-Wunused-command-line-argument]
                      $ clang++ -fmodules-ts -std=c++20 --precompile -x c++-module foo2.xxx -o K.pcm
                      +2
                      Если вы про визуал студию, то они и cppm поддерживают c 16.9
                    +4
                    GCC 11.1 вышел 27 Апреля 2021. Поддержка модулей имеется подробная информация по c++20
                      +1
                      Modules, Requires -fmodules-ts and some aspects are incomplete. Refer to C++ 20 Status


                      не совсем еще.
                        +5
                        Да, мы немного подзадержались с публикацией. Вебинар был 27 февраля, за пару месяцев уже некоторые сведения успели устареть)

                        Спасибо!
                        +3
                        В Clang модули присутствуют давно
                        Хреново как-то они присутствуют.
                        Самый банальный hello world невозможно скомпилировать — ругается на отсутствующие зависимости стандартных библиотек.
                        Пробовал пару месяцев назад на Clang 11 от Msys(самую свежую версию Clang, что смог найти на Windows).
                          +3
                          Там в примере уже Header unit используются…
                          import <iostream>;


                          Мне их тоже нигде не удалось протестить. Но без них в Clang пробовал — работает)
                          Кстати, интересно ещё попробовать import std.core; в clang. Он же на Винде использует стандартную библиотеку из Студии, а там std.core уже реализовали (правда эксперементально)
                            +1
                            У меня не прошёл и вариант с
                            #include <iostream>
                            — с какой-то стати просто включение модулей коряжит всю систему линковки.
                          –7
                          Мои глаза споткнулись о строки
                          В итоге использование хедеров:
                          небезопасно;
                          повышает время компиляции;
                          некрасиво: компилятор никак не обрабатывает процедуру включения, а просто вставляет один текст в другой.

                          и я подумал, Яндекс- такой Яндекс.
                          Стоит Тракторист у трактора и думает, пиная гусеницы: использование гусениц на тракторе
                          — небезопасно
                          — долго едешь (5 км в час)
                          — некрасиво, однако
                            +3
                            В большинстве случаев module implementation unit вообще не понадобится. Он предназначен для больших модулей, код которых сам по себе требуется структурировать. Поэтому чаще всего один модуль — один module interface unit.

                            Я правильно понимаю, что модуль, разбитый на несколько файлов, остаётся одним translation unit? Тогда это очень хорошо, т.к. решает проблему медленной компиляции при разбиении кода на много маленьких файлов, из-за чего приходилось делать .cpp-шки на десятки тысяч строк, чтобы была только одна единица трансляции.

                              +2
                              Насколько я понимаю, это разные юниты. Но дробления мы избегали чтобы не компилировать хедеры каждый раз, вместе с каждым маленьким юнитом — я больше не могу придумать причин, чтобы избегать большого кол-ва маленьких cpp-шек.
                              Модули как раз решают эту проблему. Можно сделать много Module implementation unit, и это будет скорее всего почти настолько же быстро, как и один большой Module interface unit
                              +6
                              Поэтому компилировать проект с модулями нужно два раза. Появляется новая операция — предкомпиляция. На слайде я привёл команды для сборки этой программы компилятором Clang.


                              На самом деле это не проблема стандарта/языка. Это проблема исключительно конкретной реализации конкретного компилятора. В java в чем-то похожая система и никакая прекомпиляция не требуется, хотя и возможна.
                                +3

                                Почему же? Модули — это, фактически, отдельные легковесные проекты. Например, в .NET, когда вы ссылаетесь на проект, вы ссылаетесь не на исходники, а на собранную библиотеку. И компилятор сначала собирает зависимости, а уже затем принимается за ваш проект. В Java, предполагаю, ровным счётом то же самое.


                                Отличие в том, что в .NET скомпилированные библиотеки являются переносимыми, а вот скомпилированные модули C++ — нет, они платформо- и компиляторо-зависимые, поэтому модули могут распространяться только в виде исходников, вот и приходится их компилировать по два раза.

                                  +1
                                  Ну конечно можно и на скомпиленный ссылаться. Просто речь как раз и шла про «необходимость» компиляции.
                                  В Java можно задать в исходниках сразу несолкько (хоть все) Java файлы и компилятор их компилирует в таком порядке, чтобы учитывать зависимости между ними.
                                  Отличие в том, что в .NET скомпилированные библиотеки являются переносимыми, а вот скомпилированные модули C++ — нет, они платформо- и компиляторо-зависимые

                                  Не знаю насчет платформо-зависимости, скорее всего будет зависить от использования платформо-зависимых API. А вот компиляторо-зависимости не будет если придерживаться стандарта.
                                    +3
                                    В Java можно задать в исходниках сразу несолкько (хоть все) Java файлы и компилятор их компилирует в таком порядке, чтобы учитывать зависимости между ними.

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

                                  +4
                                  Вы правы! Но если Стандарт таков, что удобную реализацию сделать очень трудно, то это становится проблемой Стандарта… А сейчас он скорее таков: идея в том, чтобы обрабатывать Module interface unit и Header unit и складывать результаты обработки куда-нибудь на диск. Иначе, почти весь профит от использования модулей пропадает. Вопрос: в какой момент это будет делаться? Если автоматически во время компиляции зависимого модуля, то может возникнуть конфликт при одновременной сборке нескольких файлов…
                                  А если не автоматически, то это и есть предкомпиляция.

                                  Также это несколько противоречит идеологии существующих компиляторов, если им придётся самостоятельно генерить много промежуточных файлов, И использовать некий «кеш». Хотя это уже меньшая проблема.
                                    +2
                                    При чем тут кэш и генерация промежуточных файлов?
                                    Если я сейчас напишу что-то типа:
                                    g++ main.cpp a.cpp b.cpp
                                    То компилятор сгенерирует объектные файлы для всех cpp и потом их слинкует. Чем это отличается от модулей? Только одним: между модулями есть зависимости и их нужно компилировать в правильом порядке. Если кому-то лень/не успели прикрутить анализатор зависимостей — это проблема реализции.

                                    Но вообще в нормальных проектах это все равно никому не надо, т.к. все эти зависимости будут прописаны в «мейкфайлах» (имя в виду не именно makefile, а все ранообразие существующих систем сборки). И модули в этом смысле ни чем не отличаются от существующих сейчас библиотек.
                                      +3
                                      Да. всё верно, в таком случае, он в фоне скомпилит и слинкует сразу три файла:
                                      g++ main.cpp a.cpp b.cpp

                                      Правда если вы потом захотите собрать другую программу, частично перекрывающуюся с приведённой, то файлы a.cpp и b.cpp компилятору придётся пересобрать:
                                      g++ main2.cpp a.cpp b.cpp

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

                                      Но вот с модулями переиспользование — это юзкейс частый. Если компилер будет по предкомпилировать всё в фоне, то профит ускорения компиляции потеряется.
                                        +2

                                        Отличается от модулей это тем, что компилятор сгенерит только код, а вот header-ы останутся как есть. А в случае модулей компилятор "компилирует" ещё в хедеры во внутреннее представление. Короче, что-то вроде precompiled headers, только стандартизованно.

                                      +4
                                      Мне было очень странно и непонятно видеть такой факт: название модуля никак не связано с названием файла, как это принято в Java, Python и т. д. Cпециально, чтобы в примере подчеркнуть это, я назвал модуль M, а файл foo.cppm.

                                      Поэтому, если вы заранее руками не предкомпилировали модуль и не назвали его нужным образом, то компилятор никогда в жизни не поймёт, что модуль M нужно искать в foo.cppm, если вы просто попросили его скомпилировать bar.cpp, зависящий от M.

                                      И это по всей видимости, проблема именно Стандарта.
                                        +2
                                        так в одной единице трансляции может быть определено несколько модулей.
                                          +1
                                          Это как? Проверил Clang и VS — везде ошибка. В Стандарте тоже подобных примеров не видел. Да и вообще очень странно звучит. Есть Пример?
                                            +1
                                            Упс, ну значит я ошибся. Мне казалось я про подобное читал раньше в черновиках. Сейчас уже спать, попробую завтра еще поискать где я такое вообще видел, но скорее всего это ложные воспоминания.
                                              +1
                                              Спокойной ночи) Если найдёте будет любопытно, но вообще мне кажется, такое запрещено. Хотя бы если судить по названию — module unit… Да и преамбула только одна в файле
                                          0

                                          А в чём проблема? Название выходного файла (dll, so, exe, lib) тоже никак не связано с именем cpp-файла. И когда вы подключаете статическую библиотеку, вы указывайте имя файла явно.

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

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

                                            Зависимые библиотеки также прописываются в конфигурации сборки. И это было бы ещё более утомительно, если бы наряду с import T; приходилось прописывать эту зависимость в конфигурации сборки. Представьте, что каждый include вам нужно дублировать в конфигурации.

                                            Поэтому, я скорее предположу, что в конфиге сборки не должно писаться, какой модуль собирается из какого файла, а также, какой модуль от какого зависит. А значит, без анализа содержимого этих файлов, система сборки не сможет понять, что именно нужно собрать и в каком порядке. Хотя, в других языках это получается делать хотя бы по имени файла…
                                              0
                                              Более того, в конфигурации сборки пишется от каких библиотек зависит каждая цель.

                                              Вот здесь и кроется нюанс. Компилятор ничего не знает о зависимостях между библиотек. Что ему сказала система сборки — то он и делает.


                                              И точно так же система сборки, а не компилятор, должна будет следить за сборкой модулей.


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

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


                                              Зависимые библиотеки также прописываются в конфигурации сборки. И это было бы ещё более утомительно, если бы наряду с import T; приходилось прописывать эту зависимость в конфигурации сборки. Представьте, что каждый include вам нужно дублировать в конфигурации.

                                              А вот этого делать уже не нужно. Директива import T — это указание компилятору найти предкомпилированный модуль и подключить его.


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

                                              Ну да. Система сборки должна стать умной.

                                                +1
                                                Ну да, выходит, что механизм такой: система сборки должна проанализировать преамбулы всех юнитов программы и исходя из этого построить граф зависимостей.
                                                Только из него будет понятно, какие файлы нужно предкомпилировать, чтобы собрать каждый юнит.

                                                Выходит, что будет три прохода:
                                                1. анализ сборочной системой
                                                2. предкомпиляция
                                                3. компиляция
                                                  0
                                                  Выходит, что будет три прохода:

                                                  В принципе да. Но я бы не отделял предкомпиляцию от компиляции. Причина следующая: отдельные cpp-файлы все равно компилируются только один раз. Просто в одном случае они компилируются в obj-файлы, а в другом — в pcm-файлы. Ну и порядок компиляции становится критичен.

                                                    0
                                                    При предкомпиляции нет генерации кода. Поэтому pcm всё равно нужно явно или неявно компилировать в obj. Во всяком случае, так в clang
                                        –1

                                        .

                                          +2
                                          Это как в паскале модули или есть отличия?
                                            +8

                                            С одной стороны, довольно странно в 2021 году видеть ТАКУЮ реализацию модулей в современном языке. С другой стороны, это же C++ с его стремлением сохранить обратную совместимость со старыми принципами и фичами, хорошо что хоть сейчас и хоть так ввели

                                              +2
                                              Тоже были такие мысли. Но когда начинаешь думать «А как надо было», то ничего лучше придумать не получается) Если есть идеи — велком, интересно послушать
                                                +3
                                                В таком случае меня в принципе удивляет как был реализован Pascal/Delphi в которых однопроходный компилятор разделял секции interface и implementation в юнитах (тут должно быть немного сарказма) решая почти все проблемы модульности.
                                                  0

                                                  Там не было тьюринг-полных шаблонов, препроцессора и контекстно-зависимой грамматики языка.

                                              +6
                                              Из неупомянутого в статье. Еще у модулей наконец-то прикрутили вменяемый анализ кольцевых зависимостей, который для заголовочных файлов может быть настоящим адом. Иными словами имеем вот такой пример файлов
                                              HeaderA.h
                                              #pragma once
                                              #include "HeaderB.h"
                                              int FuncA ()
                                              {
                                                  return 42;
                                              }
                                              

                                              HeaderB.h
                                              #pragma once
                                              #include "HeaderA.h"
                                              int FuncB ()
                                              {
                                                  return FuncA ();
                                              }
                                              

                                              При компиляции получаем «HeaderB.h(5,12): error C3861: 'FuncA': identifier not found» Понятно, что компилятор не нашел объявление FuncA, но почему он его не нашел, когда у нас есть явным образом заданное #include «HeaderA.h»? В данном примере причина кольцевого include очевидна, т.к. имеется только два файла с простейшей структурой, но на то он и пример, чтобы все показать в максимально простом виде. А в реальности из самого хардкорного у меня была история, когда такое кольцо образовывали двенадцать файлов, разбросанных по самым разным частям проекта, содержащих на первый взгляд абсолютно никак между собой не соотносящийся код, и, разумеется, помимо этих 12 файлов там еще были десятки других многоуровневых #include
                                              Теперь проверим то же самое с модулями.
                                              ModuleA.ixx
                                              export module ModuleA;
                                              import ModuleB;
                                              export int FuncA ();
                                              int FuncA ()
                                              {
                                                  return 42;
                                              }
                                              

                                              ModuleB.ixx
                                              export module ModuleB;
                                              import ModuleA;
                                              export int FuncB ();
                                              int FuncB ()
                                              {
                                                  return FuncB();
                                              }
                                              

                                              Компилируем и получаем «Microsoft.CppCommon.targets(458,5): error: Cannot build the following source files because there is a cyclic dependency between them: ModuleA.ixx depends on ModuleB.ixx depends on ModuleA.ixx.» Красота: сразу видна не только кольцевая зависимость, но нам еще и перечислили входящие в нее модули и показали схему ссылок модулей друг на друга. Эх, было бы у меня такое в тот момент, когда я пытался найти вышеописанное кольцо…
                                                +2
                                                Спасибо! Действительно крутая вещь. Ещё один плюс к мотивации
                                                +6
                                                А как по мне модули/концепты это конечно хорошо, но киллер фича нового C++20 — это сравнение интовых переменных.

                                                Наконец то добавили функцию для сравнения int разного размера и знаков и возвращающую математически верные результаты. en.cppreference.com/w/cpp/utility/intcmp
                                                  +3
                                                  Не поспоришь, крутые функции! Но киллер-фичей я бы не назвал, так как у них possible implementation на 50 строк :)
                                                    +2
                                                    Но за 40 лет истории с++ их написали только сейчас. А баги со сравнением интов есть в 9ти из 10 проектов.
                                                    +3
                                                    А для сложения с переносом чего-нибудь добавили?
                                                    +2
                                                    Как выглядит создание(использование) динамических библиотек с модулями?
                                                      +3
                                                      Не проверял, конечно, но думаю, точно также как и обычных executable-файлов. Динамическая библиотека точно также линкуется, особых отличий нет.
                                                        +3
                                                        Проект библиотеки состоит из модулей(.cppm), собирается в бинарный файл(.so/.dll).
                                                        В линковку основного проекта добавляется бинарный файл. Как должен выглядеть «интерфейс» библиотеки? отдельный .h файл? исходники всех «публичных» модулей?
                                                          +2
                                                          Давайте подумаем в теории. Все декларации, которые экспортирует модуль должны быть module linkage. Для экспорируемых функций из DLL/so применяется свой нестандартный linkage.

                                                          Поэтому логично предположить, что те функции, которые экспортирует DLL, не могут быть экспортируемы в смысле модуля. По-видимому, необходим всё равно h-файл. Но ничто не мешает в модуле определять эти функции. И кстати, инклюдить h-файл тоже ничего не мешает.
                                                            +2
                                                            Не знаю как это задумывалось, но по логике вместо .h файла должен быть module interface unit. И не в исходниках, а бинарем.

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

                                                      Самое читаемое