Грокаем C++
9.37K subscribers
44 photos
1 video
3 files
575 links
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.

По всем вопросам (+ реклама) @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Динамический полиморфизм: разделяемые библиотеки
#опытным

В тему указателей на функции вкину еще один способ реализации полиморфизма в С++ - разделяемые или динамические библиотеки.

Обычно разделяемые библиотеки загружаются на самом старте программы(какие-нибудь libc и libstdc++ например неявно подгружаются на старте). Основную часть таких библиотек мы прописываем в опциях линковки.

Однако динамические библиотеки можно неявно подгружать прямо из кода! Для этого на разных системах существует разное системное апи, но для юниксов это dlopen+dlsym.

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

Тут пример будет довольно длинный, поэтому начнем с начала.

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

class PluginInterface {
public:
virtual int method() = 0;
};

extern "C" PluginInterface* create_plugin();

extern "C" void destroy_plugin(PluginInterface* obj);


Эта команда берет и реализует этот интерфейс:

#include "PluginInterface.hpp"
#include <iostream>

class MyPlugin : public PluginInterface {
public:
virtual void method() override;
};

int MyPlugin::method() {
std::cout << "Method is called\n";
return 42;
}

extern "C" PluginInterface* create_plugin() {
return new MyPlugin();
}

extern "C" void destroy_plugin(PluginInterface* obj) {
delete obj;
}


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

Функции create_plugin и destroy_plugin обязаны иметь сишную линковку, чтобы достать указатели на них по их имени из библиотеки с помощью dlsym:

#include "PluginInterface.hpp"
#include <dlfcn.h>
#include <iostream>

typedef PluginInterface *(*creatorFunction)();
typedef void (*destroyerFunction)(PluginInterface *);

int main() {
void *handle = dlopen("myplugin.so", RTLD_LAZY);
if (!handle) {
std::println("dlopen failure: {}", dlerror());
return 1;
}
creatorFunction create = reinterpret_cast<creatorFunction>(dlsym(handle, "create_plugin"));
destroyerFunction destroy = reinterpret_cast<destroyerFunction>(dlsym(handle, "destroy_plugin"));

PluginInterface *plugin = (*create)();
std::println("{}", plugin->method());
(*destroy)(plugin);
dlclose(handle);
}


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

Разве по имени функции можно получить указатель на нее? Похоже на какую-то рефлексию с первого взгляда.

Тут дело в именах функций и отображении их в символы бинарного файла при компиляции. В С нет никакого манглинга имен, поэтому в готовом бинарном файле можно найти символ, соответствующий названию функции, и связанный с ним адрес этой фукнции. Именно поэтому create_plugin и destroy_plugin помечены extern "C", чтобы их имена обрабатывались по правилам С.

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

Choose the right name. Stay cool.

#cppcore #OS #compiler
1029👍9🔥8
#include <filename> vs #include "filename"
#новичкам

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

Дано: файл, который нужно включить в проект. Задача: определить, включать его через треугольные скобки или кавычки. Какой подход выбрать?

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

Для начала: #include - это директива препроцессора. На месте этой директивы на этапе препроцессинга вставляется тело включаемого файла. Но для того, чтобы вставить текст файла, его надо в начале найти. И вот здесь главное различие.

У компилятора есть 2 вида путей, где он ищет файлы - системные директории и предоставленные юзером через опцию компиляции -I.

Так вот #include <filename> в начале ищет файл в системных директориях. Например, в линуксе хэдэры устанавливаемых библиотек могут оказаться в директориях /usr/include/, /usr/local/include/ или /usr/include/x86_64-linux-gnu/(на x86 системах).

А #include "filename" в начале ищет файл в текущей директории и в директориях, предоставленных через опцию компиляции.

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

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

Используем #include <filename> для включения стандартных файлов и хэдэров сторонних библиотек. Так как они скорее всего установлены по стандартным директориям, логично именно там начинать их поиск.

#include <stdio.h> // Стандартный заголовочник
#include <curl/curl.h> // Заголовочник из системной директории

int main(void) {
CURL *curl = curl_easy_init();
if(curl) {
printf("libcurl version: %s\n", curl_version());
curl_easy_cleanup(curl);
}
return 0;
}


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

// include/mylib.hpp - объявляем функцию из нашего проекта
#pragma once
void print_hello();

// src/main.cpp - используем локальный заголовочник через " "
#include <iostream> // Системный заголовочник
#include "mylib.hpp" // Заголовочник локального проекта, ищем в указанных путях

int main() {
print_hello();
return 0;
}

void print_hello() {
std::cout << "Hello from my project!\n";
}

// компиляция: g++ -Iinclude/ src/main.cpp -o my_program -std=c++17


See the difference. Stay cool.

#cppcore #compiler
162👍25🔥10
​​Zero-Cost Abstractions
#новичкам

Нельзя в промышленных масштабах писать код без абстракций. Вряд ли вы за обозримое время напишите даже эхо-сервер на ассемблере. Даже сам язык программирования - это абстракция. Он позволяет писать программы более-менее на английском языке(или на патриотическом Русском на 1С).

Абстракции упрощают программирование. А что с перфомансом?

Гипотеза такова, что чем выше уровень абстракции, тем выше косты производительности в рантайме.

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

Однако есть и такие абстракции, которые не накладывают оверхэд на рантайм! Они называются Zero-Cost абстракциями.

И это чуть ли не основаная философия программирования на С++. Мы можем играться с уровнями абстракции, писать самый отдаленный от железа код и за все это мы ничего не платим! Компилятор своим мегамозгом анализирует на код, выполняет кучу оптимизаций и на выходе получаем быструю, высокопроизводительную конфетку.

За примерами долго ходить не надо. Можно сложить элементы вектора руками, а можно использовать std::accumulate:

void foo(std::vector<int>& vec) {
int sum = 0;
for(int i = 0; i < vec.size(); i++) {
sum += vec[i];
}
std::cout << sum;
}

void bar(std::vector<int>& vec) {
int sum = std::accumulate(vec.begin(), vec.end(), 0);
std::cout << sum;
}


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

На каких китах стоит возможность использовать "бесплатные" абстракции в С++?

🐳 Полиморфизм времени компиляции, compile-time вычисления и метапрограммирование. Тут все просто: переносим вычисления с рантайма в компайл тайм с помощью шаблонов, constexpr и меты и радуемся жизни. Параллельно можно еще и кофеек себе заваривать, пока проект билдится.

🐳 Инлайнинг. Одна из основных оптимизаций компилятора. Позволяет встраивать код функции в вызывающий код. С помощью инлайнинга 4 вложенных вызова функций могут превратиться в одну сплошную портянку низкоуровневого кода без дорогостоящих инструкций call.

🐳 Другие оптимизации. Компилятор дополнительно выполняет кучу оптимизаций: переставляет инструкции, подставляет известные значения сразу в код, обрезает ненужные инструкции, векторизует циклы и тд. Все они нужны для одной цели - ускорение кода.

Объединяя эти механизмы, C++ стремится обеспечить абстракции высокого уровня, которые можно использовать для написания выразительного и читаемого кода, сохраняя при этом производительность сравнимую с более низкоуровневым кодом, написанным например на С.

Don't pay for abstraction. Stay cool
#cppcore #compiler
👍3315🔥6😁3
Unity build
#опытным

Чем знаменит С++? Конечно же своим гигантским временем сборки программ. Пока билдится плюсовый билд, где-то в Китае строится новый небоскреб.

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

Компиляция всяких шаблонов сама по себе долгая, особенно, если использовать какие-нибудь рэнджи или std::format. Но помните, что конкретная инстанциация шаблона будет компилироваться независимо в каждой единице трансляции. В одном цппшнике использовали std::vector<int> - компилируем эту инстанциацию. В другом написали std::vector<int> - заново скомпилировали эту инстанциацию. То есть большая проблема в компиляции одного и того же кучу раз.

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

Обе эти проблемы можно решить одним махом - просто берем и подключаем все цппшники в один большооой и главный цппшник. И компилируем только его. Такой себе один большой main. Такая техника называется Unity build (aka jumbo build или blob build)

Условно. Есть у вас 2 цппшника и один хэдэр:

// header.hpp
#pragma once
void foo();

// source1.cpp
#include "header.hpp"
void foo() {
std::cout << "You are the best!" << std::endl;
}

// source2.cpp
#include "header.hpp"
int main() {
foo();
}


Вы все цппшники подключаете в один файл unity_build.cpp:

#include "source1.cpp"
#include "source2.cpp"


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

Или нет?

У этой техники есть ряд недостатков:

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

Потенциальные конфликты имен. Конфликты статических переменных и функций с одинаковыми именами в разных файлах, конфликты символов из анонимных namespace'ов, неожиданное разрешение перегрузки функций - все это может подпортить вам жизнь.

Сложность отладки. Вас ждут увлекательные ошибки компиляции и нетривиальная навигация по ним.

У кого был опыт с unity билдами, отпишитесь по вашим впечатлениям.

Solve the problem. Stay cool.

#cppcore #compiler #tools
123🔥7👍6😁2🗿1
​​Распределенные компиляторы
#опытным

Как только ваш проект достигает определенного размера, время компиляции начинает становиться проблемой. В моей скромной практике были проекты, которые полностью собирались с 1-2 часа. Но это далеко не предел. Пишите кстати в комментах ваши рекордные тайминги сборки проектов.

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

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

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

Так как обычно девелоперские задачи в среднем потребляют мало ресурсов(не так много нужно, чтобы писать буквы в редакторе), этими удаленными CPU могут быть даже машины ваших коллег!

Наиболее известный представитель систем распределенной компиляции с открытым исходным кодом — это distcc. Он состоит из демона-сервера, принимающего задания на сборку по сети, и обёртки (wrapper) для компилятора, которая распределяет задания по доступным узлам сборки в сети.

Вот примерная схема его работы(у кого-то может некорректно отображаться, ничего поделать не можем):

┌─────────────────┐    ┌─────────────────────────────────┐
│ Клиентская │ │ Ферма компиляции │
│ машина │ │ │
│ │ │ ┌───────┐ ┌───────┐ ┌───────┐│
│ ┌─────────────┐│ │ │Worker │ │Worker │ │Worker ││
│ │ Координатор │◄──────►│ 1 │ │ 2 │ │ N ││
│ └─────────────┘│ │ └───────┘ └───────┘ └───────┘│
│ │ │ │
│ ┌─────────────┐│ │ ┌─────────────────────────────┐│
│ │ Кэш .o ││ │ │ Distributed Cache ││
│ │ файлов │◄──────►│ ││
│ └─────────────┘│ │ └─────────────────────────────┘│
└─────────────────┘ └─────────────────────────────────┘

- Координатор анализирует зависимости между файлами и распределяет задачи компиляции

- Компиляционные ноды выполняют фактическую компиляцию, их набор конфигурируется на клиенте

- Распределенный кэш хранит скомпилированные объектные файлы, кэширует результаты компиляции, тем самым ускоряя повторные сборки

Этапы работы примерно такие:

1️⃣ Все цппшники проекта проходят этап препроцессинга на локальной машине и уже в виде единиц трансляции перенаправляются на ноды компиляции.

2️⃣ Ноды компиляции преобразуют единицы трансляции в объектные файлы и пересылают их на клиентскую машину.

3️⃣ Последним этапом идет бутылочное горлышко всей системы - линковка. Для нее необходим доступ ко многим объектым файлам одновременно и эта задача слабо параллелится, поэтому и выполняется на клиентской машине.

Таким образом вы можете уменьшить время сборки проекта в разы и ускорить разработку в целом. Вот ссылочка на доку для заинтересовавшихся.

Speed up processes. Stay cool.

#compiler #tools
1👍259🔥8
​​ccache
#опытным

Еще один полезный и простой во внедрении инструмент для ускорения компиляции - ccache.

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

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

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

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

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

Кэшу не надо беспокоиться за криптостойкость, поэтому в нем спокойно используется небезопасная, но быстрая функция c хорошим распределением.

После вычисления значения тега ccache проверяет, существует ли запись с таким тегом в кеше. Если да, перекомпиляция не нужна. Что удобно, ccache запоминает не только сам артефакт, но и вывод компилятора в консоль, который был сгенерирован при его создании — поэтому, если вы извлекаете закешированный файл, который ранее вызывал предупреждения компилятора, ccache снова выведет эти предупреждения.

Если распределенный компилятор distcc каждый раз выполняет препроцессинг, то для ccache это не обязательно. В одном из режимов работы ccache вычисляет хэши MD4 для каждого включаемого заголовочного файла отдельно и сохраняет результаты в так называемом манифесте. Поиск в кеше выполняется путём сравнения хэшей исходного файла и всех его включений с содержимым манифеста; если все хэши попарно совпадают, мы имеем попадание. В текущих версиях ccache прямой режим включён по умолчанию.

Для того, чтобы начать пользоваться ccache, достаточно его установить, добавить в PATH и в cmake'е прописать CMAKE_CXX_COMPILER_LAUNCHER=ccache. Это можно сделать и через команду запуска, и через установку переменной окружения. Вот вам ссыль.

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

Compile fast. Stay cool.

#compiler #tools
1👍2112🔥9🤯1
​​ccache vs cmake
#опытным

И давайте раскроем очевидный вопрос: чем кэширование ccache отличается от кэширования самого cmake'а? Ведь при искрементальной сборке cmake пересобирает только те файлы, которые поменялись.

Основное отличие: cmake - это система сборки, а ccache - это четко кэш. cmake не может себе позволить анализировать контент всех файлов, его основная задача - билдить проект. Поэтому ему нужно очень быстро понять, изменился файл или нет. И принимает он решение на основе времени модификации файла. А ccache не ограничен такими рамками. Он учитывает контент препроцесснутого файла и контекст компиляции.

Проще понять разницу на примерах:

🔍 Вы скомпилировали проект и случайно или специально(бывает нужно, если cmake троит) удалили папку с билдом. Без ccache нужно перекомпилировать все, а с ним - ничего, только линковку сделать.

🔍 Вы плотно работаете с несколькими ощутимо отличающимися бранчами, собираете, коммитите и переключаетесь. Без ccache нужно будет перекомпилировать все измененные при переключении бранчей файлы. С ccache - только те, которые вы сами изменили после последней сборки.

🔍 Если вы правите только комменты в файле, то голый cmake пойдет перекомпилировать его. ccache - нет, потому что работает с препроцесснутым файлом.

🔍 Вы активно переключаетесь между конфигурациями при сборке. Например между релизом и дебагом. cmake будет полностью пересобирать проект при изменении типа билда. А ccahce после одной сборки на каждую конфигурацию все запомнит и вы будете компилировать только последние изменения.

Not much, но каждый из нас частенько сталкивается с одним из этих пунктов. Поэтому ставьте ccache. Это сделать просто, но импакт дает ощутимый в определенных кейсах.

Compile fast. Stay cool.

#compiler #tools
134👍14🔥8
​​Быстрые линкеры
#опытным

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

GCC как самый широкоиспользуемый компилятор использует ld в качестве линкера. ld считается очень громоздким, раздутым и от этого медленным. Можно решить проблему гениально и просто использовать быстрый линкер!

С ld можно бесшовно перейти на другой совместимый компоновщик с помощью опции -fuse-ld. То есть буквально:

g++ -fuse-ld=<my_linker> main.cpp -o program


И ваша программа будет собираться с помощью my_linker. Ну или в cmake:

# Установка линкера для всего проекта
set(CMAKE_LINKER my_linker)

# Или для конкретной цели
target_link_options(my_target PRIVATE "LINKER:my_linker")


Какие альтернативные компоновщики существуют?


GNU Gold. Еще один официальный линковщик из пакета GNU. Создавался как более быстрая альтернатива ld для линковки ELF файлов. Он действительно быстрее ld, но теперь его уже никто не поддерживает и недавно в binutils задепрекейтили его.

lld (LLVM Linker). Линковщик от проекта llvm. Активно развивается и имеет интерфейсную совместимость с дефолтовым ld, как и clang имеет в gcc. Быстрее Gold.

mold. Или modern linker. В несколько раз быстрее lld и является самым быстрым drop-in опенсорсным линковщиком. Он использует более оптимизированные структуры данных и каким-то образом линкует в параллель! Благодаря этому достигается фантастическая скорость работы.

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

В любом случае стоит попробовать и, возможно, вы в несколько раз сможете сократить время линковки.

Be faster. Stay cool.

#tools #compiler
3🔥3915👍10❤‍🔥42
​​Самая надежная гарантия отсутствия исключений
#опытным

Исключения не любят не только и не столько потому, что они нарушают стандартный поток исполнения программы, могут привести к некорректному поведению системы и приходится везде писать try-catch блоки. Исключения - это не zero-cost абстракция. throw требуют динамические аллокации, catch - RTTI, а в машинном коде компилятор обязан генерировать инструкции на случай вылета исключений. Плюс обработка исключений сама по себе медленная.

Поэтому некоторые и стараются минимизировать использование исключений и максимально использовать noexcept код.

Но можно решить проблему накорню. Так сказать отрезать ее корешок под самый корешок.

Есть такой флаг компиляции -fno-exceptions. Он запрещает использование исключений в программе. Но что значит запрет на использование исключений?

👉🏿 Ошибка компиляции при выбросе исключения. А я говорил, что под корень рубим. Вы просто не соберете программу, которая кидает исключения.

int main() {
throw 1; // even this doesn't compile
}


👉🏿 Ошибка компиляции при попытке обработать исключение. Ну а че, если вы живете в мире без исключений, зачем вам их обрабатывать?

int main() {
// even this doesn't compile
try {
} catch(...) {
}

}


👉🏿 Можно конечно сколько угодно жить в розовом мире без исключений, но рано или поздно придется использовать чужой код. Что будет, если он выкинет исключение?

std::map<int, int> map;
std::cout << map.at(1) << std::endl;


Моментальное завершение работы
. Оно как бы и понятно. Метод мапы at() кидает std::out_of_range исключение, если ключа нет в мапе. Обрабатывать исключение нельзя, поэтому чего вола доить, сразу терминируемся. И никакой вам раскрутки стека и graceful shutdown. Просто ложимся и умираем, скрестив ручки.

То есть вы накорню запрещаете упоминание исключений в вашем коде, а если что-то пошло не по плану, то оно пойдет по п...

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

Как тогда код писать? А об этом через пару постов.

Handle errors. Stay cool.

#cppcore #compiler
👍2711🔥6😁3❤‍🔥2🤔1
​​Как стандартная библиотека компилируется с -fno-exceptions?
#опытным

В прошлом посте мы поговорили о том, что использование флага -fno-exceptions фактически трансформирует ваш код в диалект С++, в котором упоминание мира исключений карается ошибкой компиляции. Но каким образом компилируется код из стандартных заголовочных файлов? Там же повсюду обработка исключений?

Ответ прост. Макросы, товарищи. Вся магия в них. Вот на что заменяется обработка исключений:

#if __cpp_exceptions
# define __try try
# define __catch(X) catch(X)
# define __throw_exception_again throw
#else
# define __try if (true)
# define __catch(X) if (false)
# define __throw_exception_again
#endif


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

Ну и для большинства классов, унаследованных от exception, существуют соответствующие функции с C-линковкой:

#if __cpp_exceptions
void __throw_bad_exception()
{ throw bad_exception(); }
#else
void __throw_bad_exception()
{ abort(); }
#endif


Тогда любая функция, которая бросает исключения должна триггерить std::abort. Или нет?

Нет. Вот примерчик.

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

Чтобы это исправить, можно собрать ее с запретом исключений. Примерно так:

git clone git://gcc.gnu.org/git/gcc.git
cd gcc
git checkout <target_release_tag>
./configure
--disable-libstdcxx-exceptions
CXXFLAGS="-fno-exceptions <all_flags_that_you_need>"

make -j$(nproc)
make install


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

Extend your limits. Stay cool.

#compiler
16👍9🔥9❤‍🔥3🤔2
​​Что не так с модулями?
#опытным

Модули появились как одна из мажорных фич С++20, которая предоставляет чуть ли не другой подход к написанию С++ кода.

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

Если по простому, то модуль - это такой бинарный черный ящик, у которого четко определен интерфейс, который он экспортирует наружу.

Экспортируемые сущности явно помечаются в коде модуля. Затем модуль компилируется и из бинарного его представления можно дергать только эти экспортируемые сущности.

Короткий пример:

// math.cppm - файл модуля
export module math; // Объявление модуля

import <vector>; // Импорт, а не включение

// Макросы НЕ экспортируются!
#define PI 3.14159

// Явный экспорт - только то, что нужно
export double calculate_circle_area(double radius);

// Внутренние функции скрыты
void internal_helper();


и его использование:

// main.cpp - обычный С++ файл
import math; // Импорт интерфейса, не всего кода

// Используем экспортированную функцию
double area = calculate_circle_area(10);

// internal_helper(); // ERROR! функция скрыта
// double x = PI; // ERROR! макросы не экспортируются


Модули призваны решать следующие проблемы:

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

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

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

На словах - прекрасные плюсы будущего. Но на словах мы все Львы Толстые, а на деле...

А на деле это все до сих пор работает довольно костыльно. До 23, а скорее 24 года использовать модули было совсем никак нельзя. Сейчас все немного лучше, но реализации все еще пропитаны проблемами. А проекты не спешат переходить на модули. Но почему?

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

😡 Бинарный формат модулей нестандартизирован. Каждый компилятор выдумывает свое представление, которое несовместимо между компиляторами или даже версиями одного компилятора.

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

😡 Очень много усилий нужно потратить на переработку архитектуры и кода существующих проектов, чтобы перевести их на модули.

😡 Ускорение компиляции может неоправдать затрат. В среднем ускорение составляет порядка 30%. И это просто не стоит усилий.

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

😡 Пока популярные библиотеки не начнут распространяться через модули, существующие проекты не будут иметь большое желание переезжать на модули, потому что получится частичное внедрение.

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

Use new features. Stay cool.

#cppcore #compiler #tools
18👍7🔥6
Парсим ужас
#новичкам

Вот мы и рассмотрели все необходимые компоненты, чтобы понять, что написано здесь:

[[]][[]]int main()[[]]{{[][][[]][[]]{{{}}}(main);}}


Если включить clang-format, то код преобразится во что-то такое:

/*1*/[[]][[]] int main()/*2*/[[]] {
{
[]/*3*/[[]](/*4*/[[]] /*5*/auto [])/*6*/[[]][[]] {
{
{}
}
}/*7*/(main);
}
}


Давайте посмотрим, откуда так много скобок:

1️⃣ Перед типом возвращаемого значения main определены 2 пустые области для указания атрибутов функции main.

2️⃣ Перед телом функции main определена пустая область для атрибутов, применяемых к типу функции main.

3️⃣ После блока захвата лямбды определена пустая область для атрибутов, применяемых к самой лямбде.

4️⃣ Внутри списка параметров лямбды определена пустая область для атрибутов, применяемых к единственному параметру лямбды.

5️⃣ Сама лямбда является generic и принимает массив неизвестного типа.

6️⃣ Перед телом лямбды определены 2 пустые области для атрибутов, применяемых к типу лямбды.

7️⃣ Вызываем лямбду с помощью указателя на функцию main.

8️⃣ Ну и разбавили это дело несколькими лишними скоупами по пути.

Не так уж и сложно оказалось)

Так, новичковая часть закончилась.
#опытным

Интересно, что этот код компилируется на gcc, но не на clang.

cppinsights показывает, что лямбда раскрывается во что-то такое:

class __lambda_5_17 {
public:
template <class type_parameter_0_0>
inline /*constexpr */ auto operator()(auto *) const {
{ {}; };
}

private:
template <class type_parameter_0_0>
static inline /*constexpr */ auto __invoke(auto *__param0) {
return __lambda_5_17{}.operator()<type_parameter_0_0>(__param0);
}

public:
// /*constexpr */ __lambda_5_17() = default;
};


То есть по факту мы имеем шаблонный оператор с auto параметром.

Как интерпретировать эту штуку - дело нетривиальное и по ходу компиляторы это делают по-разному. Видимо gcc при попытке инстанцировать шаблон с параметром int() выводит auto как тот же самый тип функции int() и в итоге лямбда принимает указатель на функцию. А clang при попытке инстанцировать шаблон выводит тип параметра функции как массив функций int() и не может принять main в качестве такого параметра.

Пишите ваше мнение, кто прав, кто виноват)

Deal with horrible things step by step. Stay cool.

#cppcore #compiler
❤‍🔥21😁127👍3🔥2