Грокаем C++
Открыть в Telegram
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов. По всем вопросам (+ реклама) @ninjatelegramm Менеджер: @Spiral_Yuri Реклама: https://telega.in/c/grokaemcpp Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Больше9 362
Подписчики
+424 часа
+27 дней
+3730 дней
Архив постов
9 361
Новый год на пороге, а значит наступило время подводить итоги уходящего года! 🤩
Много всего произошло! Больше 7500 новых подписчиков и больше 300 авторских контентных постов было опубликовано. Очень круто, что у нас получилось постоянно вести этот проект, делиться с вами знаниями и самим учиться. Проект забирает много времени, не каждый готов вывести такую нагрузку.
Но то коммьюнити, которое мы здесь создаем, оно стоит каждого килоджоуля энергии, вложенной в канал. Столько крутых спецов, которые делятся своими знаниями и на постоянной основе обсуждают плюсы. Очень немногие ресурсы могут похвастаться этим.
Поэтому хочу выразить огромную благодарность вам всем.
С П А С И Б О !!! Вы драйверы этого канала и конечно же нашего развития. Почти каждый день выносить свои знания на суд огромной аудиторий потрясающих специалистов - конечно тут волей-неволей сильно прокачаешься во всех мелочах.
Круто, что канал выработал свой неповторимый стиль. Мы стараемся разбавлять нудятину о С++ всякими смешнявками и мемами. А также придумали несколько новых форматов. Мне лично очень зашел формат #ревью. Это очень крутой способ затронуть несколько сложных тем, проверить зоркость подписчиков, ну и просто потрындеть за плюсы. Вроде очевидная вещь, но я такого ни у кого не встречал.
Ну что это мы все о себе, да о себе...
Желаем вам в Новом году не останавливаться на достигнутом и мчаться вперед на всех парах! Ставьте перед собой цели и достигайте их. И главное - не давайте змею искусителю сбить вам с пути😉. Никаких питонов - только Плюсы, только хардкор!
Этим постом мы завершаем год и уходим на небольшой перерыв. Увидимся в следующем году! С праздником вас, дорогие положительные люди! 🥂🥳
Happy New Year! Stay cool.
9 361
Оператор запятая внутри 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 #cpp239 361
Ответ
На самом деле в код выше я упустил оператор 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 #cppcore9 361
Квиз
В тему индексирования многоразмерных массивов закину сегодня #quiz. Ничего супер запутанного, но от этого не менее интересно.
Вот мы в прошлых постах говорили, что начиная с С++23 мы можем определять оператор[], который принимает несколько аргументов.
А что будет, если мы просто возьмем и создадим многомерный массив, как нам бы это было удобно? И будет получать доступ к его элементам:
#include <iostream>
int main() {
auto array = new int[10, 20]{10};
std::cout << array[1, 0] << " " << array[11, 1] << std::endl;
}
Видели когда-нибудь такое? Даже если нет, у меня к вам всего один вопрос.
Какой будет результат попытки компиляции и запуска этого кода ?9 361
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
9 361
Владимир, вы уволены! Вместо вас мы взяли нейросеть!
Рынок IT скоро захватят ChatGPT и Gemini. А они появились лишь в 2022 году.
Станьте тем, кто получает выгоду с ИИ. Как их использовать для кода подробно рассказывают на канале Artificial Intelion.
Тут каждый день делают обзоры на нейросети и их применение в IT, делятся лучшими инструментами для программистов и показывают как их внедрить.
Переложите свои задачи на ИИ, чтобы не просрачивать дедлайны - @artificial_intelion
9 361
Доступ к элементам многомерных структур
#опытным
Если вы спросите разработчиков 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
9 361
Фикс баги с инициализацией инта
В прошлом посте говорили об одной неприятности при использовании универсальной инициализации интов. При таком написании:
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 #compiler9 361
Время от времени в сети появляются комментарии и персонажи, которые продвигают нарротив, что алгосы не нужны.
Я понимаю их. Приходишь на собес и тебе предлагают решить совсем какую-то оторванную от реальности задачу. "Обведите острова на карте". Это почти смешно. Когда и кому эти навыки пригодятся - не понятно.
Однако не все лежит на поверхности.
На собеседовании интервьюеры решают очень важную и нетривиальную задачу - за очень ограниченное время понять, справится ли человек с будущими задачами или нет.
И нужен инструмент, который поможет это проверить. Именно таким инструментом и являются алгосы. Популярно объясняю:
👉🏿 Знания алгоритмов и структур данных - база компьютер сайенса. Без нее в плюсах особо делать нечего. Мы часто работаем на низких уровнях абстракций и высоких нагрузках, где знания базы просто жизненно необходимы.
👉🏿 Кандидату нужно абстрактный текст задачи уметь перенести на язык компьютерных сущностей. Это очень важный навык. Бизнес приносит задачи в формате "мне нужно клиенту отправлять пиццу квадракоптером". И важным этапом работы будет перенос этих слов на язык кода. Тут и требования надо уточнять, и в целом понимать, чего от тебя хотят (не все понимают).
👉🏿 Компьютерные сущности нужно правильно уметь перенести в код на конкретном языке. Тут хоть базово, но проверяются навыки работы непосредственно с языком.
👉🏿 Кандидату нужно убедиться, что алгоритм работает. Для этого он должен уметь тестировать свой код, в том числе и устно. Тесты в проекты нужны, никто с этим спорить не будет. Но вот прилетела вам бага с прода. И иногда ее сложно вопроизвести. Чтобы побороть багу нужно вычитывать много кода и пытаться найти в нем ошибки чисто эмперически. Или написать дополнительные тесты. Так что устно проверить свой код на ошибки - это такая демо-версия обычной рабочей рутины и стрессовых событий.
Можно перечислять еще много. Но если все эти навыки нужны работодателю, то они нужны и нам всем. Алгоритмы нужны не только для собеседований, но и для реальной разработки. Они помогают стать более сильным специалистом в реальности и в глазах работодателя.
Какой вывод?
Нужно прокачиваться в алгоритмах. Можно конечно начать изучать все самостоятельно, но зная многих людей — без кнута извне их сложно заставить самообучаться. Поэтому предлагаю тебе пойти на курс от Яндекс Практикума — "Алгоритмы и структуры данных".
Хочешь писать эффективный код? Тебе туда.
Хочешь успешно проходить собесы? Тебе туда.
Хочешь накачать мышцу компьютер сайенса? Тебе туда.
Хочешь знать, что за птица эта Дейкстра, где в коде можно встретить бор, и кто сделал кучу на дереве? Знаю, что хочешь. Тебе тоже туда.
Если сомневаешься, пройди бесплатный вводный тест - на нем поймешь формат обучения и разберешься, что тебя будет ожидать на курсе.
Все дороги идут туда. Так чего ждать?
Реклама, АНО ДПО “Образовательные технологии Яндекса”, ИНН 7704282033, 119021, г. Москва, ул. Тимура Фрунзе, д. 11, корпус 2, erid: 2SDnjeQi4jV
9 361
Если бы у вас был 1'000'000$ на реализацию вашей айтишной бизнес идеи, то какой стартап бы вы создали?
9 361
Баг универсальной инициализации
В С++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 #cppcore9 361
Почему перегрузки не могут иметь разные возвращаемые значения
#новичкам
Обычно люди просто принимают на веру факт, что перегрузки различаются только набором аргументов. Впрочем, как и все, что вливается в голову в начале обучения.
Но никогда не поздно задуматься над смыслом. А почему мы не можем перегружать функции по возвращаемому значению?
Может вот так абстрактно рассуждая, и не сразу придет ответ. Но на самом деле все просто, если посмотреть на конкретный пример. Давайте представим, что никаких запретов нет. Теперь перегрузки могут различаться только возвращаемым значением. И у нас есть 2 функции, почти одинаковые , но имеют разные возвращаемые значения.
int GetRandomNumber();
float GetRandomNumber();
Пускай мы хотим генерировать и целые числа, и дробные числа. И то, и то число, общее название оправдано.
Теперь попробуем вызвать первую версию.
int number = GetRandomNumber();
А вы уверены, что мы сейчас вызвали именно первую версию? Флоат спокойно кастуется к инту. Ничего не мешает компилятору выбрать вторую версию для этого вызова.
Ситуация становится комичной, если использовать auto:
auto number = GetRandomNumber();
Тут как бы вообще нет ни намека на то, какую версию вызывать. Компилятор не умеет читать ваши мысли и намерения. Он не знает, какую версию вы хотели вызвать. Просто рандомно он выбрать не может. Появляется парадокс и вселенная схлопывается.
Вот и получили противоречие.
Компилятор просто не может отличить вызовы таких функций. Поэтому это и запрещено.
Don't be confusing. Stay cool.
#cppcore9 361
❓ Знаете, что делает код на C++ мощным? Правильная работа с библиотеками!
👉 Если вы хотите стать востребованным C++ разработчиком, пора прокачивать навыки работы с библиотеками. Это ваш ключ к написанию профессионального и оптимизированного кода!
Что вас ждет на открытом уроке:
- узнаете, какие бывают библиотеки и чем они отличаются;
- научитесь подключать сторонние библиотеки и собирать свои;
- поймете, как библиотеки упрощают вашу работу и делают код эффективнее.
⭐️ Спикер Денис Злобин — старший инженер-программист в Astra Linux, опытный наставник разработчиков.
⏰ 18 декабря в 19:00 мск. Для начинающих и Junior-разработчиков, готовых к карьерному росту. Вебинар проходит в преддверии старта курса «C++ Developer», участники урока получат скидку на большое обучение.
👉 Для участия зарегистрируйтесь: https://otus.pw/uE2f/
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
9 361
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 #cpp179 361
Подробно рассказываем о высокопроизводительных решениях с фреймворком userver на новом бесплатном вебинаре!
Он предназначен для создания надежных и молниеносных микросервисов и веб-серверов. Освойте передовой C++ фреймворк всего за 90 минут!
✅ Вы узнаете:
- Уникальные архитектурные особенности и преимущества userver, которые сделают вашу работу легче и эффективнее.
- Как быстро настроить окружение и запустить свой первый проект, не тратя лишнего времени.
- Советы высококлассных профессионалов по работе с асинхронностью, которые помогут вам стать мастером своего дела и очень много другой полезной сочной информации!
💡 Будет особенно интересно:
- C++ разработчикам, стремящимся освоить мощный инструмент для создания микросервисов.
- Backend-инженерам и Team Lead'ам, ищущим высокопроизводительные решения для своих задач.
- Специалистам, работающим над масштабируемыми системами.
❌❌Получите практический и ценный опыт работы с userver, который можно сразу применить в своих проектах - регистрируйтесь по ссылке: https://otus.pw/Ocpbe/
P.S. Все зарегистрированные участники получат приличную скидку на обновленный топовый курс «C++ Developer. Professional»
Реклама. ООО «Отус онлайн-образование», ОГРН 11777466185769 361
Получаем адрес перегрузки
#новичкам
Представьте, что у вас есть функция, которая вызывает любой коллбэк:
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.
#cppcore9 361
Получаем адрес стандартной функции
Иногда людям приходится работать с указателями на функции. И у них может возникнуть надобность взять адрес у стдшной функции. Например, вы хотите как-то трансформировать каждый символ в строке 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 #cpp209 361
⚡XMAS HACK к нам мчится!
С 20 по 23 декабря под звон Jingle bells пройдет самый яркий и праздничный хакатон 2024 года - XMAS HACK.
🎁Дед Мороз и Снегурочка уже положили под елочку XMAS HACK особый подарок - 1 000 000 рублей!
✨Уникальный кейс от Интерюнис-ИТ:
Реновация пользовательского интерфейса программного обеспечения акустико-эмиссионного измерительного комплекса.
👨💻Задача:
Улучшить текущий пользовательский интерфейс программы без модификации содержательной части кода. Основной фокус модификации визуальных элементов, навигации и общем удобстве использования, сохраняя при этом все существующие функциональные возможности.
🚀Осталось собрать команду и до 19 декабря подать заявку на участие: https://tglink.io/642c1efadcff?erid=LjN8KCi28
🎅XMAS HACK к нам мчится, скоро все случится!🌟
#реклама
О рекламодателе
9 361
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