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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Единица трансляции

В недавнем прошлом на канале было много линковочной тематики. Соответственно это требует частого упоминания термина "единица трансляции" aka translation unit aka юнит. На канале много не нюхавшего прода народа, поэтому пришла в голову идея закрыть этот гештальт.

Вот вы изучаете плюсы. Долго и упорно. И сделали свой первый нетривиальный проект. И бежите радостный показывать его маме со словами: "Мама! Смотри, какую я программу написал.". И показываете ей, как ваши условные крестики-нолики работают. Это вы запустили исполняемый файл, который является результатом превращения вашего кода на с++ в машинный код. Но каким образом все многообразие файлов собирается в одну сущность? Как они между собой взаимодействуют для этого?

В рамках культуры разработки на С++ есть 2 принципиальных вида файлов: headers(заголовочные файлы)(.h|.hpp) и sources(файлы реализации)(.cxx|.cpp). Первые обычно предназначены для помещения в них сущностей, которые будут широко(или потенциально широко) использоваться в проекте. Вторые уже конкретно реализуют поведение этих сущностей. Ответ на вопросы: "Зачем такое разделение в принципе есть?" и "Зачем мы пишем множество файлов реализации?"- заслуживают отдельных постов. Просто примем за данность всем известный факт существования двух видов файлов.

Но на вход компилятора попадает только один тип файлов - sources(не будем касаться precompiled headers и сильно усложнять разговор). На самом деле компилятора может сожрать любой подходящий файл, просто так принято, что это .cpp файлы.

Стандартный source файл состоит из кучи заинклюженых заголовочников, возможно еще несколько видов директив препроцессора, типа #ifdef или #define и прочего, ну и, собственно, нашего кода. Когда такой файл пожирает компилятор, на самом деле первым в работу вступает препроцессор. Это такой предварительный обработчик файлов с кодом. В С и С++ есть несколько директив препроцессора, которые предназначены именно для этого обработчика. Он оперирует в основном текстом программы и фактически делает текстовые манипуляции. Например, директива #include"something.hpp" заменяется на полный текст файла something.hpp. Директива #define MAXARRAYSIZE 10 обозначает, что во всем файле строку MAXARRAYSIZE нужно заменить на 10. Причем 10 это будет прям строка в файле, это надо учитывать. Никакой проверки типов на этом этапе нет.

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

То есть и вправду не очень корректно говорить, что компилируются цппшники. Это делают все-таки единицы трансляции. Но я не оч понимаю людей, которые хейтят других за это, потому что просто культурой принято файлы реализации называть .cpp. А то, что там есть препроцессор - это и так понятно. Это понимает почти любой, кто писал #include <iostream>, а то есть почти все. Я уж преувеличиваю, но вы поняли мою мысль.

Divide et impera. Stay cool.

#compiler #cppcore
🔥15👍53
std::signbit

В прошлом посте мы уже упоминали std::signbit. Сегодня мы посмотрим на эту сущность по-подробнее.

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

bool signbit( float num );  
bool signbit( double num );
bool signbit( long double num );


вот такие перегрузки мы имеем для floating-point чисел. А вот такую:

template< class Integer >  
bool signbit( Integer num );


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

В чем особенность целочисленной перегрузки. В том, что число, которое туда попадает трактуется, как double. Поэтому выражение std::signbit(num) эквивалентно std::signbit(static_cast<double>(num)).

Также эта функция детектирует наличие знакового бита у нулей, бесконечностей и NaN'ов. Да, да. У нуля есть знак. Так что 0.0 и -0.0 - не одно и то же. И если вы внимательные, то заметили даже у NaN есть знак. И std::signbit - один из двух возможных кроссфплатформенных способов узнать знак NaN. Этот факт еще больше мотивирует использовать эту функцию(в ситуациях, где это свойство решает).

Начиная с 23 стандарта функция становится constexpr, что не может не радовать любителей compile-time вычислений.

Для языка С тоже кстати есть похожая сущность. Только там это макрос

#define signbit( arg ) /* implementation defined */


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

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

Look for signs in life. Stay cool.

#cpp23 #cpp11 #goodoldc
👍137🔥7
​​Как посмотреть шаблонный тип
#новичкам

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

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

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

Для шланга и гцц этот макрос называется __PRETTY_FUNCTION__, а для msvc - __FUNCSIG__. Пользоваться ими можно примерно так:

#if defined __clang__ || __GNUC__
#define FUNCTION_SIGNATURE __PRETTY_FUNCTION__
#elif defined __FUNCSIG__
#define FUNCTION_SIGNATURE __FUNCSIG__
#endif

template<class T>
void func(const T& param) {
std::cout << FUNCTION_SIGNATURE << std::endl;
}

func(std::vector<int>{});


Для кланга вывод будет такой:
void func(const T &) [T = std::vector<int>]


Для msvc:
void __cdecl func<class std::vector<int,class std::allocator<int> >>(const class std::vector<int,class std::allocator<int> > &)


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

Можете поиграться в годболте.

See through things. Stay cool.

#compiler #template
19👍11🔥3
Макросы
#новичкам

Один из способов избежать дублирования кода — использовать макросы. Макросы — инструкции препроцессора, которые позволяют заменять одни строки на другие:

#define MAX(a, b) ((a) > (b) ? (a) : (b))


Теперь вы можете использовать этот макрос, возвращающий максимальное из 2-х значений, для любых типов данных:

int result1 = MAX(1, 2);          // Работает
double result2 = MAX(1.5, 2.5); // Тоже работает


Но использование макросов — это игра в русскую рулетку. Никогда не знаешь, когда в голове появится на 2 дырки больше. Они работают на уровне текстовой подстановки, и если что-то пойдет не так, компилятор вам не поможет. Как вы думаете, какие значения будут у переменных result, x и y после выполнения следующего кода?

#define MAX(a, b) ((a) > (b) ? (a) : (b))

int x = 5;
int y = 10;

int result = MAX(++x, ++y);


Мы хотим сравнить две переменные после их инкрементов. Поэтому ожидаемые значения: 11, 6, 11. Однако у препроцессора и компилятора есть свое мнение на этот счет. Реальный вывод:

Result: 12
x: 6
y: 12


Переменная y имеет значение на один больше ожидаемого. Это перестает быть удивительным после того, как мы посмотрим на то, во что раскрывается макрос:

int result = ++x > ++y ? ++x : ++y;


Мы просто подставили текст и в любых значениях x и y, одна из этих переменных претерпит лишний инкремент.

Более сложные ситуации генерируют все менее и менее тривиальные ошибки.

Недостатки макросов

🔞 Нетипобезопасность: Макросы не проверяют типы данных. Программа может скомпилироваться, но упасть в runtime.

🔞 Побочные эффекты: Макросы могут привести к неожиданным результатам, особенно если аргументы содержат побочные эффекты (например, инкременты).

🔞 Сложность отладки: Макросы могут раскрываться в причудливые строки, которые вы просто не увидите в своем коде. Придется отлаживаться по файлу с кодом после препроцессора, а это нетривиальная задача.

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

Поэтому CppCoreGuideLines говорят нам не использовать макросы при определении функций.

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

Don't be confusing. Stay cool.

#cppcore
1👍33😁114🔥3❤‍🔥1
​​Что не так с модулями?
#опытным

Модули появились как одна из мажорных фич С++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
17👍7🔥6