Назад к блогу

EDG открывает исходники: что это значит для экосистемы C++

EDG открывает исходники: что это значит для экосистемы C++

Компилятор EDG, десятилетиями остававшийся закрытым и лицензировавшийся производителями инструментов, теперь доступен в открытом репозитории. Разбираем, какие компоненты фронтенда опубликованы и как устроены ключевые механизмы трансляции C++. Материал будет полезен всем, кто пишет на C++ или создаёт инструменты вокруг него.

Компилятор EDG долго был закрытым: его фронтенд лицензировали производители компиляторов и инструментов, а исходный код видели единицы. Теперь фронтенд и сопутствующие компоненты лежат в открытом репозитории edgcpp/compiler. Разберём, что именно открыто, как устроены ключевые механизмы фронтенда и что это меняет для тех, кто пишет на C++ или делает инструменты вокруг него.

Что именно открыто

Основной фокус репозитория — C/C++ фронтенд. Помимо него в проект входят:

  • бэкенд, генерирующий C — им можно получать C-код для C++-программ;
  • бэкенд, генерирующий C++ — он полезен для задач source-to-source трансформаций;
  • прелинкер;
  • минимальная runtime-библиотека поддержки;
  • утилиты для работы с промежуточным языком: запись в файл, чтение обратно и отображение в удобочитаемой форме;
  • деманглер имён;
  • набор специализированных инструментов разработки.

Тесты лежат в репозитории как каталог tests. Минимальная runtime-библиотека явно не включает «настоящие» библиотеки — например, для потокового ввода-вывода.

Для начала работы большинству людей рекомендуется partial clone:

git clone --filter=blob:none [email protected]:edgcpp/compiler.git

Как устроен фронтенд: конвейер обработки

Файлы фронтенда покрывают разные фазы трансляции. expr.c отвечает за разбор выражений: scan_expr_full сканирует выражение и возвращает его в *result — это указатель, через который функция отдаёт наружу разобранное выражение; для связанной функции в C++ может дополнительно установить *bound_function_selector — указатель, через который возвращается селектор объекта связанной функции. templates.c содержит scan_template_declaration, которая сканирует объявление шаблона в области видимости пространства имён, вызывая для этого decl_specifiers и declarator. class_decl.c содержит scan_class_definition — обработку определения класса, включая частичные и нереальные классы. overload.c содержит select_overloaded_function, которая по списку аргументов определяет, какую из перегруженных функций следует вызвать. ifc_modules_read.c содержит process_ifc_declaration, которая пытается сформировать IL-сущность для объявления модуля IFC, выполняя три действия: обработку предварительных условий , создание IL-сущности и отображение информации для ленивой загрузки.

Повторный проход по выражению (rescan)

Механизм rescan в expr.c нужен, чтобы повторно выполнить семантический анализ уже разобранного выражения в контексте вывода шаблонных аргументов . Внутренняя функция rescan_expr_with_substitution_internal заново прогоняет семантический анализ выражения, а внешняя обёртка rescan_expr_with_substitution возвращает копию выражения с выполненной подстановкой.

Перед повторным проходом push_expr_rescan_context_if_necessary сохраняет текущий контекст: стек выражений, время жизни объекта, регион IL и template_decl_info. Если мы на верхнем уровне, она переключается в область файла и создаёт новый template_decl_info. Между проходами кэшируется информация о позициях в исходном тексте: record_position_in_expr_for_rescan записывает начальную и конечную позиции выражения, record_type_operand_position_for_rescan делает то же для узлов-типов. Сохраняется и rescan-информация об операторах, а также флаги в rcblock->options, например CTWS_INSIDE_EXPR_RESCAN, чтобы не заходить в рекурсивный rescan повторно. После завершения pop_expr_rescan_context_if_necessary восстанавливает сохранённый контекст.

Кэш выражений

Кэш выражений — это initializer_cache, привязанный к текущему expr_stack. Он хранит уже просканированные init-компоненты: выражения или braced-init-list. Наличие закэшированного элемента проверяет cached_initializer_present(), а извлечение делает fetch_init_component_from_initializer_cache(expr_stack->initializer_cache).

При промахе поведение зависит от текущего токена. Если это tok_lbrace и list-инициализация разрешена, сканируется braced-init-list через parse_braced_init_list. В остальных случаях сканируется выражение с запретом оператора запятая: scan_expr_as_init_component(bundle, EOPT_DISALLOW_COMMA_OPERATOR). get_braced_init_list работает так же: при наличии кэша берёт компонент оттуда, иначе сканирует список из исходника через parse_braced_init_list_full.

cache_expression сам кэш не читает: он сохраняет и восстанавливает expr_stack, выставляет consteval_call_need_not_fold = immediate_context и вызывает scan_expr_as_init_component. Функция cache_curr_token_sequence_entry к кэшу выражений отношения не имеет: она добавляет текущий токен в переданный кэш и возвращает IL-представление токена.

Шаблоны: инстанцирование и дедукция

Прототип-инстанцирование

Для шаблона класса прототип-инстанцирование выполняет prototype_instantiation_for_template: по символу шаблона класса или вложенного класса она получает supplement, берёт из него поле prototype_instantiation и возвращает тип этого символа.

Для функций решение принимает prototype_instantiation_should_be_done_for_function. Прототип-инстанцирование запускается, если включён флаг nonclass_prototype_instantiations, либо если шаблон вариадический, либо — при разрешённых расширениях Microsoft — если шаблон generic.

Само прототип-инстанцирование функции делает function_prototype_instantiation. Оно помечает, что прототипная инстанциация начата, при необходимости инстанцирует спецификацию исключений, устанавливает referencing_namespace. Если функция не определена, не defaulted и не deleted, ставится опция PS_PROTOTYPE_INSTANTIATION (или PS_GENERIC_DEFINITION для generic), при необходимости добавляются PS_IS_SPECIALIZATION и PS_IS_GENERIC_LAMBDA. Затем через push_template_instantiation_scope и rescan_reusable_cache пересканируется тело и вызывается scan_function_body. Для аргументов по умолчанию есть отдельная default_arg_prototype_instantiation: она для каждого элемента списка заново активирует кэш токенов и вызывает delayed_scan_of_default_arg_expr.

Для переменных-шаблонов и статических данных-членов прототип-инстанцирование делает variable_template_prototype_instantiation. Оно пропускает повторную инстанциацию члена во время инстанциации охватывающего класса, а также пропускает работу, если не включены nonclass_prototype_instantiations, шаблон не вариадический и в кэше нет requires-токена.

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

Дедукция аргументов шаблона

Дедукцию для вызова функции выполняет function_template_call_argument_deduction. Она проходит по списку параметров и аргументов, пропуская designator, и для каждого параметра вызывает deduce_one_parameter.

Пакеты параметров обрабатываются в deduce_one_parameter: при ptp->is_parameter_pack начинается контекст дедукции пакета, затем цикл итерирует по аргументам, а advance_to_next_deduced_element переходит к следующему элементу пакета. Для нетрейлинг пакетов в function_template_call_argument_deduction параметр пропускается как невыводимый контекст. Для параметров, являющихся результатом раскрытия нетрейлинг пакета, дедукция пакетов запрещается.

Невыводимые контексты обрабатываются так: если параметр не вовлекает выводимый шаблонный параметр, дедукция не делается, аргумент пропускается, а для пакета берутся все оставшиеся аргументы. Если adjust_deduction_pair не даёт результата и consider_nondeduced истинно, дедукция откладывается, но для пакета это сразу ошибка.

std::initializer_list обрабатывается через is_instance_of_std_initializer_list, которая возвращает elem_type и elem_pack (TRUE для формы T...), и через deduce_from_braced_init_list, вызываемую из deduce_one_parameter при is_braced_init_component(arg). Сама дедукция пары выполняется в deduce_from_one_pair через matches_template_type с флагом MTT_ALLOW_INEXACT_DEDUCTION, а при неудаче для указателей — через matches_template_type_with_qualification_conversion.

Частичный порядок шаблонов

Частичный порядок реализован через сравнение специализации. compare_function_templates возвращает 1, если первый шаблон более специализирован, -1 если второй, 0 если неупорядочены.

Для частичных специализаций compare_partial_specializations сначала пытается вывести параметры одного шаблона из прототипа другого через matches_partial_specialization. Если вывод удался только в одну сторону, эта сторона более специализирована. Если вывод не дал порядка, сравниваются списки аргументов на наличие variadic-параметров: список с большим числом variadic-аргументов считается более специализированным. При включённых концептах дополнительно вызывается compare_constraints, и при равенстве result == 0 и обоих match1/match2 результат берётся из порядка ограничений.

Кандидаты накапливаются в списке через add_to_partial_order_candidates_list, который удаляет менее специализированные записи и не добавляет новую, если она не более специализирована. select_best_partial_order_candidate возвращает первую запись, а при нескольких записях устанавливает ambiguous и выдаёт ошибку ec_ambiguous_partial_spec для частичных специализаций класса.

Deduction guides

create_implicit_deduction_guides обходит конструкторы шаблона класса и для каждого конструктора, не являющегося inheriting constructor, создаёт неявный deduction guide через create_implicit_deduction_guide. Если конструкторов нет, добавляется guide для гипотетического конструктора по умолчанию через add_guide_for_hypothetical_constructor. Также добавляется copy deduction candidate — тем же вызовом, но с типом параметра, равным proto_type.

Для alias-шаблонов create_transformed_deduction_guide_for_alias_template преобразует guide по правилам N4868 [over.match.class.deduct]/2 и записывает результат в deduction_guides alias-шаблона. Для inheriting constructors create_inherited_deduction_guides перебирает using-объявления с is_inheriting_ctor и iek_base_class и вызывает transform_guides_for_inherited_base, который трансформирует guides базового шаблона через изобретённый alias.

При разрешении перегрузки compare_deduction_guides_if_applicable задаёт предпочтения: guide, не порождённый inheriting constructor, предпочитается порождённому; user-declared guide предпочитается сгенерированному; copy deduction candidate предпочитается; guide от обычного конструктора предпочитается guide от шаблона конструктора.

Разрешение перегрузки

Конвейер начинается в try_overloaded_function_match: он обходит каждый символ перегруженного множества и для каждого вызывает проверку жизнеспособности. Ранние отсевы: пропуск шаблонов при ignore_templates, пропуск в первом проходе по braced-init-list всех конструкторов, кроме initializer-list, и отсев по видимости или устаревшим объявлениям.

determine_function_viability проверяет число и типы параметров, добавляет селектор как explicit this, отбрасывает невидимые функции, не прошедшие ограничения, уже присутствующие в списке кандидатов и defaulted-операторы сравнения.

select_best_candidate_functions сначала удаляет встроенные операторы с той же сигнатурой, кандидатов с anachronism-совпадениями и не-initializer-list конструкторы при наличии initializer-list, затем для каждого аргумента строит множество лучших совпадений через compare_arg_match_levels. Финальный тест — match_is_better_on_at_least_one_arg: выбранная функция должна быть строго лучше хотя бы по одному аргументу для каждой другой функции, иначе вызов неоднозначен.

Если совпадения по аргументам равны, compare_candidate_functions применяет tiebreaker'ы по порядку: compare_late_tiebreakers, предпочтение user-defined conversion с совпадающим видом ссылки, template vs non-template, стандартные преобразования после UDC, добавленные квалификаторы, copy/move конструкторы, специализация шаблонов, наследование, inheriting ctor, deduction guides, g++ const this, enable_if и pass_object_size атрибуты, using-declaration.

Rewritten-кандидаты для operator== и operator<=>

discard_eq_candidates_that_are_not_rewrite_targets проходит по списку кандидатов и удаляет те operator==, которые не являются rewrite target для первого операнда заданного типа. Кандидат отбрасывается, если это оператор onk_eq и operator_eq_is_rewrite_target для него ложно.

operator_eq_is_rewrite_target считает operator== rewrite target, если у него нет соответствующего operator!=, либо если этот operator!= является defaulted: defaulted operator!= не мешает operator== быть rewrite target.

При обработке перевёрнутых кандидатов для == первый операнд переписанного выражения — это исходный второй операнд, поэтому отбрасывание идёт по operand_2->type. При обработке onk_ne отбрасываются переписанные operator==, не являющиеся rewrite target с исходным первым операндом. Кроме того, если переписанный supplemental-кандидат имеет тот же тип, что и не-переписанный, переписанный отбрасывается, поскольку правило разрешения предпочитает не-переписанного кандидата.

complete_comparison_rewrite применяет второй оператор к результату переписывания: для != результат инвертируется, для relational/spaceship строится нулевой литерал и вызывается process_rel_operator или process_spaceship_operator, причём spaceship переписывается только в reversed-случае. rel_op_synthesizes_from_spaceship определяет, что заданный rel_op для данных типов операндов вызывает spaceship-оператор: выбранный rewritten_candidate должен быть sfk_operator с opname_kind == onk_spaceship.

Сравнение ссылочных привязок

compare_reference_matches сравнивает две записи соответствия аргументов и возвращает +1, 0 или -1. Она применяется только если оба типа параметров ссылочные и ни одно из соответствий не является соответствием для параметра this с ref-квалификатором по умолчанию. Если один тип — rvalue-ссылка, а другой — lvalue-ссылка, привязка rvalue-ссылки лучше, кроме случая ссылки на функцию.

direct_reference_binding_possible проверяет возможность прямой привязки ссылки и учитывает версии стандарта: при !cpp11_mode || ms_version_is(<1928) используется qualification_conversion_possible, иначе при не-gpp и не-clang режимах — are_reference_compatible, иначе f_types_are_compatible с флагами.

Привязка lvalue-ссылки на const volatile к rvalue запрещается при разрешении перегрузки в gpp_mode, но допускается в microsoft_mode и в strict_ansi_mode без cpp11_mode. Также current_mode_requires_early_rvalue_ref_lvalue_test определяет, что текущий диалект следует старому правилу черновика C++11, требующему проверки запрета привязки rvalue-ссылки к lvalue до преобразований:

return rvalue_references_enabled &&
       microsoft_mode && microsoft_version < 1700;

Классы, специальные члены и переопределения

Специальные функции-члены

generate_trivial_ctors создаёт представление для класса, который полон и не имеет нетривиальных конструкторов — это нужно, например, когда дружественный класс ссылается на них. Она всегда создаёт тривиальный конструктор по умолчанию и тривиальный конструктор копирования, а тривиальный конструктор перемещения — только если generate_move_operations истинно. Все они помечаются compiler_generated = TRUE, never_throws = TRUE и inline-флагом.

generate_trivial_dtor создаёт тривиальный деструктор, но если переданный src_loc не совпадает с именем деструктора, она не генерирует его и возвращает cssp->destructor.

check_suppressed_special_functions проверяет, есть ли ошибки, препятствующие генерации неявного определения копирующего или перемещающего оператора присваивания, конструктора копирования или перемещения, деструктора. Например, нестатический член с const-квалифицированным типом подавляет копирующее и перемещающее присваивание: присваивание такому члену нельзя присвоить новое значение, поэтому сгенерировать её тело невозможно. Член ссылочного типа подавляет копирующее и перемещающее присваивание по той же причине: ссылку после инициализации нельзя перепривязать. Член типа rvalue-ссылки подавляет конструктор копирования, поскольку rvalue-ссылку не может быть скопирована из обычного lvalue-источника.

mark_suppressed_defaulted_members_as_deleted помечает defaulted-специальные члены как deleted, если соответствующий флаг подавления установлен: например, для конструктора по умолчанию при suppress_default_ctor выставляются is_deleted = TRUE и defined = TRUE.

generate_destructor добавляет объявление деструктора; если suppress_dtor истинно, вызывается mark_special_member_suppressed, иначе при включённых constexpr-деструкторах и constexpr_dynamic_alloc_enabled деструктор делается constexpr.

Отложенные fixup'ы

При разборе класса создаются записи о рутинных fixup'ах через alloc_routine_fixup, которые добавляются в список routine_fixup_list класса. Для специализаций в Microsoft-режиме используется add_routine_fixup_for_specialization, которая создаёт запись с флагом is_specialization.

Часть обработки откладывается, потому что тела inline-функций и аргументы по умолчанию нельзя обрабатывать до завершения всех определений классов: process_deferred_class_fixups_and_instantiations вызывается только когда счётчики pending_class_definitions и defer_inline_function_fixups равны нулю.

Для friend-функций, определённых в шаблонном классе, defer_routine_fixup_until_use сохраняет указатель на fixup и ставит deferred = TRUE, чтобы тело обрабатывалось только при первом использовании. Когда fixup откладывается до конца единицы трансляции, add_to_deferred_friend_function_fixup_list добавляет запись в deferred_friend_fixup_list, а process_deferred_friend_fixup_list выполняет их позже. Само выполнение делает deferred_friend_function_fixup: реактивирует область класса, сканирует тело функции через scan_function_body и помечает функцию определённой.

Финальный overrider виртуальной функции

Финальный overrider ищется функцией find_final_overrider. Она берёт список overriding_virtual_functions у базового класса и, если в нём есть запись, у которой primary_function совпадает с искомой функцией, заменяет указатель на функцию и указатель на базовый класс. Если overrider находится в самом производном классе, указатель на базовый класс обнуляется. Список упорядочен по номерам виртуальных функций primary_function, поэтому при первом элементе с большим номером поиск прекращается.

Записи в этот список добавляет record_virtual_function_override: если запись для primary_func уже есть, она переиспользуется, иначе создаётся новая и вставляется в порядке номеров виртуальных функций.

При множественном и виртуальном наследовании, когда один и тот же primary_func переопределён в нескольких базах, а в самом производном классе переопределения нет, в списке остаются две записи с одинаковым primary_function. report_virtual_function_ambiguities находит такие соседние дубликаты, удаляет лишние и выдаёт ec_ambiguous_virtual_function_override, если среди них больше одного overrider в неоднозначных базах. Если же в самом производном классе есть переопределение, record_virtual_function_override при обработке существующей записи удаляет все последующие записи с тем же primary_function, устраняя неоднозначность.

update_override_registry служит для учёта переопределений функций из набора перегрузок: он ведёт список записей по overridden_sym и base_class, считает число виртуальных функций в наборе и увеличивает override_count при успешном переопределении, а при неудаче добавляет запись в override_failures.

Лямбды

Лямбда-замыкание создаётся как неполный класс: make_closure_class создаёт безымянный тег-символ и тип класса, помечает его is_lambda_closure_class = TRUE и при необходимости фиксирует родителя — поле или переменную инициализатора.

Захваты формируются через add_lambda_capture, который для неявных захватов находит глубину области и вызывает r_add_lambda_capture. Тот рекурсивно создаёт захват во вложенной лямбде, при необходимости поднимаясь к объемлющей лямбде, и для неявного захвата вызывает compute_const_capture_flag и создаёт поле через field_for_lambda_capture.

compute_const_capture_flag выставляет const_capture = TRUE для захвата по ссылке, если исходный захват const, либо для захвата по значению, если лямбда не mutable. field_for_lambda_capture при необходимости объявляет поле захвата и соответствующее поле во внешних лямбдах, возвращая lcp->closure_field.

Конверсия в указатель на функцию генерируется только для лямбды без списка захватов, без захвата по умолчанию (с оговоркой для старых GCC) и с телом: тогда создаётся конверсионная функция и статическая точка входа, а позже — их тела.

Модули и IFC

Формат IFC

Заголовок IFC читается функцией read_file_header, которая инициализирует буфер начиная с 4-го байта (после магического числа) и конструирует из модуля узел an_ifc_file_header. Из заголовка извлекаются major- и minor-версии и архитектура.

Совместимость проверяет check_ifc_compatibility: если версия не поддерживается, вызывается handler->bad_version, а если архитектура не совместима с целевой — handler->bad_architecture. is_target_compatible_arch сравнивает архитектуру с текущей целевой: для ifc_as_arm32 требуется ARM и не 64 бита, для ifc_as_arm64 — ARM и 64 бита, для ifc_as_x86 — x86 и не 64 бита, для ifc_as_x64 — x86 и 64 бита, для ifc_as_unknown — FALSE. В an_ifc_error_severity_checker::bad_version при invalid_version_is_warning выставляется es_warning, иначе es_catastrophe; bad_architecture всегда даёт es_catastrophe.

load_string_table получает смещение и размер таблицы строк из заголовка и либо возвращает указатель в отображённой области, либо читает данные из файла. verify_checksum читает 32 байта ожидаемой контрольной суммы после 4 магических байтов и хеширует оставшуюся часть файла (f_size - 36 байт) алгоритмом SHA-256.

Ленивая загрузка

Ленивая загрузка отслеживается учётной структурой, которая ведёт три набора сущностей: сопоставление сущностей переднего плана их IFC-объявлениям, набор сущностей, обрабатываемых в данный момент, и набор сущностей, обработка которых не удалась и не должна повторяться. При обращении к отложенной сущности она попадает в набор обрабатываемых; когда обработка завершается успешно, запись убирается из этого набора и её сопоставление с IFC-объявлением снимается; если обработка завершилась ошибкой, запись убирается из набора обрабатываемых и переносится в набор неудачных, чтобы повторные попытки не предпринимались.

Отложены могут быть IL-инициализаторы переменных и тела IL-функций. Поля и битовые поля откладывать нельзя. Отложенными не могут быть hidden friends: такие функции не загружаются немедленно, и отложить их тоже нельзя, поскольку они загружаются вместе с классом. Также не откладываются классические перечисления и безымянные перечисления — они обрабатываются сразу.

Триггером загрузки служит обращение к отложенной сущности, после чего она помечается как обрабатываемая, а по завершении — как завершённая.

Восстановление объявлений из IFC

process_decl_at_index получает указатель на сущность модуля по индексу объявления и вызывает process_ifc_declaration, который создаёт IL-сущность. request_entity_at_index делает то же, но через request_entity, и обработка считается завершённой только тогда, когда она действительно завершена. process_ifc_declaration выполняет три шага: обработка предварительных условий, создание IL-сущности и привязка информации для ленивой загрузки определения.

cache_decl, cache_type и cache_expr наполняют кэш токенов: cache_type вызывает cache_type_first_part и cache_type_second_part, cache_expr разбирает вид выражения и либо выдаёт ошибку для неподдерживаемых, либо кэширует токены.

Для EDG-расширения (ifc_es_expr_vendor_extension) проверяется is_edg_authored, вычисляется ожидаемое смещение как expr.value - 1, проверяется наличие элемента в ifc_pk_edg_token_cache, затем вызывается cache_edg_token_cache с нормализованным смещением. Для MSVC-расширений в cache_operator (монадические операторы) под #if MICROSOFT_EXTENSIONS_ALLOWED кэшируются токены вроде tok_assume, tok_alignof, tok_uuidof и других; в cache_expr под тем же условием обрабатываются is_broken_reference_to_template_parameter и is_broken_indirect_reference_to_template_parameter.

Redeclaration между модулями

check_and_set_redeclaration ищет повторное объявление базовой сущности через find_redeclared_basic_entity, который проверяет активные и неактивные символы; при обнаружении вызывается maybe_diagnose_redeclaration. Однако диагностика в maybe_diagnose_redeclaration отключена директивой #if 0, поэтому ошибка не выдаётся.

Для шаблонов check_and_set_template_redeclaration использует find_redeclared_template_entity, а is_function_template_redecl сравнивает параметры функции и шаблона: при несовпадении параметров совпадение не подтверждается. Расхождения в параметрах функции и шаблона считаются несовпадением. Таким образом, ошибкой считается только случай, когда диагностика включена, но в текущем коде она отключена.

Что это значит для экосистемы C++

Проект позиционируется как «open source EDG C/C++ compiler project», основной фокус которого — фронтенд EDG C/C++. Его ключевые свойства: совместимость парсинга и эмуляция ошибок, чтобы исходный код, компилируемый Clang, GCC и MSVC, компилировался с EDG; обширная документация; крайняя конфигурируемость, позволяющая использовать его как основу для множества C/C++-ориентированных продуктов. Относительно GCC, Clang и MSVC проект позиционируется как обеспечивающий совместимость исходного кода, который компилируется этими компиляторами, с EDG.

Разработка следует трём направлениям: вклад сообщества, постоянная поддержка и коллективно финансируемые функции. Крупные возможности могут разрабатываться и финансироваться сообществом: организации поддерживают их финансово, а не выделяют своих разработчиков. После того как возможность профинансирована, она выпускается для всех одновременно, и никто не получает ранний доступ. Все изменения попадают в один публичный репозиторий одновременно.

Процессом по всем трём трекам руководит Fiscal Sponsorship Committee под председательством John Spicer, включающий волонтёров из сообщества пользователей EDG. Комитет определяет приоритеты для поддержки и новых возможностей. Финансирование идёт за счёт не облагаемых налогом взносов сообщества, а разработкой занимаются EDG-разработчики, нанятые The C++ Alliance, плюс вклад сообщества пользователей EDG. Обратная связь по составу комитета и по приоритетам разработки приветствуется.

Для связи предлагаются CppLang Slack с каналом `#

Где смотреть в коде

Источники

Похожее