Pull to refresh

Comments 74

хотя бы уж C++14, С++11 без make_unique() это провал.
17!!! C++17 же!!! Я точно помню, что пишу, что-то такое, что без 17 уже жить не могу… только забыл что…
UFO just landed and posted this here
Class template argument deduction ещё киллер-фича 17-х плюсов)
Но подождите следующих 5 статей цикла, там ещё куча крутого. Совсем скоро будем не мочь жить без C++20)

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

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

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

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

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

Увы, практика показывает обратное. 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
Если вы про визуал студию, то они и cppm поддерживают c 16.9
Modules, Requires -fmodules-ts and some aspects are incomplete. Refer to C++ 20 Status


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

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


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

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

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

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


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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


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

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


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

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


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

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

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

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

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

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

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

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

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

Из неупомянутого в статье. Еще у модулей наконец-то прикрутили вменяемый анализ кольцевых зависимостей, который для заголовочных файлов может быть настоящим адом. Иными словами имеем вот такой пример файлов
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.» Красота: сразу видна не только кольцевая зависимость, но нам еще и перечислили входящие в нее модули и показали схему ссылок модулей друг на друга. Эх, было бы у меня такое в тот момент, когда я пытался найти вышеописанное кольцо…
Спасибо! Действительно крутая вещь. Ещё один плюс к мотивации

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

И ещё, если циклическая зависимость в пределах модуля, и решил разнести его части (те же классы) по implementation unit, то тоже не ясно, как рулить циклическими зависимостями.

В С++ никогда не было поддержки никаких циклических зависимостей. Вы вообще о чем?

Я так понял он про случай, когда есть 2 класса и в каждом из них есть поле с типом другого класса

И... как вы можете сделать в C++ это сейчас? Модули в этом смысле ничего не ухудшают.

Class1.h

#pragma once

#include "Class2.h"

class Class2;

class Class1
{
public:
    Class2* cl;
  
    Class1() = default;

    Class1(const Class2& obj)
    {
    }
};


Class2.h

#pragma once

#include "Class1.h"

class Class1;

class Class2
{
public:
    Class1* cl;

    Class2() = default;

    Class2(const Class1& obj)
    {
    }
};

Main.cpp

#include "Class1.h"
#include "Class2.h"

int main()
{
    Class1 cl1;
    Class2 cl2(cl1);
}

Т.е. заголовки могут подключать друг друга, кто первый подключил, тот и молодец. А модули так не могут

Если вы из первого и второго файла уберете include, то ничего не изменится

Forward declaration в модулях вроде как не отменяли. А без него у вас ничего не получится.

А как по мне модули/концепты это конечно хорошо, но киллер фича нового C++20 — это сравнение интовых переменных.

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

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

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

Нынче нужно писать expot { ... }

С расширениями файлов для модулей вообще непонятка. Тут https://en.cppreference.com/w/cpp/language/modules из примеров чётко видно, что модули можно сохранять в cpp-файлах. Но если так сделать, то VS выдаёт ошибку "Error C3378 a declaration can be exported only from a module interface unit". VS поддерживает расширения cppm и ixx, clang вроде бы только cppm (не проверял), CMake версии 3.21 уже распознаёт cppm и ixx файлы как C++. Т.е. для module interface unit оптимально сейчас использовать расширение cppm. Но как дела обстоять с GCC?

А для module implementation unit походу работает только расширение cpp (в VS). Только в нем "module имя_модуля;" не вызывает ошибку.

Sign up to leave a comment.