Грокаем C++ @grokaemcpp Channel on Telegram

Грокаем C++

@grokaemcpp


Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.

По всем вопросам - @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat

Грокаем C++ (Russian)

Добро пожаловать в Telegram-канал "Грокаем C++"! Здесь Вас ждут два опытных сеньора в мире программирования - Владимир и Денис, которые будут вашими гидами в изучении и погружении в этот увлекательный мир языка программирования C++.

На канале можно найти полезные советы, интересные статьи, обсуждения и многое другое, что поможет вам стать профессионалом в C++. Если у вас возникли вопросы или вы хотите получить консультацию, обращайтесь к @ninjatelegramm.

Управляющий каналом - @Spiral_Yuri, так что вы всегда можете связаться с ним, если у вас есть идеи или предложения по улучшению канала.
Для рекламы вы можете обратиться по ссылке: https://telega.in/c/grokaemcpp
Присоединяйтесь к нам и станьте частью сообщества, где ценится знание и опыт в программировании на C++! Мы также находимся на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat

Грокаем C++

06 Jan, 08:10


​​Допотопный доступ к многомерному массиву Ч2

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

Самый простой из них - вместо оператора[], который может принимать только один параметр, использовать оператор(), который можно перегружать, как нашей душе угодно.

Он может принимать ничем не ограниченный набор параметров. И к тому же их несколько штук можно определить. Например, если передаем 1 аргумент, то возвращается ссылка на строку матрицы. А если 2 аргумента, то возвращаем ссылку на сам элемент.

Вот так это может выглядеть:

template <typename T>
struct ArraySpan {
ArraySpan(T * arr, size_t arr_size) : data_{arr}, size_{arr_size} {}
ArraySpan(T * arr_begin, T * arr_end) : data_{arr_begin}, size_{std::distance(arr_begin, arr_end)} {}

T& operator()(std::size_t i) {
return *(data_ + i);
}
size_t size() const {return size_;}
T * data() {return data_;}
private:
T * data_;
size_t size_;
};

template <typename T>
struct Matrix {
Matrix() = default;
Matrix(size_t rows, size_t cols, T init) : ROWS{rows}, COLS{cols}, data(ROWS * COLS, init) {}
Matrix(Matrix const&) = default;

ArraySpan<T> operator()(std::size_t row) {
return ArraySpan{data.data() + row * COLS, COLS};
}

T& operator()(std::size_t row, std::size_t col) {
return data[row * COLS + col];
}
std::vector<T>& underlying_array() { return data; }

size_t row_count() const { return ROWS;}
size_t col_count() const { return COLS;}

private:
size_t ROWS;
size_t COLS;
std::vector<T> data;
};

int main() {
Matrix mtrx(4, 5, 0);
auto& interval_buffer = mtrx.underlying_array();
std::iota(interval_buffer.begin(), interval_buffer.end(), 0);
for (int i = 0; i < mtrx.row_count(); ++i) {
for (int j = 0; j < mtrx.col_count(); ++j) {
std::cout << std::setw(2) << mtrx(i, j) << " ";
}
std::cout << std::endl;
}
std::cout << std::endl;
for (int i = 0; i < mtrx.row_count(); ++i) {
auto row = mtrx(i);
for (int j = 0; j < row.size(); ++j) {
std::cout << std::setw(2) << row(j) << " ";
}
std::cout << std::endl;
}
}


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

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

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

Ну и непонятно, мы вообще функцию вызываем или что??


Тем не менее, этот способ есть даже в FAQ на странице, посвященному стандарту C++. И он совместим с нотацией индексации в Фортране. Поэтому метод довольно распространенный.

Be consistent. Stay cool.

#cppcore

Грокаем C++

06 Jan, 07:10


Совет IT-спецам на 2025 год - чаще посещайте тематические мероприятия

Зачем тратить время на Ютубчик, когда можно интересно проводить время, при этом развивая свои навыки!

А чтобы не гуглить афиши и не подписываться на сотню разных каналов, IT-мероприятия России уже собрали все самые топовые события в одном месте.

Оффлайн и онлайн, для разных специальностей и уровней знаний.

👨🏻‍💻 В этом канале ты найдешь самые актуальные и интересные события: анонсы форумов, конференций, митапов, вебинаров, хакатонов, олимпиад и многое другое.

Присоединяйся к комьюнити лучших спецов и заново влюбись в свою работу:
IT мероприятия России / ITMeeting / IT events

Грокаем C++

05 Jan, 09:00


​​Допотопный доступ к многомерному массиву Ч1
#опытным

Начнем рассказ о том, как люди до С++23(то есть до сих пор) жили с оператором[], принимающим только один параметр.

И начнем мы с банальщины. Вот у нас есть класс матрицы. По всем канонам С++ мы должны получать доступ к ее элементам вот так matrix[i][j]. Этот формат сохраняет констистентность с доступом к элементам одномерных массивов.

Однако сразу натыкаемся на проблему. Класс один, а вызываем мы оператор два раза. Несостыковочка.

Ее решает паттерн прокси. Мы создаем прокси класс и возвращаем его объект из первого индекса. Дальше у этого прокси класса определяем оператор[] и на выходе получаем наш элемент.

Условно, из первого оператора возвращаем ссылку на строку матрицы, а из второго - уже сам элемент.

Выглядит это примерно так:

template <typename T>
struct ArraySpan {
ArraySpan(T * arr, size_t arr_size) : data_{arr}, size_{arr_size} {}
ArraySpan(T * arr_begin, T * arr_end)
: data_{arr_begin}
, size_{std::distance(arr_begin, arr_end)} {}

T& operator[](std::size_t i) {
return *(data_ + i);
}

size_t size() const {return size_;}
T * data() {return data_;}
private:
T * data_;
size_t size_;
};

template <typename T>
struct Matrix {
Matrix() = default;
Matrix(size_t rows, size_t cols, T init) : ROWS{rows}, COLS{cols}, data(ROWS * COLS, init) {}
Matrix(Matrix const&) = default;
ArraySpan<T> operator[](std::size_t row) {
return ArraySpan{data.data() + row * COLS, COLS};
}
std::vector<T>& underlying_array() { return data; }
size_t row_count() const { return ROWS;}
size_t col_count() const { return COLS;}
private:
size_t ROWS;
size_t COLS;
std::vector<T> data;
};

int main() {
Matrix mtrx(4, 5, 0);
auto& interval_buffer = mtrx.underlying_array();
std::iota(interval_buffer.begin(), interval_buffer.end(), 0);
for (int i = 0; i < mtrx.row_count(); ++i) {
for (int j = 0; j < mtrx.col_count(); ++j) {
std::cout << std::setw(2) << mtrx[i][j] << " ";
}
std::cout << std::endl;
}
std::cout << std::endl;
for (int i = 0; i < mtrx.row_count(); ++i) {
auto row = mtrx[i];
for (int j = 0; j < row.size(); ++j) {
std::cout << std::setw(2) << row[j] << " ";
}
std::cout << std::endl;
}
}

В примере мы 2 раза проходимся по матрице, чтобы продемонстрировать возможность индексации элементов через объект самой матрицы и объекта строки.

Можно было бы конечно не писать отдельно наш прокси тип ArraySpan, а использовать готовый std::span из С++20, но оставим так. Идея использовать такой легковесный объект понятна - нам не нужно лишнего оверхеда на копирование или создание сложного объекта, чтобы просто получить доступ к элементу матрицы.

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

В чем недостаток такого способа? Куча прокси классов, которые возможно и не нужны. Возможно, нам просто нужна двумерная структура, чтобы получать доступ к конкретным элементам. Использование карты для морского боя не предполагает использование отдельных строк или столбцов. Хотелось бы просто индексировать конкретные элементы. Но тем не менее мы вынуждены использовать прокси класс.

А если структура трухмерная? Уже 2 прокси нужно будет. Больше вложенность - больше классов-прослоек. Не очень удобно. Поэтому и придумали другие способы, о которых расскажу в следующих постах.

Find the way out. Stay cool.

#cppcore #cpp20 #cpp23

Грокаем C++

31 Dec, 11:00


​​Новый год на пороге, а значит наступило время подводить итоги уходящего года! 🤩

Много всего произошло! Больше 7500 новых подписчиков и больше 300 авторских контентных постов было опубликовано. Очень круто, что у нас получилось постоянно вести этот проект, делиться с вами знаниями и самим учиться. Проект забирает много времени, не каждый готов вывести такую нагрузку.

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

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

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

Ну что это мы все о себе, да о себе...

Желаем вам в Новом году не останавливаться на достигнутом и мчаться вперед на всех парах! Ставьте перед собой цели и достигайте их. И главное - не давайте змею искусителю сбить вам с пути😉. Никаких питонов - только Плюсы, только хардкор!

Этим постом мы завершаем год и уходим на небольшой перерыв. Увидимся в следующем году! С праздником вас, дорогие положительные люди! 🥂🥳

Happy New Year! Stay cool.

Грокаем C++

28 Dec, 10:00


​​Оператор запятая внутри operator[]

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

Именно поэтому начиная с С++20 использование оператора запятая внутри оператора квадратные скобки признано устаревшим(deprecated).

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

Итого:

void f(int *a, int b, int c) 
{
a[b,c]; // deprecated
a[(b,c)]; // OK
}


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

Кроме того, скорее всего уже были намеки, что в 23-х плюсах появится мультипараметрический интерфейс для operator[]. Поэтому стандарт заранее позаботился о том, чтобы был всего одна разрешенная семантика использования нескольких параметров в этом операторе.

Remove error-prone things. Stay cool.

#cpp20 #cpp23

Грокаем C++

26 Dec, 09:00


Ответ

На самом деле в код выше я упустил оператор delete[], поэтому в нем есть утечка памяти. Этого я не учел, так как на другом концентировался.

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

#include <iostream>

int main() {
auto array = new int[10, 20]{10};
std::cout << array[1, 0] << " " << array[11, 1] << std::endl;
delete[] array;
}



Такой код соберется выведет на консоль "10 0" и успешно завершится.

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

Основная загвоздка в том, что в С++ запятая - это не просто знак препинания. Это оператор! У него есть вполне четкое и прописанное поведение - он игнорирует результат любого выражения, кроме последнего.

То есть

Expression1, Expression2, Expression3


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

Поэтому когда мы пишем [i, j] до с++23, то это полностью эквивалентно [j]. Компилятор видит несколько параметров в [] и, так как сам оператор индексации не принимает несколько параметров, то выход только один - парсить через оператор запятая.

Получается, что с помощью new int[10, 20] мы создали одномерный массив на 20 элементов.

Ну и вообще, весь код полностью эквивалентен следующему:

#include <iostream>

int main() {
auto array = new int[20]{10};
std::cout << array[0] << " " << array[1] << std::endl;
delete[] array;
}


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

Теперь, почему 10 0.

При аггрегированной инициализации мы можем в фигурных скобках указывать меньше элементов, чем может поместиться в массив или структуру. При этом остальные элементов будут инициализироваться так, как если бы они инициализировались от пустых скобок(аля array_elem = {};). Для интов это значит, что все элементы, кроме первого будут иметь нулевое значение.

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

Вот такая противная запятая.

Don't be confused. Stay cool.

#cpp23 #cppcore

Грокаем C++

25 Dec, 09:00


Квиз

В тему индексирования многоразмерных массивов закину сегодня #quiz. Ничего супер запутанного, но от этого не менее интересно.

Вот мы в прошлых постах говорили, что начиная с С++23 мы можем определять оператор[], который принимает несколько аргументов.

А что будет, если мы просто возьмем и создадим многомерный массив, как нам бы это было удобно? И будет получать доступ к его элементам:

#include <iostream>

int main() {
auto array = new int[10, 20]{10};
std::cout << array[1, 0] << " " << array[11, 1] << std::endl;
}


Видели когда-нибудь такое? Даже если нет, у меня к вам всего один вопрос.

Какой будет результат попытки компиляции и запуска этого кода ?

Грокаем C++

23 Dec, 09:00


​​std::mdspan
#опытным

"Я понял, что можно перегружать оператор[] для разного числа аргументов. Но это только для моих классов. А что делать со стандартными контейнерами типа std::vector? Могу я как-то на нем использовать многоаргументный оператор, если по факту я храню в нем матрицу?"

И нет, и да.

Интерфейс семантически одномерного контейнера никто менять не будет.

Однако вместе с С++23 появился еще один полезный класс std::mdspan. Это фактически тот же std::span, то есть это view над одномерной последовательностью элементов, только он интерпретирует ее, как многомерный массив.

То есть вы теперь буквально можете интерпретировать свой std::array или std::vector, как многомерный массив.

И! У std::mdspan переопределен оператор[], который может принимать несколько измеренений и выдает ссылку на соответствующий элемент.

std::vector v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
 
// View data as contiguous memory representing 2 rows of 6 ints each
auto ms2 = std::mdspan(v.data(), 2, 6);
// View the same data as a 3D array 2 x 3 x 2
auto ms3 = std::mdspan(v.data(), 2, 3, 2);

// Write data using 2D view
for (std::size_t i = 0; i != ms2.extent(0); i++)
for (std::size_t j = 0; j != ms2.extent(1); j++)
ms2[i, j] = i * 1000 + j;

// Read back using 3D view
for (std::size_t i = 0; i != ms3.extent(0); i++)
{
std::println("slice @ i = {}", i);
for (std::size_t j = 0; j != ms3.extent(1); j++)
{
for (std::size_t k = 0; k != ms3.extent(2); k++)
std::print("{} ", ms3[i, j, k]);
std::println("");
}
}


Вывод:

slice @ i = 0
0 1
2 3
4 5
slice @ i = 1
1000 1001
1002 1003
1004 1005


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

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

Метод extend возвращает размер вьюхи по заданному ранк индексу.

Так что скоро можно даже будет обойтись без сооружения своих оберток над стандартными контейнерами для получения доступа к многомерному оператору[]. И использовать стандартный инструмент std::mdspan.

Use standard things. Stay cool.

#cpp23 #STL

Грокаем C++

23 Dec, 08:00


Владимир, вы уволены! Вместо вас мы взяли нейросеть!

Рынок IT скоро захватят ChatGPT и Gemini. А они появились лишь в 2022 году.

Станьте тем, кто получает выгоду с  ИИ. Как их использовать для кода подробно рассказывают на канале Artificial Intelion.

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

Переложите свои задачи на ИИ, чтобы не просрачивать дедлайны - @artificial_intelion

Грокаем C++

22 Dec, 09:00


​​Доступ к элементам многомерных структур
#опытным

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

Если вы спросите кого-то, кто не очень опытен или работает в нематематической области, есть большая вероятность, что ответ будет таким, что вы должны использовать несколько операторов[] подряд: myMatrix[x][y].

Есть несколько проблем с таким подходом:

⛔️ Это не очень удобно чисто внешне. Все номальные люди используют синтаксис [x, y].

⛔️ Это работает "из коробки" на реально многомерных структурах, типа вложенных массивов(типа вектора векторов). Чтобы поддержать даже такой синтаксис для кастомных классов, придется несколько приседать.

⛔️ Поэтому многие находят лазейки, чтобы делать что-то похожее на [x, y], этих лазеек много, нет какого-то стандарта.

⛔️ Стандарт использует operator[] с одним аргументом для получения доступа к элементам массивов.

⛔️ Лазейки неконсистентны с одноразмерными массивами в плане получения доступа к элементам.

⛔️ Некоторые из них преполагают спорную семантику, а некоторые делают практически нечитаемыми сообщения об ошибках компиляции.

⛔️ Возможные проблемы с инлайнингом.

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

В С++23 наконец завезли многоаргументный operator[]. Теперь при проектировании своей матрицы или даже тензора перегружать оператор[] для 1, 2, 3 и более входных аргументов. Так для матрицы можно возвращать элемент, если мы передали 2 размерности, или возвращать всю строку, если мы передали только одну размерность.

template <typename T, std::size_t ROWS, std::size_t COLS>
class Martrix {
std::array<T, ROWS * COLS> a;
public:
Martrix() = default;
Martrix(Martrix const&) = default;
constexpr T& operator[](std::size_t row, std::size_t col) { // C++23 required
assert(row < ROWS and col < COLS);
return a[COLS * row + col];
}
constexpr std::span<T> operator[](std::size_t row) {
assert(row < ROWS);
return std::span{a.data() + row * COLS, COLS};
}
constexpr auto& underlying_array() { return a; }
};

int main() {
constexpr size_t ROWS = 4;
constexpr size_t COLS = 3;
Martrix<char, ROWS, COLS> matrix;
// fill in the underlying 1D array
auto& arr = matrix.underlying_array();
std::iota(arr.begin(), arr.end(), 'A');
for (auto row {0U}; row < ROWS; ++row) {
std::cout << "│ ";
for (auto col {0U}; col < COLS; ++col) {
std::cout << matrix[row, col] << " │ ";
}
std::cout << "\n";
}
std::cout << "\n";
auto row = matrix[1];
for (auto col {0U}; col < COLS; ++col) {
std::cout << row[col] << ' ';
}
}


Здесь мы создали матрицу 4х3, заполнили ее буквами алфавита и вывели на экран каждый элемент через matrix[x, y]. А также дальше получили целую строку матрицы через matrix[x] и вывели ее содержимое на экран:

│ A │ B │ C │ 
│ D │ E │ F │
│ G │ H │ I │
│ J │ K │ L │

D E F


В качестве обертки для строки используем std::span из С++20.

Очень красиво и удобно. Разработчикам математических библиотек сделали большой подарок.

Be consistent. Stay cool.

#cpp23 #cpp20

Грокаем C++

21 Dec, 10:00


​​Фикс баги с инициализацией инта

В прошлом посте говорили об одной неприятности при использовании универсальной инициализации интов. При таком написании:

auto i = {0};

i будет иметь тип std::initializer_list<int>.

С++17 исправил такое поведение. Но для полного понимания мы должны определить два способа инициализации: копирующая и прямая. Приведу примеры

  auto x = foo();  // копирующая инициализация
auto x{foo()}; // прямая инициализация,
// проинициализирует initializer_list (до C++17)
int x = foo(); // копирующая инициализация
int x{foo()}; // прямая инициализация

Для прямой инициализации вводятся следующие правила:

• Если внутри скобок 1 элемент, то тип инициализируемого объекта - тип объекта в скобках.
• Если внутри скобок больше одного элемента, то тип инициализируемого объекта просто не может быть выведен.

Примеры:

auto x1 = { 1, 2 }; // decltype(x1) -  std::initializer_list<int> 
auto x2 = { 1, 2.0 }; // ошибка: тип не может быть выведен,
// потому что внутри скобок объекты разных типов
auto x3{ 1, 2 }; // ошибка: не один элемент в скобках
auto x4 = { 3 }; // decltype(x4) - std::initializer_list<int>
auto x5{ 3 }; // decltype(x5) - int


Этот фикс компиляторы реализовали задолго до того, как стандарт с++17 был окончательно утвержден. Поэтому даже с флагом -std=c++11 вы можете не увидеть некорректное поведение. Оно воспроизводится только на древних версиях. Можете убедиться тут.

Fix your flaws. Stay cool.

#cpp11 #cpp17 #compiler

Грокаем C++

21 Dec, 07:00


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

Я понимаю их. Приходишь на собес и тебе предлагают решить совсем какую-то оторванную от реальности задачу. "Обведите острова на карте". Это почти смешно. Когда и кому эти навыки пригодятся - не понятно.

Однако не все лежит на поверхности.

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

И нужен инструмент, который поможет это проверить. Именно таким инструментом и являются алгосы. Популярно объясняю:

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

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

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

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

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

Какой вывод?

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

Хочешь писать эффективный код? Тебе туда.

Хочешь успешно проходить собесы? Тебе туда.

Хочешь накачать мышцу компьютер сайенса? Тебе туда.

Хочешь знать, что за птица эта Дейкстра, где в коде можно встретить бор, и кто сделал кучу на дереве? Знаю, что хочешь. Тебе тоже туда.

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

Все дороги идут туда. Так чего ждать?

Реклама, АНО ДПО “Образовательные технологии Яндекса”, ИНН 7704282033, 119021, г. Москва, ул. Тимура Фрунзе, д. 11, корпус 2, erid: 2SDnjeQi4jV

Грокаем C++

20 Dec, 09:00


Если бы у вас был 1'000'000$ на реализацию вашей айтишной бизнес идеи, то какой стартап бы вы создали?

Грокаем C++

19 Dec, 09:00


​​Баг универсальной инициализации

В С++11 нам завезли прекрасную фичу - автоматический вывод типов с помощью ключевого слова auto
. Теперь мы можем не беспокоится по поводу выяснения типа итераторов для какой-нибудь мапы от мапы о вектора и написания этого типа. Вместо этого можно просто сделать вот так:
const auto it = map_of_map_of_vectors_by_string_key.find(value);


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

struct MyClass {
int a = 0;
};

int main() {
MyClass x();
std::cout << x.a << std::endl;
}

компилятор выдаст что-то такое: warning: empty parentheses interpreted as a function declaration.

Ну вот захотел я вызвать дефолтный конструктор явно. А мое определение трактуют как объявление функции(most vexing parse). Если круглые скобки заменить на фигурные, то все будет пучком.

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

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

auto i{0};


А что, имеем право. auto ведь из инициализатора выводит тип, да?. Передали в качестве инициализатора целочисленный литерал.

Какой тип будет у i?

i будет иметь тип std::initializer_list<int>. Снова проблема именно в синтаксисе определения std::initializer_list и фигурными скобками.

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

Да, в будущих стандартах эту "особенность" убрали, но не все могут пользоваться самими современными стандартами. Но хорошие новости, что, скорее всего, на современных версиях компиляторов при -std=c++11 вы не получите этого бага. Этот момент я объясню в следующем посте.

Don't be confusing. Stay cool.

#cpp11 #cppcore

Грокаем C++

17 Dec, 10:00


​​Почему перегрузки не могут иметь разные возвращаемые значения
#новичкам

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

Но никогда не поздно задуматься над смыслом. А почему мы не можем перегружать функции по возвращаемому значению?

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

int GetRandomNumber();
float GetRandomNumber();


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

Теперь попробуем вызвать первую версию.

int number = GetRandomNumber();


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

Ситуация становится комичной, если использовать auto:

auto number = GetRandomNumber();


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

Вот и получили противоречие.

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


Don't be confusing. Stay cool.

#cppcore

Грокаем C++

16 Dec, 10:00


std::visit
#опытным

Не так уж и просто работать с вариантными типами. Надо точно знать, какого типа объект находится внутри. Если не угадали - получили исключение. Ну или тестить объект на содержание в нем конкретного типа с помощью лапши из if-else.

Так вот чтобы голова не болела при работе с std::variant надо 2 раза в день после еды принимать std::visit.

Эта функция позволяет применять функтор к одному или нескольким объектам std::variant. И самое главное, что вам не нужно беспокоиться по поводу того, какой именно объект находится за личиной варианта. Компилятор все сам сделает.

template< class Visitor, class... Variants >  
constexpr visit( Visitor&& vis, Variants&&... vars );

template< class R, class Visitor, class... Variants >
constexpr R visit( Visitor&& vis, Variants&&... vars );


Так выглядят ее сигнатуры. Первым параметром передаем функтор, дальше идут варианты.

Попробуем использовать эту функцию:

using var_t = std::variant<int, long, double, std::string>;

std::vector<var_t> vec = {10, 15l, 1.5, "hello"};

for (auto& v: vec)
{
var_t w = std::visit([](auto&& arg) -> var_t { return arg + arg; }, v);
std::visit([](auto&& arg){ std::cout << arg; }, w);
}
//OUTPUT:
// 20 30 3 hellohello


Главное, чтобы функтор умел обрабатывать любую комбинацию типов, которую вы можете передать в него. Обратите внимание, что мы используем здесь generic лямбду, которая может принимать один аргумент любого типа.

Если вы хотите передать в std::visit несколько объектов, то функтор должен принимать ровно такое же количество аргументов и уметь обрабатывать любую комбинацию типов, которая может содержаться в вариантах.

std::visit([](auto&&... arg){ ((std::cout << arg << " "), 
...,
(std::cout << std::endl)); }, vec[0], vec[1]);
std::visit([](auto&&... arg){ ((std::cout << arg << " "),
...,
(std::cout << std::endl)); }, vec[0], vec[1], vec[2]);
// OUTPUT
// 10 15
// 10 15 1.5


Используем здесь дженерик вариадик лямбду, чтобы она могла принимать столько аргументов, сколько нам нужно. И эта конструкция работает для любого количества переданных объектов std::variant;

Так что std::variant и std::visit - закадычные друзья и им друг без друга грустно! Не заставляйте их грустить.

Have a trustworthy helper. Stay cool.

#template #cpp17

Грокаем C++

15 Dec, 09:00


Получаем адрес перегрузки
#новичкам

Представьте, что у вас есть функция, которая вызывает любой коллбэк:

template<class T, class... Args>
void call_callback(T callback, Args... args) {
callback(args...);
}


И есть другая функция, которую вы вызываете через call_callback.

int func() {
return 42;
}

call_callback(func);


Все работает прекрасно. Но теперь мы добавляем перегрузку func и пытаемся их вызвать через call_callback.

int func() {
return 42;
}

int func(int num) {
return num;
}

call_callback(func);
call_callback(func, 42);


Получаем ошибку

error: no matching function for call to
‘call_callback(<unresolved overloaded function type>)


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

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

Что же делать?

Дать компилятору подсказку и использовать static_cast.

call_callback(static_cast<int(*)()>(func));
call_callback(static_cast<int(*)(int)>(func), 42);


Теперь все работает, как надо.

Give useful hints. Stay cool.

#cppcore

Грокаем C++

14 Dec, 09:11


Получаем адрес стандартной функции

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

std::transform(s.begin(), s.end(), s.begin(), std::toupper);


Вроде ничего страшного не должно случиться. Но согласно стандарту, поведение программы в этом случае unspecified и потенциально даже ill-formed.

Все потому что нельзя брать адреса у стандартных функций. Начиная с С++20 это явно прописано в стандарте.

Let F denote a standard library function, a standard 
library static member function, or an instantiation of
a standard library function template.

Unless F is designated an addressable function,
the behavior of a C++ program is unspecified
(possibly ill-formed) if it explicitly or implicitly
attempts to form a pointer to F.

Possible means of forming such pointers include
application of the unary & operator, addressof,
or a function-to-pointer standard conversion.

Moreover, the behavior of a C++ program is
unspecified (possibly ill-formed) if it attempts
to form a reference to F or if it attempts to form
a pointer-to-member designating either a standard library
non-static member function or an instantiation of
a standard library member function template.


Нельзя формировать указатели и ссылки на стандартные функции и нестатические методы.


Но почему? Чем они отличаются от обычных функций?

На самом деле ничем. Дело в том, что будет с функциями в будущем.

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

А стандарт - это штука не статичная. В него постоянно добавляются новые фичи и обновляются в том числе старые инструменты. Например, с приходом С++11 у нас появилась мув-семантика. И условный метод вектора push_back обзавелся новой перегрузкой для правой ссылки. И это сломало код, который брал адрес метода push_back.

#include <vector>

template<typename T>
void Invoke(std::vector<int>& vec, T mem_fun_ptr, int arg)
{
(vec.*mem_fun_ptr)(arg);
}

int main()
{
std::vector<int> vec;
Invoke(vec, &std::vector<int>::push_back, 42);
}


Этот код успешно собирается на 98 плюсах, но не может этого сделать на 11-м стандарте. Можете поиграться с примером на годболте.

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

std::transform(s.begin(), 
s.end(),
s.begin(),
[](unsigned char c){ return std::toupper(c); });


Don't break your future. Stay cool.

#cppcore #cpp20

Грокаем C++

13 Dec, 09:00


​​std::jthread

С std::thread в С++ есть один интересный и возможно назойливый нюанс. Давайте посмотрим на код:

int main()
{
std::thread thr{[]{ std::cout << "Hello, World!" << std::endl;}};
}


Простой Хелло Ворлд в другом потоке. Но при запуске программы она тут же завершится примерно с таким сообщением: terminate called without an active exception.

Эм. "Я же просто хотел быть счастливым вывести в другом потоке сообщение. Неужели просто так не работает?"

В плюсах много чего просто так не работает)

А вот такая программа:

int main()
{
std::thread thr{[]{ std::cout << "Hello, World!" << std::endl;}};
std::this_thread::sleep_for(std::chrono::seconds(1));
}


Все-таки напечатает Hello, World!, но потом все равно завершится с std::terminate.

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

С помощью слипа мы немного затормозили main тред и сообщение появилось. То есть мы оттянули выход из main и завершение программы и дали возможность подольше поработать новосозданному потоку. А что происходит при выходе из скоупа функции? Вызов деструкторов локальных объектов.

Так вот в деструкторе единственного локального объекта и проблема. Согласно документации, для каждого потока мы обязательно должны выбрать одну из 2-х стратегий поведения: отсоединить его от родительского потока или дождаться его завершения. Делается это методами detach и join соответственно.

И если мы не вызовем один из этих методов для объекта потока, то в своем деструкторе он вызовет std::terminate. То есть корректный код выглядит так:

int main()
{
std::thread thr{[]{ std::cout << "Hello, World!" << std::endl;}};
thr.join();
}


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

Но зачем эти формальности? Вообще говоря, часто мы хотим присоединить поток почти сразу перед вызовом его деструктора. А вот отсоединяем поток мы почти сразу после создания объекта. Мы же заранее знаем, хотим ли мы отпустить поток в свободное плавание или нет. И, учитывая эти факты, было бы приятно иметь возможность не вызывать join самостоятельно, а чтобы он за нас вызывался в деструкторе.

И С++20 приходит здесь нам на помощь с помощью std::jthread. Он делает ровно это. Если его не освободили и не присоединили мануально, то он присоединяется в деструкторе.

Поэтому такой код сделает то, что мы ожидаем:

int main()
{
std::jthread thr{[]{ std::cout << "Hello, World!" << std::endl;}};
}


jthread не только этим хорош. Его исполнение можно еще отменять/приостанавливать. Но об этом уже в другой раз.

Кстати. Вопрос на засыпку. Слышал, что там какие-то сложности у кланга с jthread были. Сейчас все нормально работает?

Make life easier. Stay cool.

#cpp20 #concurrency

Грокаем C++

12 Dec, 10:00


Дефолтные и ссылочные параметры

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

И вот допустим, у нас программист Иннокентий. У него в проекте есть функция, которая хорошо и без нареканий работает и отправляет запрос базе

bool DBConnection::SendQuery(const char* query, 
const DbQueryParams& params = DbQueryParams{});

Если запрос отослан удачно, возвращаем тру, если нет, то фолс.

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

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

А в проекте у него используется паттерн с выходными параметрами. Обычно это реализовывается так: в начале в аргументах функции идут все входные параметры и затем все выходные. Вроде как логично.

Он, согласно своему код стайлу, пишет:

bool DBConnection::SendQuery(const char* query, 
const DbQueryParams& params = DbQueryParams{},
DbConnectionState& state);


Реализовал функцию и запустил билд. А он, хромоногий, упал.

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

Видимо это сделано так, потому что иначе появляется пространство для неопределенности.

Ладно еще в этом случае компилятор видит разные типы и может как-то соотнести 2 и 2. Но например в случае тривиальных типов все не так однозначно. Они могут неявно конвертироваться друг в друга и тут уже распарсить нельзя.

void foo(int i, float j = 5.0, unsigned k);

foo(1, 2); // Невозможно понять, вызвали ли вы foo(1, 5.0, 2) или
например по ошибке передали слишком мало аргументов


Закатив глаза и особо не думая, он исправляет:

bool DBConnection::SendQuery(const char* query, 
const DbQueryParams& params = DbQueryParams{},
DbConnectionState& state = DbConnectionState::OK);


И билд тоже падает! Но теперь уже по другому поводу.

Неконстантные левоссылочные параметры нельзя определять со значениями по-умолчанию.

Причина тут очень простая. Вы не можете создать левую ссылку из значения типа rvalue reference. У объекта должно быть имя, чтобы его можно было присвоить неконстантной левой ссылке. У DbConnectionState::OK имени нет, поэтому и упали.

Выход только один. Нарушать свой код-стайл. Придется пихать параметр DbConnectionState& state либо первым параметром, либо между query и params.

Первый способ вообще в принципе нарушает все негласные соглашения в объявлениях функции среди всех языков.

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

Кенни не придумал ничего лучше и сильно расстроился. Пошел в лес, увидел там машину, сел в нее и сгорел.

А что Иннокентий в принципе мог сделать в этой ситуации? Жду ваших вариантов в комментариях)

Don't worry. All difficulties will pass.

#cppcore

Грокаем C++

11 Dec, 09:00


Подкасты

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

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

Кажется, есть кандидат, который сочетает в себе и то, и другое. Еще и на смежную с плюсами тему!

У компании Ядро(неподтвержденная информация) есть свой подкаст "Битовые маски", где специалисты высочайшего класса обсуждают низкоуровневое программирование, процессоры, компиляторы и операционные системы.

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

У ребят уже 18 выпусков, так что контента вам хватит за глаза.

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

Вот ссылочка на плейлист с выпусками: https://www.youtube.com/watch?v=wknD9AGvKdc&list=PL0YYm7t_DM63uOt3OF2qRpB5rL27aceLs

Enjoy education. Stay cool.

Грокаем C++

10 Dec, 10:00


​​std::apply
#опытным

Метапрогеры очень любят работать с компайл-тайм структурами, типа std::array, std::pair и std::tuple. Когда работают с такими структурами, то интересны прежде всего элементы этих структур. И очень хочется иногда как-то единообразно передавать их распакованные элементы куда-то в другую функцию.

Именно этим и занимается std::apply, которая появилась в С++17. По факту, эта такое дженерик решение для того, чтобы вызвать какую-то функцию с аргументами из элементов tuple-like объектов.

Простейшее, что можно с ней делать - вывести на экран все элементы тапла.

const std::tuple<int, char> tuple = std::make_tuple(5, 'a');
std::apply([](const auto&... elem)
{
((std::cout << elem << ' '), ..., (std::cout << std::endl));
}, tuple);


Здесь мы применяем fold-expression и оператор-запятая. Можете освежить знания в этом посте.

Можно придумать чуть более сложную задачу. Надо написать функцию, которая принимает неограниченное число параметров, в том числе и tuple-like объекты. Все параметры надо распаковать в строку, а tuple-like объекты выделить с помощью фигурных скобок. Объекты естественно могут быть вложенные.

Может получится что-то такое:

template <typename T, typename = void>
struct is_tuple_like : std::false_type {};
template <typename T>
struct is_tuple_like<T, std::void_t<decltype(std::tuple_size<T>::value), decltype(std::get<0>(std::declval<T>()))>> : std::true_type {};
template <typename T>
constexpr bool is_tuple_like_v = is_tuple_like<T>::value;

template<typename Tval, typename ... T>
void serialize_tuple_like(std::stringstream &outbuf, const Tval& arg, const T& ... rest) noexcept {
if constexpr (is_tuple_like_v<Tval>){
outbuf << "{ ";
std::apply([&outbuf](auto const&... packed_values) {
serialize_tuple_like(outbuf, packed_values ...);
}, arg);
outbuf << " }";
}
else{
outbuf << arg;
}

if constexpr(sizeof...(rest) > 0){
outbuf << ' ';
serialize_tuple_like(outbuf, rest ...);
}
}

template<typename ... T>
std::string args_to_string(const T& ... args) noexcept {
std::stringstream outbuf{};
if constexpr(sizeof...(args) > 0){
serialize_tuple_like(outbuf, args ...);
}
return outbuf.str();
}

int main(){
std::cout << args_to_string("test", 1,
std::tuple{"tuple1", 2, 3.0,
std::tuple{"tuple2", "boom"}},
std::pair{"pair", 4},
std::array{5, 6, 7, 8, 9});
}


Вывод будет такой:
test 1 { tuple1 2 3 { tuple2 boom } } { pair 4 } { 5 6 7 8 9 }


Даже не знаю, как эту лапшу разбирать. Идея такая что is_tuple_like_v проверяет аргумент на соответствие tuple-like объекту. Если нам на очередном вызове serialize_tuple_like попался такой объект, то мы берем и распаковываем его параметры в рекурсивный вызов serialize_tuple_like. Если у нас не tuple-like объект, то просто выводим его в стрим. Наверное, нужны проверки на то, что объект можно вывести в стрим, но решил, что это немного борщ для этого кода.

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

Don't live in metaverse. Stay cool.

#template #cpp17

Грокаем C++

09 Dec, 08:28


​​Почему приватные методы находятся в описании класса?
#опытным

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

Приватные методы - это вообще-то детали реализации. Если я в своем фреймворке по-приколу назову какой-нибудь приватный метод KillTheNigga, то другим людям уже нельзя будет пользоваться этим фреймворком. Закенселлят еще меня. Хотя какая блин разница, какие у меня там приватные методы? Они типа и названы private, чтобы показать, что они МОИ ЛИЧНЫЕ(никому не отдам). А их оказывается нужно еще и показывать всем. Что-то не сходится.

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

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

А во-вторых, оказывается в С++ приватные методы участвуют в разрешении перегрузок(внезапно). В целом, так оно и должно быть. Никто не мешает вам определить публичный метод и перегрузить его приватным методом. Проблема(или фича) в том, что этап разрешения перегрузок стоит перед проверкой модификатора доступа. То есть даже если метод приватный и его никто не должен увидеть, он все равно участвует в разрешении перегрузок наряду со всеми остальными методами. Поэтому каждый клиент должен видеть полный набор приватных методов. Об этом мы уже говорили в контексте pimpl идиомы.

В чем прикол такого дизайн решения?

Давайте представим, что такого правила нет. И вот у нас есть две перегрузки, одна приватная для double, другая публичная для int. И перегрузка с double всегда отбрасывалась бы только лишь по причине того, что она приватная. Тогда мы легко можем вызвать публичную функцию с дробным числом 1.5 и нам ничего не будет. Оно просто неявно приведется к int и все на этом.

А теперь посмотрим, что будет, если мы поменяем модификатор приватной перегрузки на публичный? Ничего не упадет, НО! наш вызов метода с аргументом 1.5 теперь пойдет в другую функцию! То есть изменится поведение объекта. Комитет хотел избежать таких ситуаций, поэтому ввели такое вот ограничение. Наверное, причина не одна. Но это весомый аргумент.

Однако, такой протокол поведения влечет за собой различные сайд-эффекты. Я могу удалить(=delete) приватную перегрузку публичного метода, например какую мы обсуждали выше. И вызвать публичный метод опять с дробным числом. Но компилятор на меня наругается, потому что я попытался вызвать удаленную перегрузку метода. Хотя она вообще-то объявлена, как приватная! А то, что я ее удалил - это детали реализации. Получается раскрытие деталей реализации.

Stay aware. Stay cool.
#design #howitworks #cppcore #hardcore

Грокаем C++

08 Dec, 09:18


​​const this

Все мы знаем, что this - указатель на объект, на котором сейчас находится контекст исполнения. Будь то метод, default member initializer или лямбда выражение. И вот есть в этих ваших интернетах такое мнение, что этот this - нулевой неявный аргумент метода и он передается в него в таком виде:

void Foo::bar(Foo * const this, Type1 param1, Type2 param2) {}


То есть типа this - константный указатель.

Так наверное можно думать, но это не совсем правда. Точнее так. Действительно, this подразумевает под собой адрес неявного аргумента-объекта. Но нигде не написано, что он константный.

Да ему нельзя ничего присваивать. Например

void Foo::change(Foo *foo) { this = foo; }


При компиляции кода появится примерно такая ошибка: lvalue required as left operand of assignment. Но если приглядеться, то ни про какой const там речь не идет. Ему не хватает lvalue слева.

Все потому что this - prvalue выражение. То есть можно даже сказать, что это и не указатель. Это выражение, значение которого равно адресу объекта. Оно не может быть использовано слева от знака равенства и у него нельзя взять адрес.

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

Но вот cv-квалификация метода может повлиять на cv-квалификацию указателя на объект. Тип this в обычном методе - Foo *(указатель на Foo). Однако для cv-квалифицированных методов this становится cv Foo *. То есть:

void Foo::ConstMemberFunction(Type1 param1, Type2 param2) const {
// this - const Foo *
this->field = param1; // Error!
}


Сделано это, естественно, чтобы мы никак не могли изменить объект, на который указывает this, в константном методе.

Так что this - prvalue и точка!

Make points in your life. Stay cool.

#cppcore

Грокаем C++

07 Dec, 09:00


​​Double lookup
#опытным

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

Не нужно использовать методы count(key) и contains(key), если вы потом собираетесь работать с этим ключом в ассоциативном контейнере(например изменять объект по ключу). Это может привести к так называемому double lookup. То есть двойной поиск по контейнеру.

Возьмем для примера std::map для показательности. И вот такой код:

std::map<std::string, std::string> map;

std::string get_value(const std::string& key) {
if (!map.contains(key)) {
std::string value = longCalculations(key);
map[key] = value;
return value;
} else {
return map[key];
}
}


Здесь мы по ключу key выдаем какое-то значение value. Эти значения вычисляются при помощи долгой функции longCalculations, поэтому было решено закэшировать все уже вычисленные значения в мапе. Так мы сможем обойти долгие вычисления и быстро дать ответ в случае, если уже искали это значение.

Только вот в чем проблема. Поиск по мапе - логарифмическая по времени операция. И в этом примере мы всегда делаем 2 поиска: первый раз на contains(мы должны пройтись по контейнеру, чтобы понять, есть ли элемент) и второй раз на operator[](нужно пройтись по контейнеру, чтобы вставить в него элемент/получить к нему доступ). Долговато как-то. Может можно получше?

В случае, если ключ есть в мапе, то мы можем делать всего 1 поиск! С помощью метода find и итератора на элемент.

std::string get_value(const std::string& key) {
auto it = map.find(key);
if (it == map.end()) {
std::string value = longCalculations(key);
map[key] = value;
return value;
} else {
return it->second;
}
}


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


Методы count и contains нужно использовать только тогда, когда у вас нет надобности в получении доступа к элементам контейнера. Find в этом случае, по моему мнению, немного синтаксически избыточен. А вот говорящие методы - самая тема. Например, у вас есть множество, в котором хранятся какие-то поля джейсона. И вам нужно как-то трансформировать только те значения, ключи которых находятся во множестве.

std::set<std::string> tokens;
std::string json_token;
Json json;
if (tokens.contains(json_token)) {
transformJson(json, json_token);
}


Все прекрасно читается и никакого двойного поиска!

Don't do the same work twice. Stay cool.

#cppcore #goodpractice

Грокаем C++

06 Dec, 09:00


Проверяем вхождение элемента в ассоциативный контейнер

Нужно вот нам по ключу проверить вхождение элемента допустим в мапу.

Обычно мы пишем:

if (map.count(key)) {
// do something
}


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

Такие вот маленькие семантические несостыковочки. С ними вроде все смирились, но осадочек остался...

И 20-е плюсы наконец нам подарили замечательный публичный метод для всех ассоциативных контейнеров contains. Он проверяет, если ли в контейнере элементы с данным ключом. Теперь код выглядит так:

if (map.contains(key)) {
// do something
}


И вот уже стало чуть приятнее и понятнее читать код.

Если есть доступ к 20-м плюсам, то переходите на использование этого метода.

Make things clearer. Stay cool.

#STL #cpp20

Грокаем C++

05 Dec, 09:00


Addressof
#опытным

Говорят вот, что питон - такой легкий для входа в него язык. Его код можно читать, как английские английский текст. А вот С/С++ хаят за его несколько отталкивающую внешность. Чего только указатели стоят...

Кстати о них. Все мы знаем, как получить адрес объекта:

int number = 42;
int * p_num = &number;


Человек, ни разу не видевший код на плюсах, увидит здесь какие-то магические символы. Вроде число, а вроде какие-то руны * и &. Но плюсы тоже могут в читаемость! Причем именно в аспекте адресов.

Вместо непонятного новичкам амперсанда есть функция std::addressof! Она шаблонная, позволяет получить реальный адрес непосредственно самого объекта и доступна с С++11. Для нее кстати удалена перегрузка с const T&&

template< class T >
T* addressof( T& arg ) noexcept;

template< class T >
const T* addressof( const T&& ) = delete;


Это делает функцию еще одним примером использования константной правой ссылки .

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

А вот теперь мы возвращаемся к предыдущему посту про перегрузку оператора взятия адреса. Так как его можно перегружать, то мы можем возвращать вообще любой адрес, который потенциально никак не связан с самим объектом. В этом случае не очень понятно, как взять трушный адрес объекта. Как раз таки std::addressof - способ получить валидный адрес непосредственно самого объекта.

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

А с С++17 она еще и констэкспр, это для любителей компайл-тайма.

Вот вам примерчик:

template<class T>
struct Ptr
{
T* pad; // add pad to show difference between 'this' and 'data'
T* data;
Ptr(T* arg) : pad(nullptr), data(arg)
{
std::cout << "Ctor this = " << this << '\n';
}
 
~Ptr() { delete data; }
T** operator&() { return &data; }
};
 
template<class T>
void f(Ptr<T>* p)
{
std::cout << "Ptr overload called with p = " << p << '\n';
}
 
void f(int** p)
{
std::cout << "int** overload called with p = " << p << '\n';
}
 
int main()
{
Ptr<int> p(new int(42));
f(&p); // calls int** overload
f(std::addressof(p)); // calls Ptr<int>* overload, (= this)
}

// OUTPUT
// Ctor this = 0x7fff59ae6e88
// int** overload called with p = 0x7fff59ae6e90
// Ptr overload called with p = 0x7fff59ae6e88


Здесь какие-то злые персоналии перегрузили оператор взятия адреса у класса Ptr так, чтобы он возвращал указатель на одно из его полей. Ну и потом сравнивают результат работы оператора с результатом выполнения функции std::addressof.

Видно, что трушный адрес объекта, полученный с помощью this и адрес, возвращенный из std::addressof полностью совпадают. А перегруженный оператор возвращает другое значение.

Express your thoughts clearly. Stay cool.

#cpp #cpp11 #cpp17

Грокаем C++

03 Dec, 09:00


​​CamelCase vs Under_Score

Вдохновился прошлым постом и родилось это.

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

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

В настоящее время существует много стандартов наименования переменных, но два из них являются наиболее популярными среди программистов: это camel case («Верблюжья» нотация) и underscore (именование переменных с использованием символа нижнего подчеркивания в качестве разделителя).

Верблюжья нотация является стандартом в языке Java и в его неродственнике JavaScript, хотя ее можно встретить также и в других местах. Согласно этому стандарту, все слова в названии начинаются с прописной буквы, кроме первого. При этом, естественно, не используется никаких разделителей вроде нижнего подчеркивания. Пример: яШоколадныйЗаяцЯЛасковыйМерзавец. Обычно данный стандарт применяют к именам функций и переменных, при этом в именах классов, структур, интерфейсов используется стандарт UpperCamelCase(первая буква заглавная).

В стандарте underscore слова пишутся с маленькой буквы, а между ними стоит _ ( типа такого: йоу_собаки_я_наруто_узумаки). Обычно этот стандарт используется в названиях функций и переменных, а для названий классов, структур, интерфейсов используется стандарт UpperCamelCase. Обычно используется с родственных С языках.

Каждый из этих двух стандартов имеет свои сильные и слабые стороны. Вот основные:

👉🏿 Нижнее подчеркивание лучше читается: сравните стандарт_с_нижним_подчеркиванием и стандартНаписанияПрописнымиБуквами

👉🏿 Зато camel case делает более легким чтение строк, например:
my_first_var=my_second_var-my_third_var
и
myFirstVar=mySecondVar-myThirdVar

Очевидно, что camel case читается лучше: в случае с нижним подчеркиванием и оператором «минус» выражение с первого взгляда вообще можно принять за одну переменную.

👉🏿 Подчеркивание сложнее набирать. Даже при наличии intellisense, во многих случаях необходимо набирать символ нижнего подчеркивания. И имена получаются длиннее.

👉🏿 Camel Case непоследователен, потому что при использовании констант (которые иногда пишутся целиком заглавными буквами) нам приходится использовать нижнее подчеркивание. С другой стороны, стандарт underscore может быть полным, если вы решите использовать в названиях классов (структур, интерфейсов) нижнее подчеркивание в качестве разделителя.

👉🏿 Кроме того, для камел кейса не так уж и просто работать с аббревиатурами, которые обычно представлены в виде заглавных букв. Например, как вот правильно iLoveBDSM или iLoveBdsm?. Непонятно. Можете написать в комментах, как это по-вашему пишется)

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

Расскажите в комментах, какую нотацию вы используете? Интересно иметь большую репрезентативную статистику.

Choose your style. Stay cool.

#fun

Грокаем C++

02 Dec, 10:05


​​Поля_класса
#опытным

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

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

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

Чисто идейно мне нравится такой стайл: открытые члены без подчеркиваний, защищенные с одним подчеркиванием, приватные - с двумя. Спереди или сзада - тут уже вкусовщина.

Но вот беда. У нас проблемы, Хьюстон.

В С++ есть определенные навязанные ограничения на нейминг сущностей со стороны underscore. Стандарт говорит:

Certain sets of names and function signatures 
are always reserved to the implementation:

- Each name that contains a double underscore (__) or
begins with an underscore followed by an uppercase letter
is reserved to the implementation for any use.
- Each name that begins with an underscore is reserved
to the implementation for use as a name in the global namespace.

Such names are also reserved in namespace ::std (17.4.3.1).


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

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

Закрытые поля можно помечать одной черточкой с обеих сторон. Только если решите использовать спереди - не пишите дальше заглавную букву. И будет вам счастье.

В питоне кстати именно через нижнее подчеркивание помечаются "непубличные" части API. Но там правда есть один нюанс. Зацените веселый мем по этой теме. Так что сегодня мем про питухон.

Be happy. Stay cool.

#cppcore

Грокаем C++

02 Dec, 07:01


Хотите создавать идеальные C++ API, которые не ломаются на первой же нагрузке?

👉 Тогда не пропустите этот бесплатный вебинар! 3 декабря в 20:00 мск — открытый урок, который кардинально изменит ваш подход к проектированию API на C++!

**Что вас ждет?**
- Понимание плохого и хорошего API: как отличить чудовищное API от шедевра?
- Умение правильно именовать сущности и разбивать их на атомарные элементы. Прокачаем навыки, чтобы не было «кучи кода» и «головной боли».
- Идеи data-oriented подхода для создания API в высоконагруженных приложениях.

**Кому это будет полезно?**
- Разработчикам, кто только знакомится с C++ или переходит с других языков.
- C++-программистам, которые хотят прокачать свои навыки разработки API.

Вы научитесь проектировать удобный, стабильный и эффективный API для C++, который будет работать как часы.

⭐️ Спикер Андрей Рыжиков — разработчик в НИИ обработки аэрокосмических изображений.

Успейте записаться на открытый урок и получите скидку на большое обучение «C++ Developer».

Для участия зарегистрируйтесь: https://otus.pw/rUbl/

Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru

Грокаем C++

01 Dec, 10:00


Return в main

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

В С++ разрешено не указывать возвращаемое значение для функции main. Но только для нее! Это единственное исключение.

Однако процессу требуется какой-то код возврата по завершению работы, это требование ОС. Поэтому, если исполнение дошло до конца main и ничего плохого не произошло - подразумевается, что возвращается 0. Компилятор также добавляет соответствующую инструкцию в асм.

На счет сишки точно не знаю, но вроде в С99 можно было опускать инструкцию возврата.

#include <iostream>

int main() {
std::cout << "Hello, World!" << std::endl;
}

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

Так что все в порядке, город может спать спокойно без return.

Sleep well. Stay cool.

#cppcore

Грокаем C++

30 Nov, 09:00


​​Инкапсуляция и структуры
#новичкам

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

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

class MyClass
{
    int m_foo;
    int m_bar;
public:
    int addAll() const;
    int getFoo() const;
    void setFoo(int foo);
    int getBar() const;
    void setBar(int bar);
};
int MyClass::addAll() const
{
    return m_foo + m_bar;
}

int MyClass::getFoo() const
{
    return m_foo;
}

void MyClass::setFoo(int foo)
{
    m_foo = foo;
}

int MyClass::getBar() const
{
    return m_bar;
}

void MyClass::setBar(int bar)
{
    m_bar = bar;
}


Выглядит солидно. Сеттеры, геттеры, все дела. Но вот души в этом коде нет. Он какой-то.. Бесполезный чтоли.

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

Этим грешат новички: поначитаются модных концепций(или не очень модных) и давай скрывать все члены.

Brah... Don't do this...

Здесь нет никаких инвариантов, которые нужно было бы сохранять. Интерфейс класса и так позволяет все, шо хош делать с объектом.

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

struct MyClass
{
    int m_foo;
    int m_bar;
}


Делов-то. Зато прошлым примером можно хорошо отчитываться за количество написанных строчек кода=)

Show your inner world to others. Stay cool.

#cppcore #goodpractice #OOP

Грокаем C++

29 Nov, 10:00


​​Результаты ревью

Всем участникам спасибо за развернутые ответы. Самый объемный комментарий с большим количеством правильных предположений относительно кода написал Alex D. Давайте ему все вместе похлопаем👏👏👏.

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

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

Но и на этом поле есть, где развернуться.

Итак, пойдем по в рандомном порядке:

💥 Очередь копируется в воркер потока, поэтому мы обрабатываем не просто копию очереди, а копии!. Каждый поток имеет свою копию. Передача по значению не была бы прям уж сильной проблемой, если бы был один воркер. А их тут 2. И вся работа будет делаться дважды. Это конечно не порядок. Меняем параметр функции на ссылку.

💥 Но если вы думаете, что вы так передадите потоку ссылку - вы ошибаетесь! На самом деле для всех объектов-аргументов потоковой функции во внутреннем сторадже нового потока создается их копия, чтобы не иметь дело с висячими ссылками. Если вы хотите передать истинную ссылку в поток, то надо воспользоваться std::ref.

💥 Нет join'ов у потоков. Надо их либо вызвать, либо использовать jthread из С++20.

💥 Одна из самых больших непоняток в коде для комментаторов - что за переменная lck? На самом деле автор кода немного с ним экспериментировал и просто допустил ошибку, не переименовав lck в mtx. Такое бывает, особенно с новичками. Особенно в проектах без CI.

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

💥 Использование чистых вызовов lock и unlock на мьютексах является очень плохой практикой. В случае изначально пустой очереди или такого распределения расписания потоков, что очередь окажется пуста до входа в while, один из потоков никогда не отпустит лок.
Да и вот это засыпание после execute выглядит очень криво.
Нужно использовать RAII обертки, типа std::lock_guard.

💥 Но в нашем случае lock_guard будет плохим решением. Во-первых, не очень понятно, куда его вставлять. Изначальная проверка на пустоту тоже должна быть "обезопашена". Тогда надо ставить до while. Но в этом случае один из потоков будет выполнять всю работу целиком. А нам хотелось бы использовать преимущества многопоточки.

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

В общем, нехватает гибкости. Хочется иметь возможность отпускать замок на время, а потом опять его захватывать. Но и преимуществами RAII тоже хочется пользоваться.
Выход - std::unique_lock. Он позволяет делать и то, и то.

💥 Так как выполнение задачи теперь не под локом, то sleep в воркере не нужен, другие потоки имеют достаточно времени, чтобы захватить мьютекс.

💥 Неплохо бы обернуть выполнение задачи в блок try-catch. Так если вылетит исключения из выполнения одной задачи, мы можем как-то обработать эту ситуацию и пойти работать дальше, а не просто свалиться в std::terminate.
Пусть этот пример останется "учебным", но теперь хотя бы корректным.

ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ

Fix your flaws. Stay cool.

Грокаем C++

28 Nov, 10:00


Ревью

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

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

Вот собственно код:

struct Task {
void Execute() {
// pretend that this is very long calculations
std::this_thread::sleep_for(std::chrono::seconds(2));
}
};

void WorkingThread(std::deque<Task> queue) {
std::mutex mtx;
mtx.lock();
while (!queue.empty()) {
auto elem = queue.front();
queue.pop_front();
elem.Execute();
lck.unlock();
// to get other threads opportunity to work
std::this_thread::sleep_for(std::chrono::milliseconds(1));
lck.lock();
}
}

int main() {
std::deque<Task> queue(10);
std::thread thr1{WorkingThread, queue}, thr2{WorkingThread, queue};
}


Чего ждем? Айда в комменты поливать г ревьюить код.

Ask for objective critique. Stay cool.

Грокаем C++

27 Nov, 09:00


А вам когда-нибудь снился С++ в кошмарах?)

Грокаем C++

26 Nov, 10:00


​​Могу ли я вызвать функцию main?
#опытным

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

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

В C нет запрета вызывать main(). А все, что не запрещено - разрешено. Вот и вы легко можете вызвать main() из любого места программы.

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

#include <stdio.h>
int main (int argc, char *argv[]) {
printf ("Running main with argc = %d, last = '%s'\n",
argc, argv[argc-1]);
if (argc > 1)
return main(argc - 1, argv);
return 0;
}


Если это запускать, как 'recursive_main 1 2 3', то вывод будет такой:

Running main with argc = 4, last = '3'
Running main with argc = 3, last = '2'
Running main with argc = 2, last = '1'
Running main with argc = 1, last = './recursive_main'


Но вы скорее попадете на переполнение стека от неконтролируемой рекурсии, чем сделаете что-то полезное.

Ну и конечно, в С сложно сделать так, чтобы код выполнялся в глобальной области.

А вот в С++ это сделать суперпросто. В конструкторах глобальных объектов. И вот здесь уже интересно. Инициализация глобальных переменных имеет свой определенный порядок. Что будет, если мы в этот порядок вклинимся и запустим программу раньше?

Такое чувство, что ничего хорошего мы не получим.

В случае с рекурсией любой код теоретически может уйти в переполнение, поэтому как будто и не особо важно, main это или нет. Не запрещать же рекурсию из-за возможности ее никогда не остановить.

Но вот если мы можем прервать подготовку программы к вызову main ее преждевременным вызовом - у нас 100% возникнут проблемы.

Скорее всего это одна из мажорных причин, почему в С++ нельзя вызывать main никаким образом. Если происходит обратное, то программа считается ill-formed.

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

В общем, не думаю, что у вас было желание когда-то вызвать main. Однако сейчас вы точно знаете, что так делать нельзя)

Follow the rules. Stay cool.

#cppcore #goodoldc

Грокаем C++

26 Nov, 07:00


👍Как применить принципы ООП в языке С для создания сложных программ?

Узнайте на бесплатном уроке онлайн-курса «Программист С» — «Язык Cи и ООП: пошаговая разработка видеоплеера»: регистрация

Разберем практический пример разработки видеоплеера с использованием объектно-ориентированного подхода:
- узнаете особенности реализации ООП в языке С
- рассмотрим проектирование архитектуры видеоплеера: от интерфейсов к реализации
- проведем практику: пошаговая разработка основных компонентов плеера.

Также будет обработка ошибок и управление памятью в объектно-ориентированном стиле; live-coding: демонстрация работы с форматами видео и аудио в С.

В результате получите:
- готовый прототип видеоплеера на С с использованием ООП подходов и практические навыки применения паттернов проектирования в Си.

🔥После вебинара вы сможете продолжить обучение на курсе по спеццене, в том числе, в рассрочку.

Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576

Грокаем C++

25 Nov, 08:00


​​Еще один способ залочить много мьютексов

Последний пост из серии.

Вот у мьютекса есть метод lock, который его захватывает. А разработчики stdlib взяли и сделали функцию std::lock, которая лочит сразу несколько замков.

Также у мьютекса есть метод try_lock, который пытается в неблокирующем режиме его захватить. "Авось да получится". И видимо по аналогии с lock в стандартной библиотеке существует свободная функция std::try_lock, которая пытается захватить несколько мьютексов так же в неблокирующем режиме.

template< class Lockable1, class Lockable2, class... LockableN >  
int try_lock( Lockable1& lock1, Lockable2& lock2, LockableN&... lockn );


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

Чтобы с помощью std::try_lock наверняка захватить все замки, нужно крутиться в горячем цикле и постоянно вызывать std::try_lock, пока она не вернет -1.

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

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

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

Stay useful. Stay cool.

#concurrency

Грокаем C++

23 Nov, 09:39


Почему не используют стратегию блокировки по адресам?
#опытным

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

Начнем с того, что локать один мьютекс - это норма. Все так делают, никто от этого не помер.

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

А именно это и происходит при вызове метода lock(). Поток мытается захватить мьютекс и если не получается - блокируется до того момента, пока мьютекс не освободится.

Поэтому любая схема с последовательным вызовом методов lock() будет подвержена дедлокам.

А в схеме с упорядоченным по адресам блокировкой именно так и происходит.

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

А что если в одном потоке замки будут лочиться через scoped_lock по адресной схеме, а в другом потоке - одиночно?

            1 поток            |            2 поток              |
-------------------------------|---------------------------------|
lock(mutex2) // УСПЕШНО | |
| scoped_lock() |
| lock(mutex1) // УСПЕШНО |
| lock(mutex2) // ОЖИДАНИЕ ... |
lock(mutex1) // ОЖИДАНИЕ... | |


В этом случае настанет дедлок. Спасибо за пример Сергею Борисову.

Ну или другую ситуацию рассмотрим: есть 4 замка l1, l2, l3, l4. Поток захватил замок с самым большим адресом l4 и надолго(потенциально навсегда) заблокировался.
Но другие треды продолжают нормально работать. И они иногда захватывают пары мьютексов. Все продолжается нормально, пока один из потоков не пытается залочить l3 и l4. Из-за ордеринга захватится l3, а дальше поток будет ждать освобождения l4 aka заблокируется. Дальше другой поток будет пытаться захватить l2 и l3. Он захватит l2 и будет дожидаться l3.

Логику можно продолжать и дальше. Таким образом из-за одного мьютекса и немного поломанного потока может остановиться вся программа.

std::mutex l1, l2, l3, l4;
// Пусть я как-то гарантирую, что они пронумерованы в порядке
возрастания адресов и std::scoped_lock(sc_lock для краткости)
работает с помощью сортировки по адресам

1 поток | 2 поток | 3 поток | 4 поток
-------------|----------------|----------------|----------------
l4.lock(); | | |
//blocks here| | |
|sc_lock(l3, l4);| |
| // lock l3 | |
| // blocks on l4| |
|sc_lock(l2, l3);|
| // lock l2 |
| // blocks on l3|
| | sc_lock(l1, l2);
| // lock l1
| // blocks on l2


Примеры немного преувеличены, но тем не менее они говорят о том, что схема с адресами не совсем безопасна.

Так может тогда вообще не будем блокироваться при уже захваченном мьютексе? Именно это и делают в реализации стандартной библиотеки. Первый захват мьютекса происходит через обычный lock(), а остальные мьютексы пытаются заблокировать через try_lock. Можно сказать, что это lock-free взятие замка. Если мьютекс можно захватить - захватываем, если нет, то не блокируемся и дальше продолжаем исполнение. Так вот в случае, если хотя бы один try_lock для оставшихся замков вернул false, то реализация освобождает все захваченные замки и начинает попытку снова.

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

ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ

#concurrency

Грокаем C++

21 Nov, 08:00


Цензура ЛЮБОЙ нейронки разлетается в щепки только от ОДНОГО промпта.

После ввода — никаких запретов. Генерим хоть самый жесткий контент.

Мегапромпт лежит здесь.

Грокаем C++

20 Nov, 09:28


Спасибо всем, кто поделился своими лав-стори в комментах.
Такие разные истории, но в то же время одинаковые.
И во всех них сквозит одна мысль: "Двигайся по зову своего внутреннего огня. Он приведет тебя к цели"
Если что-то не нравится, никогда не поздно это поменять. Если что-то нравится, делай это и жизнь будет радостней. И это касается не только программирования естественно.

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

Be passionate. Stay cool.

Грокаем C++

19 Nov, 09:00


А как вы пришли к программированию на плюсах?
Как это изменило вашу жизнь?

Грокаем C++

18 Nov, 09:00


std::lock
#опытным

Сейчас уже более менее опытные разрабы знают про std::scoped_lock и как он решает проблему блокировки множества мьютексов. Однако и в более старом стандарте С++11 есть средство, позволяющее решать ту же самую проблему. Более того std::scoped_lock - это всего лишь более удобная обертка над этим средством. Итак, std::lock.

Эта функция блокирует 2 и больше объектов, чьи типы удовлетворяют требованию Locable. То есть для них определены методы lock(), try_lock() и unlock() с соответствующей семантикой.

Причем порядок, в котором блокируются объекты - не определен. В стандарте сказано, что объекты блокируются с помощью неопределенной серии вызовов методов lock(), try_lock и unlock(). Однако гарантируется, что эта серия вызовов не может привести к дедлоку. Собстна, для этого все и затевалось.

Штука эта полезная, но не очень удобная. Сами посудите. Эта функция просто блокирует объекты, но не отпускает их. И это в эпоху RAII. Ай-ай-ай.

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

struct SomeSharedResource {
void swap(SomeSharedResource& obj) {
{
// !!!
std::unique_lock<std::mutex> lk_c1(mtx, std::defer_lock);
std::unique_lock<std::mutex> lk_c2(obj.mtx, std::defer_lock);
std::lock(mtx, obj.mtx);
// handle swap
}
}
std::mutex mtx;
};

int main() {
SomeSharedResource resource1;
SomeSharedResource resource2;
std::mutex m2;
std::thread t1([&resource1, &resource2] {
resource1.swap(resource2);
std::cout << "1 Do some work" << std::endl;
});
std::thread t2([&resource1, &resource2] {
resource2.swap(resource1);
std::cout << "2 Do some work" << std::endl;
});

t1.join();
t2.join();
}


Раз мы все-таки за безопасность и полезные практики, то нам приходится использовать std::unique_lock'и на мьютексах. Только нужно передать туда параметр std::defer_lock, который говорит, что не нужно локать замки при создании unique_lock'а, его залочит кто-то другой. Тем самым мы убиваем 2-х зайцев: и RAII используем для автоматического освобождения мьютексов, и перекладываем ответственность за блокировку замков на std::lock.

Можно использовать и более простую обертку, типа std::lock_guard:
struct SomeSharedResource {
void swap(SomeSharedResource& obj) {
{
// !!!
std::lock(mtx, obj.mtx);
std::lock_guard<std::mutex> lk_c1(mtx, std::adopt_lock);
std::lock_guard<std::mutex> lk_c2(obj.mtx, std::adopt_lock);
// handle swap
}
}
std::mutex mtx;
};


Здесь мы тоже используем непопулярный конструктор std::lock_guard: передаем в него параметр std::adopt_lock, который говорит о том, что мьютекс уже захвачен и его не нужно локать в конструкторе lock_guard.

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

Использование unique_lock может быть оправдано соседством с условной переменной, но если вам доступен C++17, то естественно лучше использовать std::scoped_lock.

Use modern things. Stay cool.

#cpp11 #cpp17 #concurrency

Грокаем C++

17 Nov, 11:00


​​Локаем много мьютексов
#опытным

Cтандартное решение этой проблемы дедлока из постов выше - лочить замки в одном и том же порядке во всех потоках. Но как это сделать? Они не же на физре, "по порядку рассчитайсьььь" не делали.

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

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

std::scoped_lock был введен в С++17 и представляет собой RAII обертку над локом множества мьютексов. Можно сказать, что это std::lock_guard на максималках. То есть буквально, это обертка, которая лочит любое количество мьютексов от 0 до "сами проверьте верхнюю границу".

Но есть один важный нюанс. Никак не гарантируется порядок, в котором будут блокироваться замки. Гарантируется лишь то, что выбранный порядок не будет приводить к dead-lock'у.

Пример из прошлого поста может выглядеть теперь вот так:

struct SomeSharedResource {
void swap(SomeSharedResource& obj) {
{
std::scoped_lock lg{mtx, obj.mtx};
// handle swap
}
}
std::mutex mtx;
};

int main() {
SomeSharedResource resource1;
SomeSharedResource resource2;
std::mutex m2;
std::thread t1([&resource1, &resource2] {
resource1.swap(resource2);
std::cout << "1 Do some work" << std::endl;
});
std::thread t2([&resource1, &resource2] {
resource2.swap(resource1);
std::cout << "2 Do some work" << std::endl;
});

t1.join();
t2.join();
}


И все. И никакого дедлока.

Однако немногое лишь знают, что std::scoped_lock - это не только RAII-обертка. Это еще и более удобная обертка над "старой" функцией из С++11 std::lock.

О ней мы поговорим в следующий раз. Ведь не всем доступны самые современные стандарты. Да и легаси код всегда есть.

Be comfortable to work with. Stay cool.

#cpp17 #cpp11 #concurrency

Грокаем C++

15 Nov, 11:00


​​Зачем локать мьютексы в разном порядке
#новичкам

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

Дело в том, что иногда это не совсем очевидно. Точнее почти всегда это не очевидно.

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

Разберем чуть более сложный пример:

struct SomeSharedResource {
void swap(SomeSharedResource& obj) {
{
std::lock_guard lg{mtx};
// just for results reproducing
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::lock_guard lg1{obj.mtx};
// handle swap
}
}
std::mutex mtx;
};

int main() {
SomeSharedResource resource1;
SomeSharedResource resource2;
std::mutex m2;
std::thread t1([&resource1, &resource2] {
resource1.swap(resource2);
std::cout << "1 Do some work" << std::endl;
});
std::thread t2([&resource1, &resource2] {
resource2.swap(resource1);
std::cout << "2 Do some work" << std::endl;
});

t1.join();
t2.join();
}


Все просто. У нас есть пара объектов, которые используются в качестве разделяемых ресурсов (их могут изменять более 1 потока). Объекты могут обмениваться между собой данными.

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

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

И это все еще может показаться детским садом и очевидушкой. Только вот сам момент взятия замка уже не выглядит так подозрительно. А сами объекты могут лежать в каких-то страшных структурах данных и могут быть разбросаны по коду. В таком случае все не так очевидно становится.

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

Don't be obvious. Stay cool.

#concurrency

Грокаем C++

15 Nov, 07:01


🚀 Приглашаем на бесплатный вебинар по C++! 🚀

Дата: 19 ноября 2024 года
Время: 20:00
Тема: Как протестировать C++ код и оценить степень собственной лени

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

На занятии:
1. Научимся подключать фреймворки тестирования к своему проекту при помощи CMake.
2. Напишем готовые к запуску тесты.
3. Поговорим о том, как измерить покрытие тестами кода, какие инструменты для этого существуют.

В результате:
1. Придем к тому, что вместе с кодом стоит сразу писать и юнит-тесты к нему.
2. В будущем с легкостью интегрируем любой фреймворк тестирования в свой проект.
3. Научимся не только писать тесты, но и мерить их покрытие.

👉🏻О курсе "Специализация C++ Developer" на Otus:
Длительность курса: 10 месяцев.
Формат: Онлайн.

Программа курса:
· Введение в язык C++: основы синтаксиса, структура программ, базовые конструкции.
· Классы и структуры: ООП, наследование, полиморфизм, шаблоны.
· Основы unit-тестирования: подключение фреймворков, написание тестов, измерение покрытия.
· Стандартная библиотека и полезные алгоритмы: контейнеры, ввод-вывод, алгоритмы.

📌Скидка 15%: действует до 17 ноября!

Не упустите шанс стать профессионалом в C++! Присоединяйтесь к вебинару и узнайте больше о курсе.
🔗 Регистрация на вебинар

Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru

Грокаем C++

13 Nov, 11:00


Дедлокаем код
#новичкам

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

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

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

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

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

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

Самый простой пример, который иллюстирует эту ситуацию:

std::mutex m1;
std::mutex m2;

std::thread t1([&m1, &m2] {
std::cout << "Thread 1. Acquiring m1." << std::endl;
m1.lock();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::cout << "Thread 1. Acquiring m2." << std::endl;
m2.lock();
std::cout << "Thread 1 perform some work" << std::endl;
});

std::thread t2([&m1, &m2] {
std::cout << "Thread 2. Acquiring m2." << std::endl;
m2.lock();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::cout << "Thread 2. Acquiring m1." << std::endl;
m1.lock();
std::cout << "Thread 2 perform some work" << std::endl;
});

t1.join();
t2.join();


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

Thread 1. Acquiring m1.
Thread 2. Acquiring m2.
Thread 2. Acquiring m1.
Thread 1. Acquiring m2.


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

Do useful work. Stay cool.

#concurrency

Грокаем C++

13 Nov, 08:00


Получите оффер Software Engineer всего за 3 дня! 🚀

Вендор и производитель IT-инфраструктуры YADRO приглашает Software Engineer на SPRINT OFFER.

Команда KVADRA OS ждёт кандидатов сразу на два направления: Linux-based (от уровня junior) и Android (уровня middle и senior).

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

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

Читайте подробности на сайте, оставляйте заявку до 24 ноября и присоединяйтесь к команде KVADRA OSпо ссылке.

Грокаем C++

12 Nov, 12:04


Возвращаем ссылку в std::optional
#новичкам

В прошлом посте я упоминал, что методы front и back последовательных контейнеров играют в ящик, если их пытаться вызвать на пустом контейнере. Это приводит к UB.

Один из довольно известных приемов для обработки таких ситуаций - возвращать не ссылку на объект, а std::optional. Это такая фича С++17 и класс, который может хранить или не хранить объект.

Теперь, если контейнер пустой - можно возвращать std::nullopt, который создает std::optional без объекта внутри. Если в нем есть элементы, то возвращать ссылку.

Только вот проблема: std::optional нельзя создавать с ссылочным типом. А копировать объект ну вот никак не хочется. А если он очень тяжелый? Мы программисты и тяжести поднимать не любим.

И вроде бы ситуация безвыходная. Но нет! Решение есть!

Можно возвращать std::optional<std::reference_wrapper<T>>. std::reference_wrapper - это такая обертка над ссылками, чтобы они вели себя как кошерные объекты со своими блэкджеком, конструкторами, деструкторами и прочими прелестями.

Это абсолютно легально и теперь у вас никакого копирования нет!

std::optional<std::reference_wrapper<T>> Container::front() {
if (data_.empty()) {
return std::nullopt;
}
return std::ref(data_[0]);
}


И в добавок есть нормальная безопасная проверка.

Пользуйтесь.

Stay safe. Stay cool.

#cpp17 #STL #goodpractice

Грокаем C++

11 Nov, 09:00


А что вы думаете по поводу вектора и скорости развития С++?
И почему у нас до сих пор нет стандартного тредпула??

Грокаем C++

09 Nov, 12:01


​​Виртуальная дружественная функция
#новичкам

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

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

Рассмотрим банальный пример - сериализация объекта в поток. Обычно для такой задачи используют дружественный оператор <<. Но вот хотелось бы сериализовать в зависимости от динамического типа. Фигня вопрос.

struct Person {
Person(const std::string &first_name,
const std::string &last_name) : first_name_{first_name},
last_name_{last_name} {}

friend std::ostream& operator<<(std::ostream& o, const Person& b) {
return o << b.to_str();
}
protected:
virtual std::string to_str() const{
return first_name_ + " " + last_name_;
}
private:
std::string first_name_;
std::string last_name_;
};

struct TaxPayer : Person {
TaxPayer(const std::string first_name,
const std::string last_name,
const std::string itn) : Person{first_name, last_name}, itn_{itn} {}
protected:
virtual std::string to_str() const{
return Person::to_str() + " " + itn_;
}
private:
std::string itn_;
};

int main() {
auto prs1 = std::make_unique<Person>("Ter", "Minator");
auto prs2 = std::make_unique<TaxPayer>("Ace", "Ventura", "0000");
std::cout << *prs1 << std::endl;
std::cout << *prs2 << std::endl;
}
// OUTPUT:
// Ter Minator
// Ace Ventura 0000


Есть у нас класс человека с его именем и фамилией. Есть класс налогоплательщика, который наследуется от человека и добавляет к полям ИНН. Допустим, мы хотим как-то выводить на консоль содержимое этих людей(в смысле поля; расчленением мы не занимаемся, не в Питере живем все-таки). Можно конечно в каждом классе определять свой оператор или выводить на консоль результат работы метода to_str, но это лишний апи и лишний код.

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

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

Have a real friends. Stay cool.

#cppcore

Грокаем C++

08 Nov, 09:00


​​Может ли дружественная функция быть виртуальной?
#опытным

Еще один вопрос из серии "а могут ли рыбы быть рыжими?" - "Да вроде как по цвету могут, но рыжие это обычно про волосы, а волос у рыб нет. Да и вообще, кому это знание нужно?!". Что-то типа такого. Но раз такие вопросы задают, то пора в ваше кунг-фу добавить и этот приемчик.

Давайте начнем с начала.

Дружественная функция - обычная функция, которая имеет доступ к полям класса-друга. И все. Никаких других особенностей у нее нет. И ограничений тоже никаких нет. Ее сигнатура может быть любой. Объект класса-друга может в ней вообще не фигурировать.

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

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

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

Теперь сможете смело парировать интервьюеров четким ударом в глазницу быстрым точным ответом

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

Have a real friends. Stay cool.

#interview #cppcore

Грокаем C++

08 Nov, 08:00


Приглашаем на митап для бэкенд-разработчиков от Еком-сервисов Яндекса
В Минск приехал Яндекс Foodtech Tour — серия митапов в столицах, на которых эксперты Еды, Лавки и Маркета рассказывают о внутренней кухне разработки сервисов. В Минске спикеры расскажут о core-технологиях, которые лежат в основе работы продуктов. Митап пройдет 7 декабря. 

Программа насыщенная: 
👉Доклады о BDUI и ускорении разработки. Никита Шумский из Еды расскажет об особенной инфраструктуры Еды, различиях классического и мобильного бэкенда и преимуществах BDUI. Ваня Ходор из Лавки поделится кейсом ускорения разработки, причем не скорости работы кода, а его написания.
👉CaseLab о мультизаказе в Еде. Это интерактивный формат, в котором участники разбирают реальный кейс из работы сервиса, предлагают решение и получают фидбек от экспертов Яндекса.
👉Нетворкинг и afterparty

Будет интересно — зовите друзей и регистрируйтесь! 
Количество мест ограничено. Дождитесь подтверждения заявки.

Грокаем C++

05 Nov, 09:00


​​Различаем преинкремент и постинкремент
#новичкам

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

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

Преинкремент - увеличивает значение на единицу и возвращает ссылку на уже увеличенное значение. Синтаксис использования такой: ++value.

Постинкремент - делает копию исходного числа, увеличивает на единицу оригинал, и возвращает копию по значению. Синтаксис использования такой: value++.

Обычно внешний вид перегрузки операторов внутри описания класса такой:

возвращаемое_значение operator{символы_вызова_оператора}(аргументы).

Например, оператор копирования вызывается с помощью оператора присваивания(=). Поэтому подставляем в шаблон на место возвращаемое_значение ссылку на объект, на место символы_вызова_оператора подставляем =, на место аргументов - const Type&. Получается так:

Type& operator=(const Type&);


Но теперь возникает вопрос. С++ не различает функции, у которых то же имя, то же те же аргументы, но разные возвращаемые значения. У нас как раз такая ситуация: названия одинаковые(operator++), возвращаемые значения разные(в одном случае возвращаем по ссылке, во втором - по значению) и одинаково отсутствуют аргументы. Если бы мы определили операторы так:

Type& operator++() {
value_ += 1;
return *this;
}

Type operator++() {
auto tmp_value = *this;
value_ += 1;
return tmp_value;
}


То была бы примерно такая ошибка компиляции: functions that differ only in their return type cannot be overloaded.

Что же делать?

Примерно таким же вопросом задался Страуструп и сделал ход конем(правда конь ходил на костылях). Он связал префиксный оператор с человеческой формой объявления, а постфиксный - с вот такой:

Type operator++(int) {
auto tmp_value = *this;
value_ += 1;
return tmp_value;
}


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

Если видит префикс - выбирает оператор без аргумента, видит постфикс - выбирает с аргументом.

Вопрос, почему тип аргумента именно int - остается открытым. Наверное так было проще.

Итого, так перегружаются операторы инкремента:

struct Type {
Type& operator++() {
value_ += 1;
return *this;
}

Type operator++(int) {
auto tmp_value = *this;
value_ += 1;
return tmp_value;
}
private:
IntegerLikeType value_;
};


Think of your decisions twice. Stay cool.

#cppcore

Грокаем C++

04 Nov, 09:00


Принтуем уникальный указатель
#новичкам

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

И мы понимаем, что это обертка, когда нам нужно достать сам сырой указатель и вызвать метод get().

Один из таких случаев - вывод указателя в поток. Если мы захотим вывести на консоль адрес объекта, то нам нужно будет вызывать метод get.

auto u_ptr = std::make_unique<Type>();
std::cout << u_ptr.get() << std::endl;


Но это же странно!

Да, это скорее нужно в каких-то отладочно-логировачных целях и ничего более.

Но нужно же!

Че им стоило перегрузить оператор<<, чтобы мы могли непосредственно сам объект уникального указателя сериализовывать?

Видимо больших затрат. Но ребята поднатужились и справились. В С++20 мы можем выводить сам объект без метода get()!

auto u_ptr = std::make_unique<Type>();
std::cout << u_ptr << std::endl;


Ну и конечно в этом случае на консоли появится адрес объекта.

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

Make small changes. Stay cool.

#cpp20 #cppcore

Грокаем C++

03 Nov, 09:00


​​Вызываем функцию в глобальном скоупе

В отличие от С в С++ можно вызывать код до входа в main. В С нельзя вызывать функции в глобальном скоупе, и глобальные переменные там должны быть инициализированы константным выражение. Однако для С++ пришлось ослабить правила. Это было нужно для возможности создания объектов кастомных классов, как глобальных переменных. Для создания объектов нужны конструкторы. А это обычные функции.

Поэтому в плюсах можно выполнять пользовательский код до main. Но этот код должен содержаться внутри конструкторов и вызываемых ими функциях.

Но просто так вызвать рандомную функция я не могу. Это запрещено.

"Ну блин. Мне очень надо. Может как-то договоримся?"

Со стандартом не договоришься. Но обходные пути все же можно найти.

Например так:

static int dummy = (some_function(), 0);

int main() {}


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

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

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

Have unique tools in your arsenal. Stay cool.

#cppcore #goodoldc

Грокаем C++

01 Nov, 15:00


​​Что происходит до main?

Рассмотрим простую программу:

#include <iostream>
#include <random>

int a;
int b;

int main() {
a = rand();
b = rand();
std::cout << (a + b);
}


Все очень просто. Объявляем две глобальные переменные, в main() присваиваем им значения и выводим их сумму на экран.

Скомпилировав эту программу, мы сможем посмотреть ее ассемблер и увидеть просто набор меток, соответствующих разным сущностям кода(переменным a и b, функции main). Но вы не увидите какого-то "скрипта". Типа как в питоне. Если питонячий код не оборачивать в функции, то мы точно будем знать, что выполнение будет идти сверху вниз. Так вот, такой простыни ассемблера вы не увидите. Код будет организован так, как будто бы им кто-то будет пользоваться.

И это действительно так! Убирая сложные детали, можем увидеть вот такое:

a:
.zero 4

b:
.zero 4

main:

push rbp
mov rbp, rsp
call rand
...
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov eax, 0
pop rbp
ret


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

Но даже до того, как начнет работу сущность, которая вызывает main, нужно проделать большую работу по подготовке программы к исполнению. Давайте просто перечислю, что должно быть сделано:

💥 Программа загружается в оперативную память.

💥 Аллокация памяти для стека. Для исполнения функций и хранения локальных переменных обязательно нужен стек.

💥 Аллокация памяти для кучи. Для программы нужна дополнительная память, которую она берет из кучи.

💥 Инициализация регистров. Там их большое множество. Например, нужно установить текущий указатель на вершину стека(stack pointer), указатель на инструкции(instruction pointer) и тд.

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

💥 Положить на стек аргументы argc, argv(мб envp). Это аргументы для функции main.

💥 Загрузка динамических библиотек. Программа всегда линкуется с разными динамическими либами, даже если вы этого явно не делаете)

💥 Вызов всякий преинициализирующих функций.

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

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

Она уже выполняет более прикладные чтоли вещи:

👉🏿 Статическая инициализация глобальных переменных. Это и недавно обсуждаемая zero-инициализация и константная инициализация(когда объект инициализирован константным выражением). То есть инициализируется все, что можно было узнать на этапе компиляции.

👉🏿 Динамическая инициализация глобальных объектов. Выполняется код конструкторов глобальных объектов.

👉🏿 Инициализация стандартного ввода-вывода. Об этом мы говорили тут.

👉🏿 Инициализация еще бог знает чего. Начальное состояние рандомайзера, malloc'а и прочего. Так-то это часть первых шагов, но привожу отдельно, чтобы вы не думали, что только ваши глобальные переменные инициализируются.

И только вот после этого всего, когда состояние программы приведено в соответствие с ожиданиями стандарта С++, функция _start вызывает main.

Так что, чтобы вы смогли выполнить свою программу, кому-то нужно очень мощно поднапрячься...

See what's underneath. Stay cool.

#OS #compiler

Грокаем C++

31 Oct, 09:00


Ответ

Самый главный результат опроса - почти треть канала состоит из красивых людей. In mom's humble opinion. И это прекрасно! И настоящие профессионалы, и внешне обаятельны, и в душе поэты!

Но ладно, это была лирика(поэт из меня так себе). Перейдем к правильному ответу.

Он был замаскирован, видимо поэтому набрал меньше всего голосов.

Стандарт говорит:

Because the explicit template argument list follows the function template name, 
and because constructor templates are named without using a function name,
there is no way to provide an explicit template argument list for these function templates.


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

Но так как в нашем случае конструктор не принимает никаких параметров - компилятор никак не сможет вывести типы.

Поэтому невозможно вызвать конструктор у такого класса:

struct Type {
template <typename>
Type() {}
};


Но! Объект такого класса создать можно.

Некоторые функции в С++ неявно создают объекты. Например std::bit_cast.

struct Type {
template <typename>
Type() {};
};

struct Other {};

int main() {
Type t = std::bit_cast<Type>(Other{});
}


Спасибо @cppnyasha за пример.

Но конкретно в нашем случае была задача вызвать конструктор, а это невозможно.

Solve problems. Stay cool.

#cppcore #cpp20

Грокаем C++

30 Oct, 10:00


Квиз

Вчера в комментах наш подписчик @d7d1cd задал очень интересную задачку, которой мне захотелось с вами поделиться. Да, кто-то уже ее обсудил, но тем, кто не участвовал в дискуссии, тоже будет интересно проверить свои знания в #quiz.

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

Подписчику спасибо за контент, а у меня для вас всего один вопрос. Как правильно вызвать конструктор у такого класса?

struct Type {
template <typename>
Type() {}
};


Challenge your knowledge. Stay cool.

Грокаем C++

29 Oct, 09:00


А за сколько вы выучили С++?

Грокаем C++

28 Oct, 08:10


​​Линкуем массивы к объектам

Опытные читатели могли заметить кое-что странное в этом посте. И заметили кстати. Изначально cin, cout и тд определены, как простые массивы. А в iostream они уже становятся объектами потоков и линкуются как онные. То есть в одной единице трансляции

extern std::ostream cout;
extern std::istream cin;
...


А в другой

 // Standard stream objects.
// NB: Iff <iostream> is included, these definitions become wonky.
typedef char fake_istream[sizeof(istream)]
attribute ((aligned(alignof(istream))));
typedef char fake_ostream[sizeof(ostream)]
attribute ((aligned(alignof(ostream))));
fake_istream cin;
fake_ostream cout;
fake_ostream cerr;
fake_ostream clog;


Что за приколы такие? Почему массивы нормально линкуются на объекты кастомных классов?

В С++ кстати запрещены такие фокусы. Типы объявления и определения сущности должны совпадать.

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

// header.hpp
#pragma once

struct TwoFields {
int a;
int b;
};

struct ThreeFields {
char a;
int b;
long long c;
};

// source.cpp

ThreeFields test = {1, 2, 3};

// main.cpp

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

extern TwoFields test;

int main() {
std::cout << test.a << " " << test.b << std::endl;
}


На консоли появится "1 2". Но ни типы, ни размеры типов, ни выравнивания у объектов из объявления и определения не совпадают. Поэтому здесь явное UB.

Но в исходниках GCC так удачно сложилось, что массивы реально представляют собой идеальные сосуды для объектов io-потоков. На них даже сконструировали реальные объекты. Поэтому такие массивы можно интерпретировать как сами объекты.

Это, естественно, все непереносимо. Но поговорка "спички детям - не игрушка" подходит только для тех, кто плохо понимает, что делает. А разработчики компилятора явно не из этих ребят.

Take conscious risks. Stay cool.

#cppcore #compiler

Грокаем C++

28 Oct, 06:10


Я айтишник и я устал!

С годами работы в IT все сильнее напрягает рутина, прокрастинация, куча задач и 0 желания их выполнять. Че делать?

Хватит грызть самого себя и заставлять через силу - сделаешь только хуже!

Лучше подпишись на того, кто уже не первый год работает с IT-специалистами и помогает им справиться с апатией и прокрастинацией - Психолог с научным подходом.

✔️ Как оторваться от ленты соцсетей и сесть за работу с удовольствием?
✔️ Как спокойно общаться с коллегами, если они бесят?
✔️ Как избавиться от постоянной тревожности?
✔️ Как успокоить конфликты в семье и перестать срываться на всех, а вместо этого получить поддержку и понимание со стороны близких?

Подписывайся на канал @remizov_changes - начни работать и жить в кайф, не скатываясь в кризисы и выгорание!

А в закрепе тебя уже ждут бонусы:
👨🏻‍💻 Видео, в котором ты найдёшь ответ на вопрос «Почему у тебя нет энергии и что с этим делать» + гайд как it-специалисту вернуть энергию, даже если не получается отдохнуть.

https://t.me/+srQ8N42LB-42ODFi

Грокаем C++

27 Oct, 15:00


​​std::cout

Кажется, что на начальном этапе становления про-с++-ером, вывод в использование конструкции:

std::cout << "Print something in consol\n";


воспринимается, как "штука, которая выводит текст на консоль".

Даже со временем картинка не до конца складывается и на вопрос "что такое std::cout?", многие плывут. Сегодня закроем этот вопрос.

В этой строчке мы вызываем такой оператор:

std::ostream& operator<< (std::ostream& stream, const char * str)


Получается, что std::cout - объект класса std::ostream. И ни какой-то там временный. Раз он принимается по левой ссылке, значит он уже где-то хранится в памяти.

Но мы же ничего не делаем для его создания? Откуда он взялся?

Мы говорили о том, что есть "невидимые" для нас вещи, которые происходят при старте программы. Так вот, это одна из таких вещей.

std::cout - глобальный объект типа std::ostream. За его создание отвечает класс std::ios_base::Init, инстанс которого явно или неявно определяется в библиотеке <iostream>.

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

Полазаем по исходникам gcc. Ссылочки кликабельные для пытливых умов.

А в хэдэре iostream мы можем найти вот это:

extern istream cin;  ///< Linked to standard input
extern ostream cout; ///< Linked to standard output
extern ostream cerr; ///< Linked to standard error (unbuffered)
extern ostream clog; ///< Linked to standard error (buffered)
...
static ios_base::Init __ioinit;


Здесь определяются символы стандартных потоков и создается глобальная переменная класса ios_base::Init. Пойдемте тогда в конструктор:

ios_base::Init::Init()
{
if (__gnu_cxx::__exchange_and_add_dispatch(&_S_refcount, 1) == 0)
{
// Standard streams default to synced with "C" operations.
_S_synced_with_stdio = true;

new (&buf_cout_sync) stdio_sync_filebuf<char>(stdout);
new (&buf_cin_sync) stdio_sync_filebuf<char>(stdin);
new (&buf_cerr_sync) stdio_sync_filebuf<char>(stderr);

// The standard streams are constructed once only and never
// destroyed.
new (&cout) ostream(&buf_cout_sync);
new (&cin) istream(&buf_cin_sync);
new (&cerr) ostream(&buf_cerr_sync);
new (&clog) ostream(&buf_cerr_sync);
cin.tie(&cout);
cerr.setf(ios_base::unitbuf);
// _GLIBCXX_RESOLVE_LIB_DEFECTS
// 455. cerr::tie() and wcerr::tie() are overspecified.
cerr.tie(&cout);
...
__gnu_cxx::__atomic_add_dispatch(&_S_refcount, 1);


Немножко разберем происходящее.

В условии проверяется ref_count, чтобы предотвратить повторную инициализацию. Так как не предполагается, что такие объекты, как cout будут удалены, они просто создаются через placement new с помощью инстансов stdio_sync_filebuf<char>. Это внутренний буфер для объектов потоков, который ассоциирован с "файлами" stdout, stdin, stderr. Буферы как раз и предназначены для получения/записи io данных.

Хорошо. Мы видим как и где создаются объекты. Но это же placement new. Для объектов уже должная быть подготовлена память для их размещения. Где же она?

В файлике globals_io.cc:

 // Standard stream objects.
// NB: Iff <iostream> is included, these definitions become wonky.
typedef char fake_istream[sizeof(istream)]
attribute ((aligned(alignof(istream))));
typedef char fake_ostream[sizeof(ostream)]
attribute ((aligned(alignof(ostream))));
fake_istream cin;
fake_ostream cout;
fake_ostream cerr;
fake_ostream clog;


то есть, объекты - это пустые символьные массивы правильного размера и выравнивания.

Все это должно вам дать довольно полное представление, что такое стандартные потоки ввода-вывода.

#cppcore #compiler

Грокаем C++

26 Oct, 10:06


​​Программа без main?

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

Без функции main программа просто не запустится.

Или нет?

Может быть мы можем что-нибудь нахимичить, чтобы, например, написать Hello, World без main?

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

Скомпилируем вот такую программу под gcc с флагом -nostartfiles:

#include <iostream>

int my_fun();

void _start()
{
int x = my_fun();
exit(x);
}

int my_fun()
{
std::cout << "Hello, World!\n";

return 0;
}


И на консоли появится наша горячо-любимая надпись: Hello, World!

Для любителей поиграться с кодом вот вам ссылочка на годболт.

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

Make impossible things. Stay cool.

#fun #cppcore

Грокаем C++

24 Oct, 17:12


​​Почему тогда локальные переменные не зануляются?

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

Но возникает вопрос: раз ОС такая молодец и зануляет всю память, то почему локальные переменные и куча заполнены мусором? Какие-то двойные стандарты.

Все на самом деле немножко сложнее.

Есть такое понятие, как "zero-fill on demand". Заполнение нулями по требованию.

Когда процесс запрашивает память под свои сегменты, стек и кучу, ОС на самом деле не дает ему реальные страницы памяти. А дает "виртуальные". То есть ничего не аллоцирует по факту. Такие страницы заполнены нулями.

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

И так она делает один раз на каждую физическую страницу.

Вот как появляются нули в реальной памяти. Теперь почему они не остаются навсегда.

Дело в том, что процесс переиспользует свою память. Программа в течение всей своей жизни использует один и тот же стек и кучу.

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

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

Именно поэтому кстати, мы можем очень легко получить доступ к данным, которые лежали на стеке ранее:

void fun1() {
int initialize = 10;
std::cout << initialize << std::endl;
}

void fun2() {
int uninitialize;
std::cout << uninitialize << std::endl;
}

int main() {
fun2();
fun1();
fun2();
}


Возможный вывод такого кода:

32760
10
10


Обратите внимание, что, вызывая функцию с переменной uninitialize в первый раз, мы получили мусор. Однако после вызова func1, где переменная инициализирована, в памяти стека на месте, где лежала initialize будет лежать число 10. Так как сигнатуры и содержимое функций в целом идентичны, то uninitialize во второй раз будет располагаться на том же самом месте, где и была переменная initialize. Соответственно, она будет содержать то же значение.

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

Reuse resources. Stay cool.

#OS #compiler

Грокаем C++

23 Oct, 09:00


​​Бесплатная zero-инициализация

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

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

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

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

Поэтому ОС никому не доверяет и сама зануляет всю память, которую выдает новому процессу.

Компилятор/линкер при формировании бинарника собирает все неинициализированные переменные вместе в одну секцию с названием .bss.

Получается, при старте программы у ОС запрашивается память в том числе под секцию .bss, и эта память уже аллоцируется зануленной! И никакого кода не нужно, за нас все делает операционка.

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

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

Don't reveal secrets. Stay cool.

#OS #compiler #cppcore

Грокаем C++

22 Oct, 09:00


​​Разница инициализаций

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

Единые правила - хорошая вещь. И как многие хорошие вещи, они чего-то стоят. А в С++ есть такой девиз: "мы не платим за то, что не используем". Мне не всегда нужно задавать значение переменной. Иногда меня это вообще не интересует. Я могу создать неинициализированную переменную и передать ее в функцию, где ей присвоится конкретное значение.

int i;
FillUpVariable(i);


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

Рассмотрим локальные переменные.

В сущности, они являются просто набором байт на текущем фрейме стека. И программа интерпретирует эти байты, как наши локальные переменные.

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

Теперь глобальные переменные

Они инициализируются всего один раз при старте программы. Соответственно, стоимость - немножко кода 1 раз при старте программы.

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

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

Теперь представьте, что мы бы потребовали устанавливать валидное значение всегда. Это просто неэффективно. Да и не нужно.

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

Be effective. Stay cool.

#cppcore #compiler

Грокаем C++

21 Oct, 12:00


Квиз
#новичкам

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

У меня к вам всего один вопрос. Что будет в результате попытки компиляции и запуска этого кода?

#include <iostream>

int id;

int main()
{
std::cout << id;
}

Грокаем C++

21 Oct, 10:01


Бесплатное IT-образование в 2024

Отобрали для вас полезные телеграм-каналы, которые помогут освоить программирование и другие IT-направления

Выбирайте нужное и подписывайтесь:

👩‍💻 С/С++: @Cpportal
📱 GitHub: @git_developer
🤓 Книги айти: @portalToIT
👩‍💻 Frontend: @FrontendPortal
⚙️ Backend: @BackendPortal
👩‍💻 Python: @PythonPortal
👩‍💻 Java: @Java_Iibrary
👩‍💻 C#: @KodBlog
🖥 Базы Данных & SQL: @SQL
👩‍💻 Golang: @juniorGolang
👩‍💻 PHP: @PHPortal
👩‍💻 Моб. разработка: @MobDev
👩‍💻 Разработка игр: @GameDevgx
👩‍💻 DevOps: @loose_code
🖥 Data Science: @DataSciencegx
🤔 Хакинг & ИБ: @cybersecinform
🐞 Тестирование: @QAPortal
📱 Маркетинг: @MarketingPortal
🖥 Дизайн: @PortalToDesign

➡️ Сохраняйте себе, чтобы не потерять

Грокаем C++

20 Oct, 09:00


Материалы для обучения
#новичкам

В этом посте вы очень хорошо постарались и накидали много ресурсов. Сейчас мы их немного систематизируем.

Начнем с самого популярного запроса. Книги. Пдфки будут в комментах.

База:

Бьерн Страуструп. "Программирование: принципы и практика использования C++".

Стивен Прата. «Язык программирования C++»

Стенли Липпман. "Язык программирования C++. Базовый курс"

Эндрю Кениг. "Эффективное программирование на С++"

Брайан Керниган. «Язык программирования С»


Немножко компьютер сайенса:

Бхаргава Адитья. "Грокаем алгоритмы".

Кирилл Бобров. "Грокаем конкурентность".


Книжки по продвинутому С++. Накладываются уже на адекватные знания языка и навыки написания кода.

Скотт Майерс. "Эффективный и современный С++"

Бартоломей Филипек. "С++17 в деталях".

Энтони Уильямс. «С++. Практика многопоточного программирования»

Пикус Ф. «Идиомы и паттерны проектирования С++».

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



Курсы:

Пояса от Яндекса. Платный.

"Добрый, добрый ОПП С++" на Stepik. Совсем недорогой.

1 и 2 части курса программирования на C++ от Computer Science Center на платформе Stepik. Из всех курсов, которые я изучал, это лучший в рунете имхо.

Программирование на языке C++ на Stepik. Бесплатный.

Программирование на языке C++ (продолжение) на Stepik. Бесплатный.


Введение в программирование (C++)курс Яндекса на Stepik. Бесплатный

Базовый курс С++ от Хэкслет. Бесплатный

Бесплатный курс от Яндекса

C++ Tutorial . Бесплатно

Яндекс Практикум «Разработчик С++». Платно.


Ютуб:

Константин Владимиров обо всем

Илья Мещерин С++

Роман Липовский. Конкурентность. Лекции и семинары

TheCherno. Нужен английский.

Simple Code


Интернет ресурсы:

https://ravesli.com/uroki-cpp. Нужен впн

https://www.learncpp.com

https://metanit.com/cpp/tutorial

https://leetcode.com - решение алгоритмических задачек

Теперь отсебятина

У всех разная подходящая модель обучения. Не концентрируйтесь только на книгах или курсах. У всего есть свои плюсы. Надо попробовать все и найти подходящий ВАМ формат обучения. Но нужны какие-то начальные рекомендации. Я бы начал с одной из базовых книг и обязательно после каждой главы решал бы задачки(это самое главное, иначе не запомнится). "Чтобы научиться программированию, необходимо писать программы" - Брайан Керниган. Поэтому чуть освоившись с языком я бы пошел на какие-нибудь курсы из списка и просто начал бы писать код. Пройдите 3-4 из них и вы уже будете довольно хороши.

Дальше уже можете на эту базу наваливать и лекции, и специфику, и прочее.

Не обязательно делать именно так. Обучайтесь так, чтобы вам было интересно. Читайте те книги, которые вам по кайфу читать. Не любите читать книги? Пользуйтесь интернет ресурсами и ютуб уроками.

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

Заслуженно помещаем этот пост в закреп. Теперь можно отправлять всех на него.

ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ

Upgrade yourself. Stay cool.

#digest

Грокаем C++

18 Oct, 09:09


Опасности автоматического вывода типов
#новичкам

C++17 дал нам замечательную фичу CTAD. Это автоматический вывод шаблонных параметров класса по инициализатору.

Теперь, если вы хотите создать например пару строки и числа, то вместо этого:

std::pair<std::string, int> pair{"Hello there!", 1};


Можно писать так:
std::pair pair{"Hello there!", 1};


Удобно? Безусловно! Только вот один вопросик есть.

Что будет, если я попытаюсь достать размер строки?

size_t size = pair.first.size();


А будет ошибка

error: request for member 'size' in 'a.std::pair<const char*, int>::first', 
which is of non-class type 'const char*'


Пара-то на самом деле не из строки и числа, а из указателя и числа. Но это и правильно. Компилятор не умеет читать мысли, а четко работает с тем, что ему предоставили. В данном случае "Hello there!" действительно преобразуется в тип const char*. Если вы хотите std::string, то нужно явно показать это компилятору:

std::pair pair{std::string("Hello there!"), 1};


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

Поэтому при использовании CTAD нужно тщательно следить за типами аргументов. Классы с не explicit конструкторами могут наделать большую невкусную кучу беспокойства.

Кстати, знаю адептов строгой типизации, которые даже auto не признают. А как вы относитесь к автоматическому выводу типов? Жду ваши мысли в комментах)

Be careful. Stay cool.

#cppcore #cpp17

Грокаем C++

17 Oct, 09:00


Так больше нельзя жить

Все. Нет сил больше игнорировать эту тему.

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

Хватит это терпеть!

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

Прошу активно участвовать в дискуссии. Мы все-таки новое поколение плюсовиков растим. Будущую гордость страны!

Мы вообще программисты или кто? Давайте автоматизируем процесс рекомендации.

Make life easier. Stay cool.

Грокаем C++

16 Oct, 09:00


​​Достаем элемент из последовательного контейнера

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

Я говорю о попе элементов. Не вот этой ( | ), а вот этом

void pop_back();

void pop_front();


Эти методы достают из контейнера элементы из зада или из переда соответственно.

"Какие тут проблемы?" - спросите вы.

И я вам отвечу.

Что произойдет, если я вызову эти методы на пустом контейнере? Если вы задумались, то это нормально, обычно такого не происходит. Но вот я такой Маша-растеряша и забыл проверить на пустоту перед вызовом. Будет UB.

Даже не исключение, которое можно обработать. Просто УБ. И можно УБиться в поисках бага, которая появится в следствии пропуска одной проверки.

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

Туда же идут и методы front() и back(). Они дают такое же UB, когда контейнер пуст.

Почему так сложилось? Вопрос сложный.

Но не в этом суть.

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

Споры о дизайне - горячо любимый всеми процесс. Каждый может решать этот вопрос по-разному.

Даже что-то подобное, на мой взгляд, куда более безопасный дизайн:

bool pop_back() {
if (data_.empty()) {
return false;
}
// remove element
return true;
}


А если его еще и аттрибутом nodiscard пометить, будет вообще щикарно(привет фанатам южного парка).

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

Но язык С++ никогда не славился своей безопасностью. И если вы можете своими силами обезопасить свой проект - нужно это делать. Даже таким несовершенным образом.

Stay safe. Stay cool.

#cppcore #STL

Грокаем C++

15 Oct, 10:00


​​Достигаем недостижимое

В прошлом посте вот такой код:

int main() {
while(1);
return 0;
}

void unreachable() {
std::cout << "Hello, World!" << std::endl;
}


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

Темная магия это или проделки ГосДепа узнаем дальше.

Для начала, этот код содержит UB. Согласно стандарту программа должна производить какие-то обозримые эффекты. Или завершиться, или работать с вводом-выводом, или работать с volatile переменными, или выполнять синхронизирующие операции. Это требования forward progress. Если программа ничего из этого не делает - код содержит UB.

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

Тут очень важно понять одну вещь. Компилятор следует не вашей логике и ожиданиям, как должна работать программа. У него есть фактически инструкция(стандарт), которой он следует.

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

В данном случае он просто удаляет цикл. Но он не только удаляет цикл. Но еще и удаляет инструкцию возврата из main.

В нормальных программах функция main в ассемблере представляет из себя следующее:

main:
// Perform some code
ret


ret - инструкция возврата из функции. И код функции main выполняется, пока не достигнет этой инструкции.

Так вот в нашем случае этой инструкции нет и код продолжает выполнение дальше. А дальше у нас очень удобненько расположилась функция с принтом, вывод которой мы и видим. Выглядит это так:

main:

unreachable():
push rax
mov rdi, qword ptr [rip + std::cout@GOTPCREL]
lea rsi, [rip + .L.str]
call std::basic_ostream<char, std::char_traits<char>>...


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

Справедливости ради стоит сказать, что в 19-м шланге поменяли это поведение и теперь таких неожиданностей нет.

Stay predictable. Stay cool.

#fun #cppcore #compiler

Грокаем C++

14 Oct, 09:00


Правда С++ замечательный язык?)

Грокаем C++

13 Oct, 10:00


​​Неименованные параметры функций

С++ позволяет не указывать имена параметров функций, если они не используются в коде.


void foo(int /no name here/);

void foo(int /no name here/)
{
std::cout << "foo" << std::endl;
}

foo(5);


Это можно делать и в объявлении функции, и в ее определении.

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

Но вот вопрос возникает тогда. Если параметр ничего не делает, нахрена он тогда вообще нужен?

На самом деле много кейсов, где неименованный параметр может пригодится.

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

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

💥 Иногда существующие сущности в коде требуют коллбэки определенного вида. И вам в своем коллбэке возможно не нужно использовать весь набор параметров. Но для соблюдения апи вы должны их указать в сигнатуре своего обратного вызова. В этом случае можно сделать эти параметры безымянными.

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

💥 Знаменитая перегрузка постфиксного оператора инкремента/декремента. Есть 2 вида этих операторов: префикстный и постфиксный. Проблема в том, что это все еще вызов функции operator++. Как различить реализации этих функций? Правильно, нужна перегрузка. Вот здесь и приходит на помощь безымянный параметр: в коде он не нужен, но влияет на выбор конкретной перегрузки. Выглядит это так:

struct Digit
{
Digit(int digit=0) : m_digit{digit} {}
Digit& operator++(); // prefix has no parameter
Digit operator++(int); // postfix has an int parameter
private:
int m_digit{};
};


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

Stay useful. Stay cool.

#cppcore #design

Грокаем C++

11 Oct, 10:08


​​Как передать в поток ссылку на объект?

Глупый вопрос на первый взгляд. Ну вот есть у вас функция

void RunSomeThread(const & SomeType obj) {...}


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

std::thread thr(RunSomeThread, obj);


Запускаете прогу, все нормально работает, вы довольный пьете кофеек. Но решаете проверить логи. Так, на всякий случай. А вы очень не хотите лишних копирований объектов SomeFunckingType. Поэтому логируете создание этих объектов. И в логах обнаруживаете странную штуку: ваш объект скопировался. WTF???

Дело в том, что новосозданный поток копирует аргументы своего конструктора в свой внутренний сторадж. Зачем это нужно? Проблема в том, что параметры, которые вы передали, могут не пережить время жизни потока и удалиться до его завершения. Тогда обращение к ним по ссылке вело бы к неопределенному поведению. Но копирование выполняется только для тех параметров, которые переданы по значению. Если передавать параметр по ссылке, то ссылка передастся во внутренний сторадж потока без копирования. Это нужно делать только тогда, когда вы на 100% уверены, что ваш аргумент переживет цикл жизни потока.

"Но я же передал obj по ссылке!" Погоди....

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

А сделать это очень просто. С помощью std::ref. Эта функция оборачивает ваш объект в другой шаблонный класс std::reference_wrapper, который хранит адрес вашего объекта. Теперь вы можете написать вот так:

std::thread thr(RunFuckingThread, std::ref(obj));


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

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

Stay conscious. Stay cool.

#concurrency #cppcore #memory

Грокаем C++

10 Oct, 09:00


​​Как узнать четное ли число

Вы сейчас подумали, типа "wtf, он шо нас за идиотов держит". Но погодите, щас все объясню.

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

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

В общем. Нужно просто проверить последний бит. Если он ноль - число четное, если нет - число нечетное. Все очень просто. Делается это с помощью битового & с единичкой.

Но во время написания этого поста мне пришла идея задать эту задачку ChatGPT, в тему недавнего поста про него. Правда я попросил сгенерировать 3 примера. Чисто из интереса. И результат меня сильно удивил. Все 3 примера были правильные, среди них было решение из абзаца выше, но было и еще 2, о которых я и не думал. После этого попросил нагенерить еще 2 примера. И они тоже были верные. Конечно, все из них использовали битовые операции, но как филигранно!

Очень интересно решение с битовым умножением на -2. Дело в том, что -2 в памяти компьютера представляется как 111...1110. Поэтому умножение любого числа на -2 будет давать то же самое число, только если последний бит был выставлен в 0.

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

Stay amazed. Stay cool.

#fun

Грокаем C++

09 Oct, 10:00


Удаляем элемент из ассоциативного контейнера по значению

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

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

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

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

std::map<int, int> map{{1, 6}, {2, 7}, {3, 8}, {4, 9}, {5, 10}};
// вот так
auto it = std::find_if(map.begin(), map.end(), [](const auto& elem) {return elem.second == 10;});
map.erase(it);
//
std::for_each(map.begin(), map.end(), [](const auto& item){
std::cout << item.first << " " << item.second << std::endl;});
// OUTPUT
// 1 6
// 2 7
// 3 8
// 4 9


Две строчки на идейно очень простое и понятное действие. Ох, если бы был метод erase_if...

И вы знаете, в С++20 появились перегрузки свободной функции std::erase_if для каждого стандартного контейнера. Теперь можно написать просто вот так:

std::erase_if(map, [](const auto& elem) {return elem.second == 10;});


И результат вывода будет таким же.

У кого есть только древние плюсы - не переживайте. Для вас эти перегрузки реализовали в экспериментальной библиотеке. Просто сделайте так:

#include <experimental/map>
std::experimental::erase_if(map, [](const auto& elem) {return elem.second == 10;});


И все заработает.

Do things easier. Stay cool.

#STL #cpp20

Грокаем C++

08 Oct, 09:00


Шутейки от чатгпт

Почему С++ программисты не играют в прятки? Они задолбались искать баги, скрытые в своем коде!
(на картинке типа плюсовик в поте лица ищет баги. Так, что аж ноут запотел)

На самом деле не самая плохая шутка в мире. На нобелевскую премию не тянет, но под попкорн - в самый раз.
Как думаете, смогут ли нейросети отнять наш удвоено-позитивный хлеб?

Грокаем C++

07 Oct, 09:00


​​Результаты ревью
#опытным

Понимаю, что код был, мягко говоря, не для новичков, хоть и иллюстрировал начальный пример работы с epoll. Поэтому критиков не очень было много. Но все же Максим и Михаил хорошо постарались с поиском проблем. Давайте им похлопаем 👏👏👏.

А теперь суммируем.

Пройдемся по очевидному, что бросается в глаза сразу.

🔞 В функции read_data_and_count есть возвращаемое значение, но ничего не возвращается.

🔞 Повсюду утечки. Не освобождаются entries, не закрывается дескриптор epoll_fd.

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

🔞 Надоели указатели эти. Мы же С++, нам ссылки подавай!

🔞 Цикл, собирающий результаты мог бы быть более полюсовым и использовать стандартные функции из STL.

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

Из неочевидного:

😱 Использование VLA, то есть variable-length array, в строчке с epoll_event pending[N]. Это не стандарт языка С++, поэтому надо переделать на нормальный вектор.

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

😱 Еполл может вернуть событие EPOLLERR в случае какой-то ошибки. А также он может прерваться из-за приема сигнала сгенерить ошибку EINTR. Опять же нет проверки.

😱 epoll_wait последним параметром принимает таймаут. -1 значит, что мы бесконечно блочимся в ожидании событий. И выходим из него только когда события произошли. Потенциально, это может длится примерно 100500 лет. Не очень хочется иметь потенциальную возможность получить бесконечный цикл. Поэтому какой-то таймаут поставить нужно, чтобы мы хотя бы об ошибке узнали.

😱 Не очень понятно, зачем обработчику событий знать о том, сколько файлов осталось, и влиять на это количество. Из process_epoll_event вполне можно возвращать индикатор, сигнализирующий о том, что мы прочитали данные из сокета до конца. Таким образом чисто внутренняя переменная files_left становится в единоличной власти своей материнской функции. Собственно, как и должно быть. Тогда и флаг done в структуре не нужен.

😱 Обо всех ошибках вызывающему коду хотелось бы знать, поэтому надо как-то сообщать ему о них. Можно по аналогии с сисколами использовать возвращаемое значение -1, как индикатор ошибки, но по плюсовому можно использовать std::optional. Ну или кидать исключения/возвращать объект ошибки, кому как нравится.

Фух, вроде все, что увидел, написал. Кто еще что-то заметит - не стесняйтесь, пишите в комменты. Исправленный код скину в комменты.

Let people critique your solutions. Stay cool.

Грокаем C++

06 Oct, 11:00


​​Ревью
#опытным

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

1️⃣ Предварительно за кадром зарегистрировали нужные события на еполле

2️⃣ Создали массив из эвентов, в который эполл будет записывать произошедшие события

3️⃣ Дожидаемся этих событий в epoll_wait и дальше как-то обрабатываем.

Если вы знакомы с select|poll, то здесь небольшие отличия в интерфейсе + еполл сам говорит нам на каких дискрипторах появился евент.

Цель всего этого добра - подсчитать общее количество байт, которое пришло на все дескрипторы.

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

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

Ну и чего ждем? Комментарии сами себя не напишут! Погнали хейтить чужой код!

Analyse solutions. Stay cool.

Грокаем C++

04 Oct, 09:09


​​Signed Integer overflow

Переполнение знаковых целых чисел - всегда было и остается болью в левой булке. Раньше даже стандартом не было определено, каким образом отрицательные числа хранились бы в памяти. Однако с приходом С++20 мы можем смело утверждать, что стандартом разрешено единственное представление отрицательных чисел - дополнительный код или two's complement по-жидоанглосаксонски. Казалось бы, мы теперь знаем наверняка, что будет происходить с битиками при любых видах операций. Так давайте снимем клеймо позора с переполнения знаковых интов. Однако не все так просто оказывается.

С приходом С++20 только переполнение знаковых чисел вследствие преобразования стало определенным по стандарту поведением. Теперь говорится, что, если результирующий тип преобразование - знаковый, то значение переменной никак не изменяется, если исходное число может быть представлено в результирующем типе без потерь.
В обратном случае, если исходное число не может быть представлено в результирующем типе, то результирующим значением будет являться остаток от деления исходного значения по модулю 2^N, где N - количество бит, которое занимает результирующий тип. То есть результат будет получаться просто откидыванием лишних наиболее значащих бит и все!

Однако переполнение знаковых интов вследствие арифметических операций до сих пор является неопределенным поведением!(возмутительно восклицаю). Однако сколько бы возмущений не было, все упирается в конкретные причины. Я подумал вот о каких:

👉🏿 Переносимость. Разные системы работают по разным принципам и UB помогает поддерживать все системы оптимальным образом. Мы могли бы сказать, что пусть переполнение знаковых интов работает также как и переполнение беззнаковых. То есть получалось бы просто совершенно другое неожиданное (ожидаемое с точки зрения стандарта, но неожиданное для нас при запуске программы) значение. Однако некоторые системы просто напросто не продуцируют это "неправильное значение". Например, процессоры MIPS генерируют CPU exception при знаковом переполнении. Для обработки этих исключений и получения стандартного поведения было бы потрачено слишком много ресурсов.

👉🏿 Оптимизации. Неопределенное поведение позволяет компиляторам предположить, что переполнения не произойдет, и оптимизировать код. Действительно, если УБ - так плохо и об этом знают все, то можно предположить, что никто этого не допустит. Тогда компилятор может заняться своим любимым делом - оптимизировать все на свете.
Очень простой пример: когда происходит сравнение a - 10 < b -10, то компилятор может просто убрать вычитание и тогда переполнения не будет и все пойдет, как ожидается.

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

Leave room for uncertainty in life. Stay cool.

#cpp20 #compiler #cppcore

Грокаем C++

03 Oct, 09:00


Как компилятор определяет переполнение

В прошлом посте я рассказал, как можно детектировать signed integer overflow с помощью готовых функций. Сегодня рассмотрим, что ж за магия такая используется для таких заклинаний.

Сразу с места в карьер. То есть в ассемблер.

Есть функция

int add(int lhs, int rhs) {
int sum;
if (__builtin_add_overflow(lhs, rhs, &sum))
abort();
return sum;
}


Посмотрим, во что эта штука компилируется под гцц х86.

Все немного упрощаю, но в целом картина такая:

    mov %edi, %eax
add %esi, %eax
jo call_abort
ret
call_abort:
call abort

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

Инструкция jo выполняет прыжок, если выставлен флаг OF в регистре EFLAGS. То есть Overflow Flag. Он выставляется в двух случаях:

1️⃣ Если операция между двумя положительными числами дает отрицательное число.

2️⃣ Если сумма двух отрицательных чисел дает в результате положительное число.

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

127 + 127 = 0111 1111 + 0111 1111 = 1111 1110 = -2 (в дополнительном коде)

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

Для беззнаковых чисел тоже кстати есть похожий флаг. CF или Carry Flag. Мы говорили, что переполнение для беззнаковых - не совсем переполнение, но процессор нам и о таком событии дает знать через выставление carry флага.

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

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

Detect problems. Stay cool.

#base #cppcore #compiler

Грокаем C++

02 Oct, 09:00


​​Проверяем на целочисленное переполнение

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

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

Какие вообще бывают переполнения по типу операции? Если мы складываем 2 числа, то их результат может не влезать в нужное количество разрядов. Вычитание тоже может привести к переполнению, если оба числа будут сильно негативные(не будьте, как эти числа). Умножение тоже, очевидно, может привести к overflow. А вот деление не может. Целые числа у нас не могут быть по модулю меньше единицы, поэтому деление всегда неувеличивает модуль делимого. Значит и переполнится оно не может.

И какая радость, что популярные компиляторы GCC и Clang уже за нас сделали готовые функции, которые могут проверять на signed integer overflow!

bool __builtin_add_overflow(type1 a, type2 b, type3 *res);
bool __builtin_sub_overflow(type1 a, type2 b, type3 *res);
bool __builtin_mul_overflow(type1 a, type2 b, type3 *res);


Они возвращают false, если операция проведена штатно, и true, если было переполнение. Типы type1, type2 и type3 должны быть интегральными типами.

Пользоваться функциями очень просто. Допустим мы решаем стандартную задачку по перевороту инта. То есть из 123 нужно получить 321, из 7493 - 3947, и тд. Задачка плевая, но есть загвоздка. Не любое число можно так перевернуть. Модуль максимального инта ограничивается двумя миллиадрами с копейками. Если у входного значения будут заняты все разряды и на конце будет 9, то перевернутое число уже не влезет в инт. Такие события хотелось бы детектировать и возвращать в этом случае фигу.

std::optional<int32_t> decimal_reverse(int32_t value) {
int32_t result{};
while (value) {
if (__builtin_mul_overflow(result, 10, &result) or
__builtin_add_overflow(result, value % 10, &result))
return std::nullopt;
value /= 10;
}
return result;
}

int main() {
if (decimal_reverse(1234567891).has_value()) {
std::cout << decimal_reverse(1234567891).value() << std::endl;
} else {
std::cout << "Reversing cannot be perform due overflow" << std::endl;
}

if (decimal_reverse(1234567894).has_value()) {
std::cout << decimal_reverse(1234567894).value() << std::endl;
} else {
std::cout << "Reversing cannot be perform due overflow" << std::endl;
}
}

// OUTPUT:
// 1987654321
// Reversing cannot be perform due overflow


Use ready-made solutions. Stay cool.

#cppcore #compiler

Грокаем C++

01 Oct, 09:16


​​Целочисленные переполнения

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

Для беззнаковых типов тут довольно просто. Переполнение переменных этих типов нельзя в полной мере назвать переполнением, потому что для них все операции происходят по модулю 2^N. При "переполнении" беззнакового числа происходит его уменьшение с помощью деления по модулю числа, которое на 1 больше максимально доступного значения данного типа(то есть 2^N, N - количество доступных разрядов). Но это скорее не математическая операция настоящего деления по модулю, а следствие ограниченного размера ячейки памяти. Чтобы было понятно, сразу приведу пример.

Вот у нас есть число UINT32_MAX. Его бинарное представление - 32 единички. Больше просто не влезет. Дальше мы пробуем прибавить к нему единичку. Чистая и незапятнанная плотью компьютеров математика говорит нам, что в результате должно получится число, которое состоит из единички и 32 нулей. Но у нас в распоряжении всего 32 бита. Поэтому верхушка просто отрезается и остаются только нолики.

Захотим мы допустим пятерку, бинарное представление которой это 101, прибавить к UINT32_MAX. Произойдет опять переполнение. В начале мы берем младший разряд 5-ки и складываем его с UINT32_MAX и уже переполненяемся, получаем ноль. Осталось прибавить 100 в двоичном виде к нолю и получим 4. Как и полагается.

И здесь поведение определенное, известное и стандартное. На него можно положиться.

Но вот что со знаковыми числами?

Стандарт говорит, что переполнение знаковых целых чисел - undefined behaviour. Но почему?

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

Так вот во всех трех сценариях результат переполнения будет разный!

Возьмем для примера дополнительный код и 4-х байтное знаковое число. Ноль выглядит, как 000...00, один как 000...01 и тд. Максимальное значение этого типа INT_MAX выглядит так: 0111...11 (2,147,483,647). Но! Когда мы прибавляем к нему единичку, то получаем 100...000, что переворачиваем знаковый бит, число становится отрицательным и равным INT_MIN.

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

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

Don't let the patience cup overflow. Stay cool.

#cpp20 #compiler #cppcore

Грокаем C++

28 Sep, 09:00


​​Знак-амплитуда

Начнем раскрывать тему вариантов представления отрицательных чисел с метода "знак-величина" или sign-magnitude. Это самый интуитивно понятный для нас способ репрезентации целых чисел. В обычной математике как: есть грубо говоря модуль числа и его знак.

И это довольно просто перенести в память. Мы просто обговариваем, что наиболее значимый бит числа мы отводим по бит знака, а остальные биты обозначают модуль числа. Если знаковый бит - 0, значит число положительное. Если 1 - отрицательное. Таким образом получается вполне естественная ассоциация с привычными нам правилами. Есть натуральный ряд чисел плюс ноль. Чтобы получить множество всех целых чисел, нужно ко множеству натуральных чисел прибавить множество чисел, которое получается из натуральных путем добавления минуса в начале числа.

Давайте на примере. В восьмибитном числе только 7 бит будут описывать модуль числа, который может находится в отрезке [0, 127]. А самый старший бит будет отведен для знака. С добавлением знака мы теперь может кодировать все числа от -127 до 127. Так, число -43 будет выглядеть как 10101011, в то время как 43 будет выглядеть как 00101011. Однако очень внимательные читатели уже догадались, что эта форма представления отрицательных чисел имеет некоторые side-effect'ы, которые усложняют внедрение этого способа в реальные архитектуры:

1️⃣ Появление двух способов изображения нуля: 00000000 и 10000000.

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

3️⃣ Из предыдущего пункта выходит, что операции сложения и вычитания чисел с таким представлением требуют разной логики в зависимости от знакового бита. Мы должны в начале "отрезать" знаковый бит, сделать операцию, которая соответствует комбинации знаковых битов операндов и первоначальной операции, и потом обратно совместить результат со знаком.

4️⃣ Раз у нас 2 нуля, мы можем представлять на 1 число меньше, чем могли бы в идеале.

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

Однако "знак-величина" используется по сей день, но немного для другого. IEEE floating point использует этот метод для отображения мантиссы числа. Есть модуль мантиссы(ее абсолютное значение) и есть ее знаковый бит. Кстати поэтому у нас есть положительные и отрицательные нули, бесконечности и NaN'ы во флотах. Вот как оно оказывается работает.

Apply yourself in the proper place. Stay cool.

#cppcore #base

7,804

subscribers

29

photos

3

videos