Грокаем 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
​​Самая надежная гарантия отсутствия исключений
#опытным

Исключения не любят не только и не столько потому, что они нарушают стандартный поток исполнения программы, могут привести к некорректному поведению системы и приходится везде писать 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