У вас закончился пробный период!
Для полного доступа к функционалу, пожалуйста, оплатите премиум подписку
GR
Грокаем C++
https://t.me/grokaemcpp
Возраст канала
Создан
Язык
Русский
15.7%
Вовлеченность по реакциям средняя за неделю
5.39%
Вовлеченность по просмотрам средняя за неделю

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

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

Сообщения Статистика
Репосты и цитирования
Сети публикаций
Сателлиты
Контакты
История
Топ категорий
Здесь будут отображены главные категории публикаций.
Топ упоминаний
Здесь будут отображены наиболее частые упоминания людей, организаций и мест.
Найдено 99 результатов
GR
Грокаем C++
8 216 подписчиков
1
1
Виртуальные функции в compile-time Ч2
#опытным

Сходу не очень понятны кейсы применения полиморфизма на виртуальных функциях во время компиляции. У нас как бы есть шаблоны, которые прекрасно работают. Так какие применения у constexpr виртуальных функций?

constexpr виртуальные функции могут помочь перенести больше вычислений в компайл тайм. Предложение в стандарт по этому поводу содержит следующий пример:

В стандартной библиотеке есть отличный класс std::error_code. Но он не идеальный . Он не поддерживает вычисления в compile-time. Стандартную библиотеку не поправишь, но мы можем первое улучшение - сделать свой error_code с блэкджеком и constexpr:

class error_code
{
private:

int val_;
const error_category* cat_;

public:

constexpr error_code() noexcept;
constexpr error_code(int val, const error_category& cat) noexcept;
template
constexpr error_code(ErrorCodeEnum e) noexcept;

constexpr void assign(int val, const error_category& cat) noexcept;
template
constexpr error_code& operator=(ErrorCodeEnum e) noexcept;
constexpr void clear() noexcept;

constexpr int value() const noexcept;
constexpr const error_category& category() const noexcept;
constexpr explicit operator bool() const noexcept;

error_condition default_error_condition() const noexcept;
string message() const;
};

Второе улучшение, которое мы можем сделать - устранить ограничение error_code от захардкоженого в ноль значения успеха операции. Существуют категории ошибок, которые считают все неотрицательные значения успешными, и есть (по общему признанию, очень редкие) другие, в которых ноль является неудачей. Чтобы решить эту проблему, мы уже имеем механизм - внутри error_code есть указатель на базовый класс error_category*, наследникам которого мы и можем делигировать принятие решения о том, является ли значение ошибкой или нет.

class error_category
{
public:
// ...
virtual bool failed(int ev) const noexcept;
// ...
};

// И добавляем метод в класс error_code
class error_code
{
// ...
bool failed() const noexcept { return cat_->failed(val_); }
// ...
};

Однако не-constexpr виртуальные функции ломают наше желание разрешить использовать error_code во время компиляции. Благо в С++20 мы можем их пометить constexpr и все заработает как надо!

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

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

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

Increase your usability. Stay cool.

#cpp20
27.04.2025, 12:00
t.me/grokaemcpp/685
GR
Грокаем C++
8 216 подписчиков
39
28
1.5 k
Виртуальные функции в compile-time
#опытным

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

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

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

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

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

constexpr int double_me(int n)
{
return n * 2;
}
// условие верное и мы не падаем
static_assert(double_me(4) == 8);
// условие ложно и компиляция прервется на этой строчке
static_assert(double_me(4) == 7);

В примере мы определяем constexpr функцию double_me и проверяем с помощью static_assert'а то, что она вычисляется во время компиляции.

Изначально constexpr функции были довольно ограничены по возможностям своего применения. Однако с новыми стандартами спектр применений расширяется, так как все больше операций из стандартной библиотеки можно проводить в compile-time. Сейчас даже с контейнерами в complie-time можно работать. Но мы сейчас не об этом.

Начиная с С++20 constexpr функции могут быть виртуальными!

struct VeryComplicatedCaclulation
{
constexpr virtual int double_me(int n) const = 0;
};

struct Impl: VeryComplicatedCaclulation
{
constexpr virtual int double_me(int n) const override
{
return 2 * n;
}
};

constexpr auto impl = Impl{};
// для полиморфизма с виртуальными функциями нужна ссылка
constexpr const VeryComplicatedCaclulation& impl_ref = impl;

constexpr auto a = impl_ref.double_me(4);
static_assert(a == 8); // true

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

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

Increase your usability. Stay cool.

#cpp11 #cpp20 #cppcore
24.04.2025, 12:00
t.me/grokaemcpp/684
GR
Грокаем C++
8 216 подписчиков
21
15
1.6 k
Сравниваем производительности оператора<
#опытным

В этом посте я рассказал об отличном способе лексикографического сравнения набора объектов с помощью std::tie. Однако в комментариях несколько подписчиков задались вопросом, а не будет ли использование std::tie сильно ударять по производительности? Настоящих плюсовиков всегда на подкорке волнует вопрос оверхеда используемых инструментов. Поэтому сегодня мы выясним, есть ли разница в более менее практических вычислениях между разными вариантами оператора< .

Большое спасибо, @SoulslikeEnjoyer, за представление основного объема кода.

Сравним 4 реализации operator<:
struct Time_comparison_unreadable {
int hours;
int minutes;
int seconds;

bool operator<(const Time_comparison_unreadable& other) {
if ((hours < other.hours) || (hours == other.hours && minutes < other.minutes) || (hours == other.hours && minutes == other.minutes && seconds < other.seconds))
return true;
else
return false;
}
};

struct Time_comparison_readable {
// fields
bool operator<(const Time_comparison_readable& other) {
if (hours < other.hours) return true;
if (hours > other.hours) return false;
if (minutes < other.minutes) return true;
if (minutes > other.minutes) return false;
if (seconds < other.seconds) return true;
return false;
}
};

struct Time_tie {
// fields
bool operator<(const Time_tie& other) {
return std::tie(hours, minutes, seconds) < std::tie(other.hours, other.minutes, other.seconds);
}
};

struct Time_spaceship {
// fields
auto operator<=>(const Time_spaceship &) const = default;
};

Первые 2 варианта - это обычные реализации лексикографического оператора сравнения, просто второй из них более читаемый. В структуре Time_tie мы используем std::tie для формирования тупла и используем оператор сравнения тупла. В последнем варианте используем дефолтно-сгенерированный spaceship оператор.

Для того, чтобы качественно сравнить время выполнения чего-либо aka провести перфоманс тесты, нам поможет фреймфорк google benchmark. Она предоставляет гибкие инструменты для управления запуском кода и измерением времени его работы. Не будем вдаваться в детали фреймворка, а сразу посмотрим код:

std::random_device dev;
std::mt19937 rng(dev());
std::uniform_int_distribution dist(1,100);

template
static void time_comparison_experiment(benchmark::State& state) {
std::vector v(1'000'000);
std::generate(v.begin(), v.end(), [&] () -> TimeClass { return TimeClass{ dist(rng) % 24, dist(rng) % 60, dist(rng) % 60 }; });
while (state.KeepRunning()) {
auto start = std::chrono::high_resolution_clock::now();
std::sort(v.begin(), v.end());
auto end = std::chrono::high_resolution_clock::now();

auto elapsed_seconds =
std::chrono::duration_cast>(
end - start);
state.SetIterationTime(elapsed_seconds.count());
std::shuffle(v.begin(), v.end(), rng);
}
}

BENCHMARK(time_comparison_experiment)->UseManualTime()->Iterations(20);

BENCHMARK(time_comparison_experiment)->UseManualTime()->Iterations(20);

BENCHMARK(time_comparison_experiment)->UseManualTime()->Iterations(20);

BENCHMARK(time_comparison_experiment)->UseManualTime()->Iterations(20);

Все просто: создаем вектор, наполняем его объектами с рандомным временем, в цикле производим сортировку как операцию, обильно использующую сравнение, и шаффлим элементы вектора перед каждой новой итерацией цикла. Сравнивать нам нужно только время выполнения самой операции сортировки и gbenchmark предоставляет возможность пользователю самому измерять только те операции, которые имеет смысл сравнивать.

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

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

Compare things. Stay cool.

#performance #cpp20
22.04.2025, 12:00
t.me/grokaemcpp/683
GR
Грокаем C++
8 216 подписчиков
43
2
1.6 k
Итоги конкурса

Мы долго ждали и, наконец, дождались. Вчера мы честно взяли генератор случайных чисел и нашли победителя и будущего счастливого обладателя книжки "С++ для начинающих" Герберта Шилдта. Ботов розыгрышей не хотелось использовать, без души все это. Надеюсь, вы доверяете нашей непредвзятости)

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

Ну а победителем стал Антон Конев давайте похлопаем ему👏👏👏. Антон, пиши в лс по ссылке в профиле канала, чтобы получить свою книжку.

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

Be lucky. Stay cool.
19.04.2025, 12:00
t.me/grokaemcpp/682
GR
Грокаем C++
8 216 подписчиков
40
16
1.5 k
Макросы
#новичкам

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

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

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

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

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

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

int x = 5;
int y = 10;

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

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

Result: 11
x: 6
y: 12

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

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

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

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

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

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

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

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

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

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

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

Don't be confusing. Stay cool.

#cppcore
18.04.2025, 12:00
t.me/grokaemcpp/681
GR
Грокаем C++
8 216 подписчиков
1
2
Капибарам нужна ваша помощь на T-CTF

У них лапки, и они не могут защитить код от уязвимостей. Выручите их на ИТ-соревновании от Т-Банка с шансом выиграть приз до 420 000 ₽.

Без навыков в ИТ тут не обойтись — задания рассчитаны на разработчиков, QA- и SRE-инженеров, аналитиков и других ИТ-специалистов уровня middle и senior.

Вот что вас ждет:

— Выберите Лигу Разработки или Лигу Безопасности по своим скиллам. Если участвуете впервые, можно потренироваться на демозаданиях.
— Соревнуйтесь один или в команде до 3 человек. Организаторы помогут найти команду, если нет своей.
— Подключайтесь онлайн или приходите офлайн — в ИТ-хаб Т-Банка в одном из 6 городов России.
— Решайте задания по спортивному хакингу — для этого у вас будет 36 часов.

Соревнование пройдет 19 и 20 апреля.

Попробуйте свои силы — успейте зарегистрироваться до 18 апреля.

Реклама. АО «ТБанк», лицензия ЦБ РФ № 2673, erid: 2Ranyo2pn9w
18.04.2025, 11:00
t.me/grokaemcpp/680
GR
Грокаем C++
8 216 подписчиков
30
17
1.7 k
Идеальная передача из лямбды
#опытным

Мутабельные лямбды позволили нам перемещать захваченные по значению объекты в сторонние функции:

auto callback = [message=get_message(), &scheduler]() mutable {
// some preparetions
scheduler.submit(std::move(message));
}

Ну а передача копии вообще никогда не была проблемой:

auto callback = [message=get_message(), &scheduler]() {
// some preparetions
scheduler.submit(message);
}

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

callback(); // retry(callback)
std::move(callback)(); // try-or-fail(rvalue)

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

Это все можно делать с помощью явного this и std::forward_like:

auto callback = [message=get_message(), &scheduler](this auto &&self) {
return scheduler.submit(std::forward_like(message));
};

Пара интересных наблюдений:

👉🏿 Если c std::forward мы могли идеально передать лишь объект замыкания, то с использованием std::forward_like мы можем кастить любой объект к точно такому же ссылочному типу, как и у объекта замыкания. Это позволяет мувать сообщение внутрь шедулера при использовании try-or-fail подхода вызова лямбды.

👉🏿 Можно заметить, что лямбда не мутабельная, хотя в ней возможно изменение объекта message. Это потому что при использовании явного this оператор() у замыкания по умолчанию мутабельный. Таков закон стандарт.

Из адекватных примеров явного this на этом все.

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

Be a major figure. Stay cool.

#template #cpp23
16.04.2025, 13:00
t.me/grokaemcpp/679
GR
Грокаем C++
8 216 подписчиков
31
4
1.8 k
Знакомьтесь — Илья Шишков, Опытный С++, разработчик с 20-летним опытом в IT-индустрии, эксперт по техническим интервью

Профессиональный путь 👇🏻👇🏻👇🏻

🖇11 лет в Яндексе: работал в командах Поиска, Браузера и Яндекс Еды
🖇Текущая позиция: разработчик в R&D команде СУБД Pangolin в СберТехе
🖇Образовательные проекты: создатель онлайн-специализации «Пояса по С++»

За время работы в Яндексе Илья провел более 250 алгоритмических интервью..🚀

В своем канале Илья делится 👇🏻

🩵личным опытом прохождения собеседований в различные компании

🩵практическими приемами для успешного прохождения алгоритмических интервью в Яндекс-подобных компаниях👌

Присоединяйтесь, чтобы получить инсайдерскую информацию о мире технических собеседований от человека с реальным опытом по обе стороны стола! ⚡️

https://t.me/+G7trZ7mhkh82MjNi
#реклама
16.04.2025, 11:00
t.me/grokaemcpp/678
GR
Грокаем C++
8 216 подписчиков
29
15
2.0 k
std::forward_like
#опытным

Сегодня рассмотрим функцию-хэлпер, которая поможет нам в рассмотрении одного из юзкейсов применимости deduction this. Их одновременное введение в 23-й стандарт логично, хэлпер дополняет и расширяет применимость deducing this.

Эта функция очень похожа на std::move и, особенно, на std::forward. Она потенциально аффектит только ссылочность типа и может добавлять константности.

Если std::forward объявлена так

template< class T >
constexpr T&& forward(std::remove_reference_t& t ) noexcept;

template< class T >
constexpr T&& forward(std::remove_reference_t&& t ) noexcept;

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

std::forward_like делает шаг вперед. Функция позволяет выполнять идеальную передачу данных на основе типа другого выражения.

template< class T, class U >
constexpr auto&& forward_like( U&& x ) noexcept;

Заметьте, что здесь 2 шаблонных параметра. Мы будем кастить x к ссылочному типу параметра Т.

Зачем вообще так делать?

Без deduction this особо незачем. Но вместе с ним мы можем на основе типа объекта, на котором вызывается метод, идеально передавать данные наружу.

Раньше это было возможно только если бы мы возвращали мемберы объекта. На С++20 это выглядело так:

return forward(obj).member;

Это работало с кучей ограничений. Но с появлением deducing this мы можем делать так:

struct adapter {
std::deque container;
auto&& operator[](this auto&& self, size_t i) {
return std::forward_like(self.container[i]);

} };

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

Более того, с помощью такого приема вообще в принципе появляется возможность использования оператора индексации на rvalue объектах. Если вернуть левую ссылку на содержимое временного объекта, то получим висячую ссылку и UB.

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

Follow the head. Stay cool.

#cpp23 #template
15.04.2025, 12:00
t.me/grokaemcpp/677
GR
Грокаем C++
8 216 подписчиков
106
38
1.8 k
Удобно сравниваем объекты
#опытным

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

struct Time {
int hours;
int minutes;

bool operator<(const Time& other) {
if ((hours < other.hours) || (hours == other.hours && minutes < other.minutes))
return true;
else
return false;
}
};

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

Однако есть элегантное решение этой проблемы. Можно использовать оператор сравнения для тупла. Он работает ровно, как мы и ожидаем в нашем случае. Сравнивает первые поля тупла, если они равны, то сравнивает вторые поля и так далее. В общем, сравнивает свои поля по [короткой схеме](https://t.me/grokaemcpp/187).

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

struct Time {
int hours;
int minutes;

bool operator<(const Time& other) {
return std::tie(hours, minutes) < std::tie(other.hours, other.minutes);
}
};

Теперь при добавлении поля класса, мы всего лишь должны добавить аргумент к std::tie:

struct Time {
int hours;
int minutes;
int seconds;

bool operator<(const Time& other) {
return std::tie(hours, minutes, seconds) < std::tie(other.hours, other.minutes, other.seconds);
}
};

Фишка рабочая и удобная. Так что пользуйтесь.

Use lifehacks. Stay cool.

#goodpractice
14.04.2025, 20:00
t.me/grokaemcpp/676
GR
Грокаем C++
8 216 подписчиков
20
7
1.9 k
Ремонтируем серверное железо, слушаем доклады про сердце роботов и работаем в мастерской

Когда-то Яндекс начинался с Поиска, а сегодня в компании полный in-house цикл производства железа: роботы-доставщики, автономный транспорт, умные устройства для дома и многое другое. И для тех, кто разделяет этот инженерный дух и не боится пробовать и создавать, Яндекс проводит Repair Cafe.
В программе:

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

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

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

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

Не откладывайте — регистрация открыта до 16 апреля.
14.04.2025, 17:00
t.me/grokaemcpp/675
GR
Грокаем C++
8 216 подписчиков
35
11
1.5 k
Обзор книжки #2

Мы тут недавно провели опрос на канале и выяснилось, что треть наших читателей считают себя новичками, отважно сражающимися с С++, но пока перевес сил не на их стороне. Возможно некоторые из вас только написали знаменитый "hello, world!".

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

Сегодня у нас на обзоре труд Герберта Шилдта "С++ для начинающих".

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

Чудесно, что книга Шилдта реализует именно такой подход. Первое издание вышло в 2002 году, немного после начала эры стандартного С++. Поэтому там просто физически речь не идет о новых стандартах, а только о самой базе С++ и его синтаксических конструкциях: система типов, операции над ними, if'ы, циклы, функции, ООП, шаблоны и исключения. Даже стандартной библиотеки почти не касаются(за исключением iostream, чтобы можно было взаимодействовать с программой).

Как можно говорить в начале книги про std::string, когда вы еще не прошли классы и динамическое выделение памяти? Как можно полноценно рассказывать про new, не пройдя ООП и исключения? Не, ну можно, так многие делают. Только при таком подходе в голове появляется много "черных ящиков", которые работают, но нет понимания как работают. Благодаря намеренному опущению упоминания стандартной библиотеки, текст книги очень последовательный.

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

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

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

Хотите быть успешным в своем пути обучения кунг-фу С++? У меня для вас хорошие новости. От издательства Питер я получил экземпляр этой замечательной книги в печатном виде и хочу его разыграть среди подписчиков и остальных любителей понюхать переплёт.

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

Победителя выберем рандомайзером.

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

Be lucky. Stay cool
11.04.2025, 12:13
t.me/grokaemcpp/674
GR
Грокаем C++
8 216 подписчиков
41
18
1.7 k
Mutable lambdas
#опытным

Лямбда выражения имеют одну интересную особенность. И эта особенность аффектит то, что можно делать внутри лямбды.

Простой пример:
int val = 0;
auto lambda1 = [&val]() { std::cout << ++val << std::endl; };
auto lambda2 = [val]() { std::cout << ++val << std::endl; };

Определяем 2 лямбды: в одну захватываем val по ссылке, во второй - по значению.

В чем здесь проблема?

А в том, что во втором случае мы получим ошибку компиляции.

На самом деле operator() у замыкания по умолчанию помечен как const метод, видимо чтобы его можно было вызывать на константных объектах замыкания. То есть это значит, что мы не можем изменять поля замыкания при вызове лямбды.

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

Под одним из прошлых постов разгорелась дискуссия по этому моменту. @KpacHoe_ympo в этом комменте упомянул, что в константных методах можно менять объекты, на которые ссылаются ссылки. Однако на мой вгляд(и подтверждений в стандарте я не нашел), что это не уб. Иначе в лямбду нельзя было бы захватывать ссылки вообще. Вряд ли весь захват по ссылке в лямбду держится на уб.

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

Но если нам очень нужно изменять захваченные по значению поля? На помощь приходит уже полюбившийся нам mutable. Лямбду можно пометить этим ключевым словом и тогда ее константный оператор() перестанет быть константным! Тогда мы можем как угодно изменять любые захваченные значения:

int val = 0;
auto lambda2 = [val]() mutable { std::cout << ++val << std::endl; };

Теперь все работает отлично.

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

Это может использоваться, например, для перемещения захваченных объектов в one-shot коллбэках:

auto callback = [message=get_message, &scheduler]() mutable {
// some preparetions
scheduler.submit(std::move(message));
}
SomeTask task{callback};
task.run();

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

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

Break the rules. Stay cool.

#cppcore
10.04.2025, 13:00
t.me/grokaemcpp/673
GR
Грокаем C++
8 216 подписчиков
10
7
1.9 k
📈Пытаетесь сделать код быстрее и экономичнее? Время научиться использовать плоские контейнеры в C++! На открытом уроке 15 апреля в 20:00 мск мы разберемся, что такое плоские контейнеры!

Что вас ждет:
- Описание плоских контейнеров в C++: какие они бывают и чем отличаются от стандартных.
- Реальные примеры, когда и почему плоские контейнеры — это ваша идеальная пара для эффективной работы с данными.
- Практическая часть: сравнение с традиционными контейнерами и примеры применения на реальных задачах.

Кому будет полезно:
• Программистам C++, работающим с большими объемами данных.
• Разработчикам высокопроизводительных приложений и игр.
• Инженерам, которым нужно минимизировать использование памяти и повысить эффективность.

👉Регистрируйтесь прямо сейчас и получите скидку на большое обучение «C++ Developer. Professional»: https://otus.pw/Hhpt/?erid=2W5zFG7iSBJ 

Реклама. ООО "ОТУС ОНЛАЙН-ОБРАЗОВАНИЕ". ИНН 9705100963.
10.04.2025, 10:00
t.me/grokaemcpp/672
GR
Грокаем C++
8 216 подписчиков
37
15
1.8 k
Mutable. А зачем?
#опытным

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

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

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

Тем более, что есть отличный способ, как вы можете заменить использование mutable.

Используйте умные указатели!

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

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

class ThreadSafeLogger {
explicit ThreadSafeLogger(std::shared_ptr metric) : call_count{metric} {}
std::shared_ptr call_count;
public:
void log(const std::string& msg) const {
call_count->Increment(); // Works fine
// logging
}
};

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

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

Don't use dirty hacks. Stay cool.

#cppcore
9.04.2025, 12:00
t.me/grokaemcpp/671
GR
Грокаем C++
8 216 подписчиков
40
28
1.5 k
Mutable
#новичкам

Это ключевое слово - один из самых темных уголков С++. И не то, чтобы очень важный уголок. Вы вполне ни разу могли с ним не сталкиваться. Но тем не менее по какой-то причине интервьютеры часто задают вопрос: "для чего предназначен mutable?". Ответит человек или нет особо никак не показывает его навыки программиста, лишь знание узких мест языка. Но раз такие вопросы задают, то вы должны быть готовы к ответу на них. Поэтому и родился этот пост.

Проблема вот в чем. Есть константный объект. Как вы знаете, поля константного объекта запрещено изменять. Но это довольно сильное ограничение. Да, не хотелось бы, чтобы инвариаты класса менялись. Однако помимо комплекса полей класса, представляющих собой инвариант класса, в объекте могут храниться другие поля, которые не входят в этот инвариант.

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

class ThreadSafeLogger {
std::atomic call_count = 0;
public:
void log(const std::string& msg) const {
call_count++; // Error! Changing class field in const member-function
// logging
}
};

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

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

Вот тут как раз, mutable Валера, настало твое время.

Помечая ключевым словом mutable поле класса вы разрешаете менять его в константных методах:

class ThreadSafeLogger {
mutable std::atomic call_count = 0;
public:
void log(const std::string& msg) const {
call_count++; // Works fine
// logging
}
};

Теперь мы можем изменять счетчик даже в константном методе.

В целом, на это все о функциональности этого ключевого слова.

В каких кейсах его можно применять?

✅ Сбор статистики вычислений в объекте. Пример выше как раз об этом. Для сбора статистики могут использоваться и более сложные сущности, типа оберток над известными системами мониторинга(аля prometheus).

✅ Если вы хотите потокобезопасные константные методы. Вам могут понадобиться мьютексы и/или кондвары, которые придется пометить mutable, чтобы их можно было использовать в константных методах.

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

class SomeComputingClass {
mutable std::unordered_map cache;
public:
Result compute(const Key& key) const {
if (!cache.contains(key)) {
cache[key] = /* actual computing */;
}
return cache[key];
}
};

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

Ну все, никакой гадкий интервьюер вас не завалит. Ваше кунг-фу теперь сильнее его кунг-фу.

Surprise your enemy. Stay cool.

#cppcore #interview
7.04.2025, 12:00
t.me/grokaemcpp/670
GR
Грокаем C++
8 216 подписчиков
36
21
1.4 k
Рекурсивные лямбды. Кейсы
#опытным

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

1️⃣ Начнем с очевидного. Где рекурсия, там всегда ошиваются какие-то древовидные структуры. Рекурсивные лямбды могут помочь сделать простые и не очень DFS обходы деревьев.

Можно обходить literaly деревья:

struct Leaf { };
struct Node;
using Tree = std::variant;
struct Node {
Tree left;
Tree right;
};

template
struct Visitor : Lambdas...
{
Visitor(Lambdas... lambdas) : Lambdas(std::forward(lambdas))...
{}
using Lambdas::operator()...;
};

int main()
{
Leaf l1;
Leaf l2;
Node nd{l1, l2};
Tree tree = &nd;
int num_leaves = std::visit(Visitor(
[](Leaf const&) { return 1; },
[](this const auto& self, Node* n) -> int {
return std::visit(self, n->left) + std::visit(self, n->right);
}
), tree);
}

Наше дерево хранит вариант ноды и листа. И мы можем с помощью паттерна overload обойти все веточки и посчитать листочки.

У вас может возникнуть вопрос: а как мы рекурсивно проходим все варианты лямбдой, которой предназначена только для нод?

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

Можно таким же образом попробовать обходить какие-нибудь джейсоны и другие подобные структуры.

2️⃣ С помощью рекурсивных лямбд можно обходить compile-time структруры, типа туплов(даже вложенных):

auto printTuple = [](const auto& tuple) constexpr {
auto impl = [](this const auto& self, const auto& t) constexpr {
if constexpr (idx < std::tuple_size_v>) {
std::cout << std::get(t) << " ";
self.template operator()(t); // Рекурсивный вызов
}
};
impl.template operator()<0>(tuple);
};

std::tuple tp{1, 2.0, "qwe"};
printTuple(tp);

// Output:
// 1 2 qwe

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


3️⃣ Обход вложенных директорий с помощью std::filesystem:

auto listFiles = [](const std::filesystem::path& dir) {
std::vector files;
auto traverse = [&](this const auto& self, const auto& path) {
for (const auto& entry : std::filesystem::directory_iterator(path)) {
if (entry.is_directory()) {
self(entry.path());
} else {
files.push_back(entry.path().string());
}
}
};
traverse(dir);
return files;
};

Ну тут вроде без пояснений все плюс-минус понятно.

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

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

Be useful. Stay cool.

#cppcore #cpp23 #template
4.04.2025, 12:03
t.me/grokaemcpp/669
GR
Грокаем C++
8 216 подписчиков
1
1
Устроиться в Яндекс невозможно

Думаю, что вы часто слышали об этом. Также говорят, что после трудоустройства народ там вязнет в бюрократии, перекладывает джейсоны и работает 25/8.

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

❗️ Отбор сложнее, чем в космонавты

Летать в огромной центрифуге, как Гагарин, не нужно. Отбор не космически сложный. Есть 2–4 этапа, и к каждому из них ребята из Яндекса помогают подготовиться.

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

❗️Продуктовая разработка без челленджей

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

Разрабы Яндекс Поиска строят систему, которая обрабатывает петабайты информации в день и применяет последние обновы ML.

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

❗️ Море легаси-кода

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

Вообще, если появилась новая и годная технология, Я первый будет ее тестить и внедрять в продакшен.

❗️ Гигантский энтерпрайз

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

Знаменитый принцип "работает, не трогай" там применяют не к коду, а к командам.

❗️ Бесконечные переработки и дежурства

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

В общем, мифологию оставьте древним грека, викингам и рентвшникам. А если вы всё ещё не верите, попробуйте сами пройти собес в Яндекс. Они как раз ищут бэкенд-разработчиков — вот вакансия.

Check it yourself. Stay cool.
#interview #commercial
4.04.2025, 11:03
t.me/grokaemcpp/668
GR
Грокаем C++
8 216 подписчиков
52
23
2.0 k
Рекурсивные лямбды. Идеал.
#опытным

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

Но по-настоящему рекурсивные лямбды появились только в С++23 с введением deducing this.

Если лямбда - это класс с методом operator(), значит мы внутрь этого метода можем передать явный this и тогда лямбда сможет вызвать сама себя!

auto factorial = [](this auto&& self, int n) {
if (n <= 1) return 1;
return n * self(n - 1);
};

У нас конечно в С++20 есть шаблонные лямбды, но здесь это немножко оверкилл. Поэтому используем автоматический вывод типа с помощью auto aka дженерик лямбду.

У нас была цель, мы к ней шли и, наконец, пришли. Ура, товарищи, ура!

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

Find true yourself. Stay cool.

#cppcore #cpp23
3.04.2025, 12:00
t.me/grokaemcpp/667
GR
Грокаем C++
8 216 подписчиков
54
15
1.7 k
Рекурсивные лямбды. Хакаем систему
#опытным

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

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

1️⃣ Вместо использования auto, явно приводим лямбду к std::function. Тогда компилятор будет знать точный тип функционального объекта и сможет его захватить в лямбду:

std::function factorial = [&factorial](int n) -> int {
return (n) ? n * factorial(n-1) : 1;
};

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

Поэтому не самый хороший способ.

2️⃣ Используем С++14 generic лямбды:

auto factorial = [](int n, auto&& factorial) {
if (n <= 1) return n;
return n * factorial(n - 1, factorial);
};
auto i = factorial(7, factorial);

Тут надо разобраться. Мы не могли захватывать лямбду в себя, потому что мы не знали ее тип. Сейчас мы тоже не знаем ее тип, но нам это и не нужно, потому что мы используем дженерик лямбду, которая под капотом превращается в замыкание с шаблонным оператором(). Благодаря cppinsides мы можем заглянуть под капот:

class __lambda_24_20
{
public:
template
inline /*constexpr */ auto operator()(int n, type_parameter_0_0 && factorial) const
{
if(n <= 1) {
return n;
}

return n * factorial(n - 1, factorial);
}

#ifdef INSIGHTS_USE_TEMPLATE
template<>
inline /*constexpr */ int operator()<__lambda_24_20 &>(int n, __lambda_24_20 & factorial) const
{
if(n <= 1) {
return n;
}

return n * factorial.operator()(n - 1, factorial);
}
#endif

};

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

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

auto factorial_impl = [](int n, auto&& factorial) {
if (n <= 1) return n;
return n * factorial(n - 1, factorial);
};
auto factorial = [&](int n) { return factorial_impl(n, factorial_impl); };
auto i = factorial(7);

Теперь не нужно передавать доп параметры.

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

using factorial_t = int(*)(int);
static factorial_t factorial = [](int n) {
if (n <= 1) return n;
return n * factorial(n - 1);
};
auto i = factorial(7);

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

Если у вас есть какие-то еще подобные приемы - пишите в комменты.

Но это все какие-то обходные пути. Хочется по-настоящему рекурсивные лямбды.

И их есть у меня!

Об этом в следующий раз.

Always find a way out. Stay cool.

#template #cppcore #cpp11 #cpp14
2.04.2025, 12:00
t.me/grokaemcpp/666
GR
Грокаем C++
8 216 подписчиков
1
Чемпионат для подростков по 14 направлениям от «Алабуга Политех»☺️

Программирование и Битва роботов, Экономика и Юриспруденция, 3D моделирование и Английский язык и многое другое, чтобы каждый нашел свою дисциплину.

Для участия тебе нужно☺️
☺️Оставь заявку на сайте😀
☺️Пройди заочный этап на HR-платформе: Business Cats до 1,0 по «Общению» и «Аналитике» для оплаты дороги туда и обратно😀
☺️Приезжай на чемпионат😀

Мы предлагаем тебе☺️
☺️Общий призовой фонд турнира составляет 450 000 рублей😀
☺️Проживание и дорога бесплатно😀
☺️Возможность поступить в «Алабуга Политех»😀

Участвуй в турнире от образовательного центра мирового уровня и получай призы и преимущество в поступлении☺️
2.04.2025, 12:00
t.me/grokaemcpp/665
GR
Грокаем C++
8 216 подписчиков
31
6
1.9 k
Чемпионат для подростков по 14 направлениям от «Алабуга Политех»☺️

Программирование и Битва роботов, Экономика и Юриспруденция, 3D моделирование и Английский язык и многое другое, чтобы каждый нашел свою дисциплину.

Для участия тебе нужно☺️
☺️Оставь заявку на сайте😀
☺️Пройди заочный этап на HR-платформе: Business Cats до 1,0 по «Общению» и «Аналитике» для оплаты дороги туда и обратно😀
☺️Приезжай на чемпионат😀

Мы предлагаем тебе☺️
☺️Общий призовой фонд турнира составляет 450 000 рублей😀
☺️Проживание и дорога бесплатно😀
☺️Возможность поступить в «Алабуга Политех»😀

Участвуй в турнире от образовательного центра мирового уровня и получай призы и преимущество в поступлении☺️
2.04.2025, 11:00
t.me/grokaemcpp/664
GR
Грокаем C++
8 216 подписчиков
348
37
2.4 k
Больше нет сил

Ребят, плохие новости. У нас больше нет возможности вести канал. Денис лидит 2 команды и строит себе сам дачу, а я пилю курс на Яндекс Практикум и подрабатываю курьером в самокате. В одного уже все руки в дырках от гвоздей, у второго ноги сточились. * Если у кого есть хорошая бригада строителей в Нижнем Новгороде или остеопат - пишите в личку.

В общем, на канал времени не остается совсем. Чем дольше мы его пилим, тем больше понимаем, что не вывозим. Это нормально, это жизнь. Я вон 20 лет лет трехметровым был, но плита жизни придавила и уполовинила. А теперь я маленький и очень толстый.

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

Но есть и радостные новости - тому, кто наберет больше всех сердечек на любом своем комментрии под этим постом, мы передадим права на владение каналом. Уверен, что у вам небезразлично будущее Грокаем С++, поэтому лайкайте достойных людей.

На этом все. Спасибо за этот путь, он был бесценен....

Stay alert. Stay cool.
1.04.2025, 12:00
t.me/grokaemcpp/663
GR
Грокаем C++
8 216 подписчиков
44
16
2.3 k
Рекурсивные лямбды. Невозможно?
#новичкам

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

int main() {
auto factorial = [&factorial](int n) {
return n > 1 ? n * factorial(n - 1) : 1;
};
return factorial(5);
}

Вот мы пытаемся с помощью лямбды посчитать факториал числа. В чем здесь проблема?

Фактически данный код значит, что компилятор должен сгенерировать замыкание и в поля этого замыкания поместить ссылку на само замыкание:

class lkdlkhbahbahkl_danfaksdf_lamba
{
public:
int operator()(int n) const
{
return n > 1 ? n * factorial(n - 1) : 1;
}
private:
???? factorial;
};

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

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

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

Don't close on yourself. Stay cool.

#cppcore
31.03.2025, 20:30
t.me/grokaemcpp/662
GR
Грокаем C++
8 216 подписчиков
44
19
2.2 k
Неочевидное преимущество шаблонов
#новичкам

Давайте немного разбавим рассказ о фичах 23-го стандарта чем-нибудь более приземленным

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

Можно и обойтись. Возьмем хрестоматийный пример std::qsort. Это скоммунизденная реализация сишной стандартной функции qsort. Сигнатура у нее такая:

void qsort( void *ptr, std::size_t count, std::size_t size, /* c-compare-pred */* comp );
extern "C" using /* c-compare-pred */ = int(const void*, const void*);
extern "C++" using /* compare-pred */ = int(const void*, const void*);

Как видите, здесь много void * указателей на void. В том числе с помощью него достигается полиморфизм в С(есть еще макросы, но не будем о них).

Как это работает?

Функция qsort спроектирована так, чтобы с ее помощью можно было сортировать любые POD типы. Но не хочется как-то пеерегружать функцию сортировки для всех потенциальных типов. Поэтому придумали обход. Передавать void указатель, чтобы мочь обрабатывать данные любых типов. Но void* - это нетипизированный указатель, поэтому фунции нужно знать размер типа данных, которые она сортирует, и количество данных. А также предикат сравнения.

Вот тут немного поподробнее. Предикат для интов может выглядеть примерно так:

[](const void* x, const void* y)
{
const int arg1 = *static_cast(x);
const int arg2 = *static_cast(y);
const auto cmp = arg1 <=> arg2;
if (cmp < 0)
return -1;
if (cmp > 0)
return 1;
return 0;
}

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

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

Тип шаблонных параметров, напротив, известен на этапе компиляции.

template< class RandomIt, class Compare >
void sort( RandomIt first, RandomIt last, Compare comp );

Значит код компаратора шаблонной функции может быть включен в код сортировки. Именно поэтому функция std::sort намного быстрее std::qsort при включенных оптимизациях(а без них примерно одинаково)

Казалось бы плюсы, а быстрее сишки. И такое бывает, когда используешь шаблоны.

Use advanced technics. Stay cool.

#template #goodoldc #goodpractice #compiler
28.03.2025, 13:00
t.me/grokaemcpp/661
GR
Грокаем C++
8 216 подписчиков
12
4
1.6 k
💼Хотите стать востребованным разработчиком на C++?

C++ — это язык, который стоит за самыми мощными приложениями, играми и программами для «железа». Без него никуда. Но вот вопрос: готовы ли вы выйти на уровень Middle Developer за 12 месяцев? 🤔💪

💡На курсе от OTUS вы:

— Изучите C++ с нуля до продвинутого уровня.
— Освоите работу с многопоточностью, памятью, STL и Boost.
— Создадите проекты, которые впечатлят на собеседовании.

❓Что дальше?

Сможете претендовать на позиции Junior+ и Middle.
Получите навыки работы с реальными кейсами и библиотеками.
Овладеете CI/CD, NoSQL и асинхронным программированием.

👉Успейте записаться до старта курса и получите скидку до 15% по промокоду CPP_03: https://otus.pw/ABA0/

Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
28.03.2025, 10:00
t.me/grokaemcpp/660
GR
Грокаем C++
8 216 подписчиков
42
24
4.2 k
Deducing this и CRTP
#опытным

У deducing this есть одна особенность. При обычном наследовании(без виртуальных функций) методы родительского класса знают про точный тип объектов наследников, которые вызывают метод:

struct Machine {
template
void print(this Self&& self) {
self.print_name();
}
};

struct Car : public Machine {
std::string name;
void print_name() {
std::cout << "Car\n";
}
};

Car{}.print(); // Выведется "Car"

Вам ничего это не напоминает? CRTP конечно.

Этот паттерн и используется в принципе, чтобы родители имели доступ к точному типу объекта наследника:

template
struct add_postfix_increment {
Derived operator++(int) {
auto& self = static_cast(*this);

Derived tmp(self);
++self;
return tmp;
}
};

struct some_type : add_postfix_increment {
// Prefix increment, which the postfix one is implemented in terms of
some_type& operator++();
};

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

Но с появлением deducing this мы можем избежать рождения этого странного отпрыска наследования и шаблонов:

struct add_postfix_increment {
template
auto operator++(this Self&& self, int) {
auto tmp = self;
++self;
return tmp;
}
};

struct some_type : add_postfix_increment {
// Prefix increment, which the postfix one is implemented in terms of
some_type& operator++();
};

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

Все красиво, эстетично и не ломает голову людям, мало работающим с шаблонами.

Make things more elegant. Stay cool.

#template #cpp23
27.03.2025, 12:00
t.me/grokaemcpp/659
GR
Грокаем C++
8 216 подписчиков
39
28
3.5 k
Передача объекта в методы по значению
#опытным

Небольшие типы данных, особенно до 8 байт длиной, быстрее передавать в методы или возвращать из методов по значению.

С помощью deducing this мы можем вызывать методы не для ссылки(под капотом которой указатель), а для значения объекта.

Семантика будет ровно такая, как вы ожидаете. Объект скопируется внутрь метода и все операции будут происходить над копией.

Давайте посмотрим на пример:

struct just_a_little_guy {
int how_small;
int uwu();
};

int main() {
just_a_little_guy tiny_tim{42};
return tiny_tim.uwu();
}

Здесь используется старая нотация с неявным this.

Посмотрим, какой код может нам выдать компилятор:

sub rsp, 40
lea rcx, QWORD PTR tiny_tim$[rsp]
mov DWORD PTR tiny_tim$[rsp], 42
call int just_a_little_guy::uwu(void)
add rsp, 40
ret 0

Пройдемся по строчкам и посмотрим, что тут происходит:

- первая строчка аллоцирует 40 байт на стеке. 4 байта для объекта tiny_tim, 32 байта теневого пространства для метода uwu и 4 байта паддинга.
- инструкция lea загружает адрес tiny_tim в регистр rcx, в котором метод uwu ожидает свой неявный параметр.
- mov помещает число 42 в поле объекта tiny_tim.
- вызываем функцию-метод uwu
- наконец деаллоцируем памяти и выходим из main

А теперь применим deducing this с параметром по значению и посмотрим на ассемблер:

struct just_a_little_guy {
int how_small;
int uwu(this just_a_little_guy);
};

Ассемблер:

mov ecx, 42
jmp static int just_a_little_guy::uwu(this just_a_little_guy)

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

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

Optimize yourself. Stay cool.

#cpp23 #optimization #compiler
26.03.2025, 12:00
t.me/grokaemcpp/658
GR
Грокаем C++
8 216 подписчиков
26
3
1.5 k
Возможности для молодых людей в «Алабуге», Республике Татарстан

В особой экономической зоне «Алабуга» активно развивается лидерская программа «100 Лидеров». В ней могут поучаствовать молодые специалисты от 19 до 26 лет.

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

Питание и проживание за счет компании.

Работа в «Алабуге» - это зарплата от 110 до 240 тысяч рублей и участие в реализации проектов мирового уровня.

Следующий поток - с 5 по 9 апреля!
Заявку можно подать на сайте
26.03.2025, 11:00
t.me/grokaemcpp/657
GR
Грокаем C++
8 216 подписчиков
42
21
1.6 k
Deducing this
#опытным

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

И в С++23 именно этот момент и изменили. Теперь мы можем явно указывать тип объекта, на который указывает this. И это по сути полностью заменяет cv и ref квалификацию методов. Выглядит это так:

struct cat {
std::string name;

void print_name(this cat& self) {
std::cout << name; //invalid
std::cout << this->name; //also invalid
std::cout << self.name; //all good
}
void print_name(this const cat& self) {
std::cout << self.name;
}
void print_name(this cat&& self) {
std::cout << self.name;
}
void print_name(this const cat&& self) {
std::cout << self.name;
}
};

Особенности:

👉🏿 Мы явно указываем параметр this.

👉🏿 Явно указываем тип объекта и его квалификаторы.

👉🏿 Считайте, что это статические методы, внутрь которых передали объект того же класса. Синтаксис доступа в полям соотвествующий: нельзя упоминать this, нельзя неявно обращаться к членам класса, только через имя параметра.

👉🏿 Поэтому нельзя такие методы объявлять статическими, ибо невозможно будет различить вызов статического и нестатического метода с одинаковым именем.

Теперь у нас есть все инструменты и мы можем сделать шаблонный this. Давайте посмотрим на обновленный метод value класса optional:

template
struct optional {
// One version of value which works for everything
template
constexpr auto&& value(this Self&& self) {
if (self.has_value()) {
return std::forward(self).m_value;
}
throw bad_optional_access();
}
};

Вот это бэнгер! Мы деквадруплицировали код!

Здесь мы используем шаблонный параметр Self с универсальной ссылкой. В этом случае параметр self будет в точности повторять тип объекта, на котором вызван метод. И для правильной передачи значения наружу мы используем идеальную передачу и std::forward + auto&& возвращаемое значение, которое тоже будет соответствовать cv+ref типу объекта.

Настоящая магия, причем вне хогвартса!

Имена Self и self использовать необязательно, это отсылки к питону и первом параметру методов классов self.

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

Simplify your life. Stay cool.

#cpp23 #template
25.03.2025, 12:00
t.me/grokaemcpp/656
GR
Грокаем C++
8 216 подписчиков
27
10
1.5 k
Проблемы ref-qualified методов
#опытным

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

Но один из примеров в том посте выбивается из общей массы. Еще раз посмотрим на него:

template
class optional {
// version of value for non-const lvalues
constexpr T& value() & {
if (has_value()) {
return this->m_value;
}
throw bad_optional_access();
}

// version of value for const lvalues
constexpr T const& value() const& {
if (has_value()) {
return this->m_value;
}
throw bad_optional_access();
}

// version of value for non-const rvalues... are you bored yet?
constexpr T&& value() && {
if (has_value()) {
return std::move(this->m_value);
}
throw bad_optional_access();
}

// you sure are by this point
constexpr T const&& value() const&& {
if (has_value()) {
return std::move(this->m_value);
}
throw bad_optional_access();
}
// ...
};

Это примерно то, как метод value класса std::variant был введен в стандарт С++17.
Мягко говоря, есть ощущение, что код дублируется. А если не считать мува, то вообще квадруплицируется.

Это вот стандартная штука, когда функции отличаются немного и их нельзя объединить в одну.

В таких случаях обычно помогают шаблоны. А учитывая, что у нас для левых ссылок нет мува, а для правых - есть, очень сильно напрашиваются универсальные ссылки и шаблонный std::forward.

Но тут шаблон вообще никак не вписывается. Методы же не принимают даже никаких аргументов. Какой шаблонный параметр сюда вообще вписывается?

Ну вообще говоря, методы принимают неявный аргумент this....

To be continued.

Intrigue people. Stay cool.

#cppcore
24.03.2025, 13:00
t.me/grokaemcpp/655
GR
Грокаем C++
8 216 подписчиков
13
12
1.9 k
⚡️Асинхронность без сложных потоков? В C++20 это возможно. Корутины позволяют выполнять задачи параллельно без создания лишних потоков, экономя ресурсы и упрощая код.

На открытом вебинаре 27 марта в 20:00 мск разберём, как co_await и co_yield работают в современных C++-проектах, где применяются в реальных задачах и почему это важно для высоконагруженных систем. Разберём практические примеры из сетевого программирования и обработки данных.

Освойте новую парадигму асинхронности, избавьтесь от проблем с потоками и сделайте свой код проще и быстрее. Вы научитесь внедрять корутины в свои проекты, разберётесь в новшествах C++20/23 и сможете использовать их в продакшене.

👉Регистрируйтесь и получите скидку на большое обучение «C++ Developer. Professional»: https://otus.pw/qwii/?erid=2W5zFGdMrLG 

Реклама. ООО "ОТУС ОНЛАЙН-ОБРАЗОВАНИЕ". ИНН 9705100963.
24.03.2025, 10:00
t.me/grokaemcpp/654
GR
Грокаем C++
8 216 подписчиков
148
25
2.1 k
Как думаете, нужен ли С++ стандартный сборщик мусора?
23.03.2025, 12:00
t.me/grokaemcpp/653
GR
Грокаем C++
8 216 подписчиков
40
22
1.4 k
auto аргументы функций
#опытным

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

До С++14 у нас были только шаблонные параметры в функциях и лямбда выражения, без возможности передавать в них значения разных типов

Начиная с С++14, мы можем объявлять параметры лямбда выражения auto и передавать туда значения разных типов:

auto print = [](auto& x){std::cout << x << std::endl;};
print(42);
print(3.14);

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

У обычных функции, тем не менее, так и остались обычные шаблонные параметры.

Но! Начиная с С++20, параметры обычных функций можно также объявлять auto:

void sum(auto a, auto b)
{
    auto result = a + b;
    std::cout << a << " + " << b << " = " << result << std::endl;
}

sum(1, 3);
sum(3.14, 42);
sum(std::string("123"), std::string("456));
// OUTPUT:
// 1 + 3 = 4
// 3.14 + 42 = 45.14
// 123 + 456 = 123456

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

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

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

Кому нравится, тот обрадуется и будет пользоваться. Кому не нравится, может писать в стиле С++03 и все будет у него прекрасно.

Hide unused details. Stay cool.

#cpp11 #cpp14 #cpp20 #template
21.03.2025, 12:00
t.me/grokaemcpp/652
GR
Грокаем C++
8 216 подписчиков
17
1
1.4 k
IMG_1979.MP4
🔥 Меняете мир вокруг и IT-сферу?

Приходите на Яндекс Dev Day&Night — конференцию для мобильных и бэкенд-разработчиков, продактов и аналитиков!
⏰ Когда? 19 апреля
📍 Где? Москва

💡 Что вас ждёт?
– 5 треков докладов от экспертов
– Полезные знакомства и общение
– Тусовка до 2 ночи с коктейлями, диджеями и дискуссиями не под запись!

В прошлом году нас было 670 участников, а в этом будет ещё больше. Такое точно нельзя пропускать!

👉 Регистрируйтесь прямо сейчас и зовите друзей.

Реклама. ООО «Яндекс.Такси» ИНН: 7704340310 Erid: 2VfnxvpbsBN
21.03.2025, 11:00
t.me/grokaemcpp/651
GR
Грокаем C++
8 216 подписчиков
42
14
1.7 k
Перегружаем деструктор
#новичкам

Мы знаем, что методы класса можно перегружать, как обычные фукнции. Мы также поняли, что можно перегружать методы так, чтобы они отдельно работали для rvalue и lvalue ссылок. Можно даже перегружать конструкторы класса, чтобы они создавали объект из разных данных.

Но можно ли перегружать деструктор класса?

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

По поводу дополнительных параметров деструктора.

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

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

Ну а вообще. Задача деструктора - освободить ресурсы класса. Для конкретного класса набор его ресурсов определен на этапе компиляции. И есть всего один способ корректно освободить ресурс: вызвать delete, закрыть сокет или вызвать деструктор. И этот способ определен самим ресурсом.

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

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

Have a deeper understanding. Stay cool.

#memory #cppcore
20.03.2025, 12:00
t.me/grokaemcpp/650
GR
Грокаем C++
8 216 подписчиков
29
13
1.4 k
Кейсы применения ref-qualified методов
#опытным

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

✅ Разработка библиотек. Довольно очевидно, что разработчикам всяких библиотек нужно учитывать примерно все сценарии использования их классов. Пользователи(безумные) могут скастить объект к константной правой ссылке и методы класса должны работать корректно. Тут очень важно, чтобы тип возвращаемого значения методов соответствовал типу объекта. Пример:

template
class optional {

constexpr T& value() & {
if (has_value()) {
return this->m_value;
}
throw bad_optional_access();
}

constexpr T const& value() const& {
if (has_value()) {
return this->m_value;
}
throw bad_optional_access();
}

constexpr T&& value() && {
if (has_value()) {
return std::move(this->m_value);
}
throw bad_optional_access();
}

constexpr T const&& value() const&& {
if (has_value()) {
return std::move(this->m_value);
}
throw bad_optional_access();
}
// ...
};

Если объект временный, то возвращаем правую ссылку на мувнутый ресурс. Если объект lvalue, то возвращаем обычную ссылку.

✅ Форсить ограничения на методы. Если у вас методы возвращают левые ссылки(константные и неконстантные), то неплохо бы их пометить &, чтобы эти методы могли вызываться только у именованных объектов. Ведь если получить ссылку на внутренний ресурс временного объекта, то временный объект уничтожится, а вы останетесь с разбитым корытом висячей ссылкой. Спасибо @d7d1cd за кейс)

struct Vector {
int & operator[](size_t index) & { // notice & after arguments
return vec[index];
}
std::vector vec;
};

Vector v;
v.vec = {1, 2, 3, 4};
v[1]; // ok
Vector{{1, 2, 3, 4}}[1]; // compile error

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


✅ Оптимизации. Иногда для определенных ссылочных типов мы можем оптимизировать какой-то метод. Например, в С++23 ввели rvalue reference перегрузку для метода substr класса std::basic_string. Мы знаем, что метод substr формирует новую строку, копируя туда рэндж из оригинальной строки. С++23 теперь сделал так, чтобы при вызове метода substr у правых ссылок объект подстроки тырил данные у оригинальной строки и фактически формировался из ее внутреннего буфера. Более подробно можно почитать в пропоузале.

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

struct Vector {
int operator[](size_t index) && { // notice & after arguments
return vec[index];
}
std::vector vec;
};

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

Так что ref-qualified методы - это прекрасный инструмент тонкой настройки в руках профессионалов.

Be useful. Stay cool.

#cppcore #optimization #cpp23
19.03.2025, 13:00
t.me/grokaemcpp/649
GR
Грокаем C++
8 216 подписчиков
13
3
1.7 k
😮‍💨Устали вручную разруливать зависимости в C++ проектах? Время автоматизировать процесс! 🕒💻

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

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

Спикер Денис Злобин — старший инженер-программист в Astra Linux, опытный наставник разработчиков.

➡️Встречаемся 20 марта в 20:00 мск, разберем всё на практике! Участники получат скидку на большое обучение по разработке на С++.

Регистрация: https://otus.pw/X2us/

Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru
19.03.2025, 10:00
t.me/grokaemcpp/648
GR
Грокаем C++
8 216 подписчиков
8
2
1.8 k
Третий пошел

struct SomeClass {
// void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
// void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};

int main() {
SomeClass lvalue;
lvalue.foo();
const_cast(lvalue).foo();
}
18.03.2025, 12:00
t.me/grokaemcpp/646
GR
Грокаем C++
8 216 подписчиков
9
3
1.6 k
Второй пошел

struct SomeClass {
// void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1

void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2

void foo() && = delete; //3

void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};

int main() {
SomeClass lvalue;
lvalue.foo();
SomeClass{}.foo();
}
18.03.2025, 12:00
t.me/grokaemcpp/644
GR
Грокаем C++
8 216 подписчиков
14
2
1.6 k
Мини-квизы

Сегодня будет вторая и последняя пачка мини-квизов на тему перегрузки методов cv-ref квалификаторами.

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

Также по прежнему в код за кадром подключаются все необходимые хэдэры, а программа собирается на 17-м стандарте. А в ответах квиза перенос строки обозначается через "\n".

Вроде с дикслеймером все.

Первый пошел:
struct SomeClass {
void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
// void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
// void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};

int main() {
SomeClass lvalue;
lvalue.foo();
SomeClass{}.foo();
}
18.03.2025, 12:00
t.me/grokaemcpp/642
GR
Грокаем C++
8 216 подписчиков
28
6
1.6 k
Ответы на мини-квизы

struct SomeClass {
// void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};

int main() {
SomeClass lvalue;
lvalue.foo();
SomeClass{}.foo();
}
Здесь вызовутся методы 2 и 3 по порядку. За неимением неконстантной перегрузки для левых ссылок, остается только константная перегрузка для первого вызова.Во втором случае rvalue reference может приводиться к константной левой ссылке, но в этот раз есть более подходящие кандидаты на перегрузку. И самым подходящим будет 3 метод.

struct SomeClass {
// void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
// void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};

int main() {
SomeClass lvalue;
lvalue.foo();
std::move(lvalue).foo();
}

Вызовутся методы 2 и 4 по порядку. rvalue reference может приводиться к константной левой ссылке, но также может приводиться к const rvalue ref. Второе преобразование достигается меньшими усилиями, поэтому вызовется 4 метод.

struct SomeClass {
// void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
// void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
void foo() const && = delete; //4
};

int main() {
SomeClass lvalue;
lvalue.foo();
SomeClass{}.foo();
}

Здесь будет ошибка компиляции на втором вызове. Для него подходили бы 3, 4 и 2 перегрузки в порядке приоритета. Но 3 нет, а следующая наиболее подходящая перегрузка удалена. Удаленные функции участвуют в разрешении перегрузки, поэтому компилятор решит, что мы хотим вызвать удаленную форму, и запретит нам это делать.
17.03.2025, 18:00
t.me/grokaemcpp/641
GR
Грокаем C++
8 216 подписчиков
1
Ответ: здесь будет ошибка компиляции на втором вызове. Для него подходили бы 3, 4 и 2 перегрузки в порядке приоритета. Но 3 нет, а следующая наиболее подходящая перегрузка удалена. Удаленные функции участвуют в разрешении перегрузки, поэтому компилятор решит, что мы хотим вызвать удаленную форму, и запретит нам это делать.
17.03.2025, 13:00
t.me/grokaemcpp/640
GR
Грокаем C++
8 216 подписчиков
8
5
1.8 k
Третий пошел
struct SomeClass {
// void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
// void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
void foo() const && = delete; //4
};

int main() {
SomeClass lvalue;
lvalue.foo();
SomeClass{}.foo();
}
17.03.2025, 13:00
t.me/grokaemcpp/638
GR
Грокаем C++
8 216 подписчиков
1
Ответ:
Вызовутся методы 2 и 4 по порядку. rvalue reference может приводиться к константной левой ссылке, но также может приводиться к const rvalue ref. Второе преобразование достигается меньшими усилиями, поэтому вызовется 4 метод.
17.03.2025, 13:00
t.me/grokaemcpp/637
GR
Грокаем C++
8 216 подписчиков
1
Ответ: Вызовутся методы 2 и 3 по порядку. За неимением неконстантной перегрузки для левых ссылок, остается только константная перегрузка для первого вызова.Во втором случае rvalue reference может приводиться к константной левой ссылке, но в этот раз есть более подходящие кандидаты на перегрузку. И самым подходящим будет 3 метод.
17.03.2025, 13:00
t.me/grokaemcpp/634
GR
Грокаем C++
8 216 подписчиков
6
4
1.7 k
Второй пошел
struct SomeClass {
// void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
// void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};

int main() {
SomeClass lvalue;
lvalue.foo();
std::move(lvalue).foo();
}
17.03.2025, 13:00
t.me/grokaemcpp/635
GR
Грокаем C++
8 216 подписчиков
12
7
1.8 k
Мини-квизы

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

В режиме опроса сложно указывать варианты с переносом строк. Поэтому на месте, где должен быть перенос буду ставить"\n".

Ответы выложу вечером.

Первый пошел:
struct SomeClass {
// void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};

int main() {
SomeClass lvalue;
lvalue.foo();
SomeClass{}.foo();
}
17.03.2025, 13:00
t.me/grokaemcpp/632
GR
Грокаем C++
8 216 подписчиков
13
3
2.0 k
sysconf 2025 — конференция по системному программированию от создателей C++ Russia и DevOops

📅 22 марта в Москве + онлайн

Вас ждут 22 спикера, 19 докладов и Lightning Talks. Разберемся на реальных кейсах, как устроены многопоточные рантаймы, компиляторы и низкоуровневые оптимизации. Подробнее — в расписании.

🎟 Билеты уже на сайте. Если оплачиваете самостоятельно — промокод GROKAEMCPP дает скидку 15%.

Реклама. ООО «Джуг Ру Груп». ИНН 7801341446
17.03.2025, 10:00
t.me/grokaemcpp/631
GR
Грокаем C++
8 216 подписчиков
24
18
1.5 k
Совмещаем ссылочные и cv квалификаторы методов

Далеко не всегда очевидно, какая именно функция является лучшим кандидатом для перегрузки в том или ином случае. Да, когда это только & или &&, то все довольно просто. Но что получается, когда мы добавим константность методам?

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

struct SomeClass {
void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};

Дело в том, что все правые ссылки могут каститься к const lvalue reference, а левые к правым - ни при каких обстоятельствах. Неконстантные типы могут каститься к константным. И никак наоборот.

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

SomeClass lvalue;
const SomeClass const_lvalue;

lvalue.foo();
const_lvalue.foo();

В случае lvalue.foo() вызываем перегрузку для неконстантной левой ссылки. Левые ссылки не могут приводиться к правым. Поэтому методы под номерами 3 и 4 не подходят.
Неконстантные типы могут приводиться к константным. Поэтому нам подходят и 1, и 2 методы. Однако для вызова 2 придется сделать шажок - добавить константности, а для вызова первого - ничего. Поэтому выбирается первая перегрузка.

В случае const_lvalue.foo() вызываем перегрузку для константной левой ссылки. 3 и 4 также откидываем по тем же причинам. Однако в этот раз нам подходит лишь 2 перегрузка, так как константный тип не может быть приведен к неконстантному.

Итого вывод получится такой:

Call on lvalue reference
Call on const lvalue reference

Для rvalue ссылки нехитрыми рассуждениями можно прийти к правильному ответу о вызываемой перегрузке

SomeClass{}.foo();
// OUTPUT
// Call on rvalue reference

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

Choose the right way. Stay cool.

#cppcore
14.03.2025, 12:00
t.me/grokaemcpp/629
GR
Грокаем C++
8 216 подписчиков
3
4
1.5 k
Открытый вебинар «Архитектурные решения в Backend-разработке»

📚На вебинаре вы узнаете:
1. Как выбрать архитектурный стиль в зависимости от требований к производительности, масштабируемости и отказоустойчивости.

2. Микросервисы vs монолит: плюсы и минусы, примеры смены подходов. 

3. Событийно-ориентированная архитектура: когда и как ее использовать, основные принципы и инструменты.

4. CQRS и источник событий: как управлять данными в сложных условиях.

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

6. Ключевые ошибки при проектировании конструкции и как их избежать.

👨‍💻Кому будет полезно?
+ Разработчикам Backend
+ Разработчикам FullStack
+ Системным аналитикам

⏰ 18 марта в 20:00 (мск).
🆓Бесплатно. Вебинар в рамках курса «Software Architect»

👉Записывайтесь:https://otus.pw/qoYh4/?erid=2W5zFHYn66t

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

#реклама
О рекламодателе
14.03.2025, 11:00
t.me/grokaemcpp/628
GR
Грокаем C++
8 216 подписчиков
40
31
1.8 k
ref-qualified методы
#опытным

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

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

struct SomeClass {
void foo() {std::cout << "Non-const member function" << std::endl;}
void foo() const {std::cout << "Const member function" << std::endl;}
};

SomeClass nonconst_obj;
const SomeClass const_obj;
nonconst_obj.foo();
const_obj.foo();

// OUTPUT
// Non-const member function
// Const member function

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

В примере видно что у константного объекта вызывается константная перегрузка.

По аналогии с cv-квалификаторами методов начиная с С++11 существуют ref-квалификаторы. Мы можем перегрузить метод так, чтобы он мог раздельно обрабатывать левые и правые ссылки.

struct SomeClass {
void foo() & {std::cout << "Call on lvalue reference" << std::endl;}
void foo() && {std::cout << "Call on rvalue reference" << std::endl;}
};

SomeClass lvalue;
lvalue.foo();
SomeClass{}.foo();

// OUTPUT
// Call on lvalue reference
// Call on rvalue reference

Обратим внимание на сигнатуру методов. Метки ссылочных квалификаторов ожидаемо принимают форму одного и двух амперсандов, по аналогии с типами данных левых и правых сслылок соотвественно. Располагаются они после скобок с аргументами метода.

Работают они примерно также, как вы и ожидаете. lvalue-ref перегрузка вызывается на именованном объекте, rvalue-ref перегрузка - на временном.

Зачем это придумано?

Здесь на самом деле большие параллели с cv-квалификацией методов. Допустим, у вас класс - это какая-то коллекция. И вы хотите давать пользователям доступ к элементам этой коллекции через оператор[]. Для неконстантных объектов удобно возвращать ссылку. А вот для константных возвращение ссылки - потенциальное нарушение неизменяемости объекта. Поэтому в таких случаях константный оператор может возвращать элемент по значению или по константной ссылке.

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

Подробнее об этом чуде-юде будем разбираться в следующих постах.

Stay flexible. Stay cool.

#cpp11 #design
13.03.2025, 12:00
t.me/grokaemcpp/627
GR
Грокаем C++
8 216 подписчиков
45
33
1.9 k
std::array
#новичкам

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

template
struct array
{
T& operator[](size_t index) {
return _data[index];
}
T& front() {
return _data[0];
}
T& back() {
return _data[N-1];
}
T* data() {
return _data;
}
constexpr size_t size() const {
return N;
}
constexpr bool empty() const {
return N == 0;
}
// еще const версии перечисленных методов и некоторые другие методы и алиасы типов

T _data[N];
};

За счет использования шаблоного типа нижележащего массива std::array может работать с любыми встроенными и кастомными типами.

А за счет нетипового шаблонного аргумента N, std::array знает количество элементов, которое в нем находится, еще на этапе компиляции!. И не нужно ничего вычислять! Достаточно вызвать метод size(), который буквально constexpr.

std::array arr{1, 2, 3};
static_assert(arr.size() == 3); // здесь не упадем

Обычно удобство абстракций идет вместе с платой за это удобство. Но это не тот случай. За счет того, что все методы std::array буквально занимают одну строчку, компилятору очень удобно инлайнить их код в caller'ов. Это приводит к тому, что низкоуровневый ассемблерный код при работе с C-style массивами и std::array практически всегда идентичен.

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

template
std::array double_elements(std::array& array) {
std::array result = array;
for (auto& elem: result)
elem = elem * 2;
return result;
}

Если мы создаем массив в локальной области функции(99% случаев), то элементы std::array располагаются непрерывно на стеке. И размер std::array равен размеру C-style массива с одинаковым количеством элементов и их типом.

int c_arr[N];
std::array cpp_arr;
sizeof(cpp_arr) == cpp_arr.size() * sizeof(int) ==
sizeof(c_arr) == N * sizeof(int) == std::size(c_arr) * sizeof(int);

Итак. Выходит, что std::array идентичен сишному массиву по внутреннему устройству и произодительности, да еще и решает все проблемы неумелого использования последнего. Идеальный высокоуровневый инструмент!

Так что std::array должен быть первым выбором в случае необходимости создания массива с длиной, известной на этапе компиляции. У PVS-Studio есть прекрасная статья на этот счет.

Fix your flaws. Stay cool.

#STL #cppcore
12.03.2025, 12:00
t.me/grokaemcpp/626
GR
Грокаем C++
8 216 подписчиков
17
1
2.0 k
☝️ Это проект нашего подписчика, так что давайте его поддержим)
12.03.2025, 10:00
t.me/grokaemcpp/625
GR
Грокаем C++
8 216 подписчиков
22
4
2.0 k
Всем привет!👋 Мы занимаемся разработкой приложения, которое соединяет людей.
Мы хотим понять, что действительно нужно людям, какие функции могут сделать приложение полезным, удобным и приятным.

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

Заранее благодарим за помощь!
🎁В конце вас будет ждать приятный подарок😊

Пройти опрос ⬅️
12.03.2025, 10:00
t.me/grokaemcpp/624
GR
Грокаем C++
8 216 подписчиков
50
31
2.0 k
Проблемы С-style массивов
#опытным

В наследство от языка С С++ достались статические массивы. Так называемые С-style массивы. Это проверенные средства языка, успешно решающие свои задачи. Но у них есть серьезные недостатки, которые в основном связаны с низкоуровневостью этого инструмента.

Давайте кратко повторим, что такое C-style массив.

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

Определяется сишный массив вот так:

// создаем массив на 5 элементов,
// которые базово инициализируются мусором
int arr1[5];
// создаем массив на 5 элементов,
// которые инициализируются нулями
int arr1[5]{};

// создаем массив и предоставляем набор элементов, с помощью
// которых компилятор вычисляет длину массива и инициализирует элементы
int arr2[] = {1, 2, 3, 4, 5};

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

constexpr size_t array_size = 5;
int arr[array_size];
sizeof(arr) == array_size * sizeof(int); // true

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

auto array_size = sizeof(arr) / sizeof(Type);

В чем же его недостатки?

❗️ Массивы нельзя сравнивать напрямую, а только поэлементно. Напрямую сравниваются указатели на первый элемент.
int arr1[] = {0, 1, 2, 3};
int arr2[] = {0, 1, 2, 3};

// ложь так как сраниваются указатели,
// а они разные для разных объектов
arr1 == arr2;

❗️ Мимикрирование под массивы разрешает странную семантику с условиями и арифметическими операциями.
// создаем пустую строку в виде массива
char arr[] = "";

// условие будет всегда true, хотя мы создали пустую строку
if (arr);

// разрешается, но зачем? что значит прибавить к массиву число?
arr + 1;

❗️В С разрешены [массивы переменной длины](https://t.me/grokaemcpp/56) на уровне стандарта. И синтаксис у них ровно такой же, как и у статических массивов, только при его создании размер указывается не константой, а переменной. В С++ это не стандартная фича, а расширения компилятора. То есть нельзя писать кроссплатформенный код с использованием массивов переменной длины. Но за счет идентичного синтаксиса очень легко спутать один вид массива с другим и похерить переменосимость.

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

int foo(int arr[4]);
// На самом деле такая сигнатура полностью эквивалентна int foo(int * arr),
// что позволяет принимать в функцию массив любой длины и указатели.
// В С++ нет синтаксиса приема массива по значению.

void foo(int (&arr)[4]); // зато есть синтаксис приема массива по ссылке

// Нормального синтаксиса для возврата массива из функции также не завезли.
// Вот воркэраунды.
int get_array()[10];
auto get_array() -> int[10];

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

❗️Из-за сложности синтаксиса, вы скорее всего захотите обрабатывать массивы с помощью функций с похожей сигнатурой:

void foo(int * p, size_t size);

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

В общем, сишные массивы - это не объекты и не обладают преимуществами ООП и универсальной семантики для объектов в С++.

Поэтому стандартная библиотека предоставляет нам инструмент, который решает все проблемы C-style массивов. Это контейнер std::array. О нем мы поговорим в следующий раз.

Upgrade your tools. Stay cool.

#cppcore #goodoldc
11.03.2025, 12:00
t.me/grokaemcpp/623
GR
Грокаем C++
8 216 подписчиков
30
18
2.0 k
Когда мы вынуждены явно использовать new
#опытным

Сырые указатели - фуфуфу, бееее. Это не вкусно, мы такое не едим. new expression возвращает сырой указатель на объект. Соотвественно, мы должны максимально избегать явного использования new. У нас все-таки умные указатели и функции std::make_* довольно давно завезли.

Однако все-таки есть кейсы, когда мы просто вынуждены использовать new явно:

👉🏿 std::make_unique не может в кастомные делитеры. Если хотите создать уникальный указатель со своим удалителем - придется использовать new.

auto ptr = std::unique_ptr(new int(42), [](int* p) {
delete p;
std::cout << "Custom deleter called!\n";
});

👉🏿 Приватный конструктор у класса. Странно вообще пытаться создать объект такого класса, но не торопитесь. Приватный конструктор может быть нужен, чтобы оставить только один легальный способ создания объекта - фабричную функцию Create. Она возвращает уникальный указатель на объект и обычно является статическим членом класса. Функция Create имеет доступ к приватным методам, поэтому может вызвать конструктор. Но вот std::make_unique ничего не знает о приватных методах класса и не сможет создать объект. Придется использовать new.

struct Class {
static std::unique_ptr Create() {
// return std::make_unique(); // It will fail.
return std::unique_ptr(new Class);
}
private:
Class() {}
};

👉🏿 Жизнь без 20-го стандарта. До 20-го стандарта вы не могли создать объект POD класса без указания фигурных скобок. Но именно так и делает std::make_unique.

То есть вот так нельзя делать в С++17:
struct MyStruct {
int a, b, c;
};
auto ptr = std::make_unique(1, 2, 3); // Will fail C++17
auto ptr = std::unique_ptr(new MyStruct{1, 2, 3}); // Norm

Но можно в С++20. Так что тем, кто необновился, придется использовать new.

В целом, все. Если что забыл - накидайте в комменты.

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

Stay safe. Stay cool.

#cppcore #memory #cpp20 #cpp17
10.03.2025, 12:00
t.me/grokaemcpp/622
GR
Грокаем C++
8 216 подписчиков
17
8
1.8 k
Ты умеешь кодить, создавать телеграм-ботов,
знаешь много теории, есть потенциал,
но не знаешь где применить свои навыки и начать работать?

Тогда тебе стоит подать заявку в FUNDAMENT,
Это новый проект готовый приступать работать. Мы ищем программистов и сотрудников что бы запускаться!

Заходи в @ruFUNDAMENT и отправляй заявку (рассматриваем все)
10.03.2025, 11:00
t.me/grokaemcpp/621
GR
Грокаем C++
8 216 подписчиков
41
30
1.7 k
​​emplace_back vs push_back
#новичкам

Раз уж такая масленица пошла, расскажу про весь сыр-бор с методами вектора(да и не только вектора).

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

template< class... Args >
reference emplace_back( Args&&... args ); // returns ref to created element

void push_back( const T& value );
void push_back( T&& value );

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

Начнем со сложного. emplace_back принимает пакет параметров. Эти параметры предполагаются как аргументы конструктора хранимого типа T. Реализован он примерно так:
template
reference emplace_back(Args&&... args) {
if (size == capacity) grow();
return *new (start + size++) T(std::forward(args)...);
}

Если надо, то расширяемся и делаем placement new на участке памяти для нового объекта, попутно используя perfect forwarding для передачи аргументов в конструктор. Вот тут кстати те самые круглые скобки используются, которые не давали pod типам нормально конструироваться.

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

void push_back(T&& value) {
emplace_back(std::move(value));
}

Чтобы вызвать пуш бэк нужно вызвать 2 конструктора: от аргументов и copy|move. Для emplace_back же нужен только один конструктор - от аргументов.

То есть emplace_back банально эффективнее, чем push_back. Для случаев, когда мы почему-то не можем создать объект внутри emplace_back(POD типы и < С++20) мы его создаем снаружи и копируем/муваем внутрь. Тогда эффективности двух методов одинаковая.

Получается, что emplace_back в любом случае не менее эффективнее, чем push_back. Именно поэтому нужно всегда предпочитать использовать emplace_back.

Be just better. Stay cool.

#STL #memory
7.03.2025, 13:01
t.me/grokaemcpp/620
GR
Грокаем C++
8 216 подписчиков
23
20
2.0 k
C++ Russia 2025 — конференция для опытных разработчиков на C++

🧑‍💻 13 марта онлайн
📍 20–21 марта в Москве в отеле «МонАрх»

Вас ждут 39 выступлений от 47 спикеров. Без поверхностных рассуждений — только техническая конкретика и примеры из реальных проектов.

Доклады, на которые стоит обратить внимание:
→ Антон Полухин — [Не]очевидные оптимизации и паттерны из userver
→ Евгений Ерохин — Branch prediction, или Откуда процессор берет производительность
→ Константин Владимиров — Каша из топора: модули в C++, проблемы и решения
→ Алексей Веселовский — LLVM MemProf и методы профилирования памяти

Все выступления — на сайте. Кроме докладов будут дискуссии, нетворкинг, активности и розыгрыши от партнеров.

🎟 Купить билеты

Если оплачиваете самостоятельно — промокод GROKAEMCPP дает скидку 15% на билеты «Для частных лиц». Но выгоднее будет поучаствовать за счет компании. В этой статье есть аргументы, почему это будет выгодно не только вам, но и работодателю.

Реклама. ООО «Джуг Ру Груп». ИНН 7801341446
7.03.2025, 10:00
t.me/grokaemcpp/619
GR
Грокаем C++
8 216 подписчиков
47
20
2.0 k
​​Фиксим проблему с конструированием POD типов
#опытным

В прошлом посте говорили о том, что тяжело POD типам работать с функциями, которые внутри себя вызывают new. Клятые скобки!

Это очевидные недоработки стандарта. Которые взяли и пофиксили в С++20!

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

struct A {
  int a;
  int&& r;
};

int f();
int n = 10;

A a1{1, f()};               // OK, lifetime is extended
A a2(1, f());               // well-formed, but dangling reference
A a3{1.0, 1};               // error: narrowing conversion
A a4(1.0, 1);               // well-formed, but dangling reference
A a5(1.0, std::move(n));    // OK

Теперь можно использовать все преимущества метода emplace_back и писать такой код:

std::vector vec;
vec.emplace_back(1, 2, 3);

Вроде мелочь, а раньше это выбивало из колеи. Правило almost always use emplace_back здесь не работало.

Давайте добавлю еще чуть больше контекста(и своей боли) про emplace_back vs push_back.

Что было до с++20.
push_back спокойно переваривал следующий код:

std::vector vec;
vec.push_back({1, 2, 3});

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

Как видите здесь нет явного указания типа. И это работало до 20-х плюсов и было причиной делать исключения для правила с emplace_back'ом. Сравните:

std::vector vec;
vec.push_back({1, 2, 3});
vec.emplace_back(Point{1, 2, 3});

Больше букав! А если название структуры длинное? Вот вот.

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

Благо теперь наши пальцы сильнее защищены от стачивания, что не может не радовать!

Кстати, с С++20 и сам термин POD стал помечаться устаревшим, теперь его заменили более тонкие типы TrivialType, ScalarType, и StandardLayoutType. Ну и функции std::make_* тоже работают как надо с простыми структурами.

Enjoy small things. Stay cool.

#cppcore #cpp20
6.03.2025, 12:00
t.me/grokaemcpp/618
GR
Грокаем C++
8 216 подписчиков
34
16
1.8 k
Ограничения в конструировании POD типов
#опытным

Довольно часто приходится работать с plain old data типами. Они не имеют никаких специальных методов и конструкторов, это просто структуры с полями. Например:

struct Point {
int x;
int y;
int z;
}

Создают объекты таких типов с помощью синтаксиса универсальной инициализации через фигурные скобки {}:

Point p{1, 2, 3};
Point p(1, 2, 3); // Wrong

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

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

Но все-таки у этого есть проблемы.

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

Но с POD типами он работает хреново.

std::vector vec;
vec.emplace_back(1, 2, 3); // Error!

Вот так вы написать не можете. emplace_back под капотом использует создание объекта с помощью new именно через круглые скобки. А как мы помним, так инициализировать POD типы нельзя.

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

vec.emplace_back(Point{1, 2, 3});

что убивает преимущества emplace_back перед push_back. Либо можно использовать непросредственно push_back.

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

std::unique_ptr(new Point{1, 2, 3});

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

В общем, не жизнь, а страдания.

Однако есть свет в конце тоннеля! Но об этом в следующий раз.

See the light. Stay cool.

#cppcore
5.03.2025, 12:00
t.me/grokaemcpp/617
GR
Грокаем C++
8 216 подписчиков
11
5
1.7 k
RAID, бэкапы, репликация, WORM — если это часть вашей работы, добро пожаловать в BAUM.

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

Присоединяйтесь, если хотите разбираться в СХД так же хорошо, как в мемах.
5.03.2025, 11:00
t.me/grokaemcpp/616
GR
Грокаем C++
8 216 подписчиков
31
10
2.0 k
​​Почему мы везде не используем nothrow new?
#опытным

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

1️⃣ В современных плюсах вообще не часто можно увидеть прямой вызов new. Контейнеры и функции helper'ы std::make_* инкапсулируют в себе аллокации. Внутри них вызывается обычный бросающий new. Только в очень специфических кейсах явный вызов new оправдан. Поэтому пул примеров в принципе очень небольшой.

2️⃣ Представьте, что у вас закончилась память и nothrow new вернул вас nullptr. Можете ли вы локально обработать ошибку недостатка памяти? 99.9%, что нет. Поэтому вы будете вести эту ошибку по всему стеку вызовов до того места, где ее возможно обработать. То есть весь проект должен быть построен с учетом возможности возврата ошибки и постоянной проверкой этих ошибок.

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

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

Сейчас может начаться холивар "исключения vs объекты ошибок". Но вряд ли можно отрицать, что работать с bad_alloc исключением банально проще, чем обрабатывать nullptr. Именно поэтому вы скорее всего вообще не увидите nothrow new.

Handle problems easily. Stay cool.

#cppcore
4.03.2025, 13:00
t.me/grokaemcpp/615
GR
Грокаем C++
8 216 подписчиков
17
11
2.0 k
C++ — мощный инструмент, но с ним нужно правильно работать

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

◽️ Компилятор не видит файлы?
◽️Дебаг работает через раз?
◽️VSCode — просто красивый редактор?

💡 Приходите на открытый вебинар «Готовим рабочее место: C++ + VSCode»!

📅 5 марта в 20:00 (мск)

Мы разберём:
◽️Как настроить VSCode для C++: компиляторы, расширения, дебаг
◽️Как запускать программы без ошибок и ручной магии
◽️Как сделать среду разработки удобной и эффективной

🎯 А ещё всем участникам подарим скидку на топовый курс «C++ Developer»!

🔗 Регистрация: https://otus.pw/6LYpx/

Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
4.03.2025, 10:00
t.me/grokaemcpp/614
GR
Грокаем C++
8 216 подписчиков
59
22
2.0 k
Еще один способ сделать new небросающим
#опытным

Дело в том, что new - не совсем ответственнен за поведение при недостатке памяти. По плюсовой традиции тут можно кастомизировать почти все. Встречайте: std::new_handler.

Это такой typedef'чик:

typedef void (*new_handler)();

И алиас для функций, которые отвечают за обработку ситуации нехватки памяти. И вызываются они аллоцирующими функциями operator new и operator new[].

Чтобы установить такой хэдлер используется функция std::set_new_handler:

std::new_handler set_new_handler(std::new_handler new_p) noexcept;

Она делает new_p новой глобальной функцией нового обработчика и возвращает ранее установленный обработчик.

Предполагаемое назначение хэндлера - одна из трех вещей:

1️⃣ Сделать больше памяти доступной. (За гранью фантастики)

2️⃣ Залогировать проблему и завершить программу (например, вызовом std::terminate) +. Если вам плевать на graceful shutdown, то ок.

3️⃣ Кинуть исключение типа std::bad_alloc или его отпрысков с какой-нибудь кастомной надписью и/или залогировать проблему.

Раз std::new_handler - алиас на указатель функции, то что будет, если мы передадим nullptr в качестве хэндлера?

На самом деле это и делается по умолчанию. При старте программы nullptr выставляется в качестве хэндлера. При невозможности выделить память operator new вызывает std::get_new_handler. Если указатель на функцию нулевой, то в этом случае new ведет себя дефолтно - просто кидает исключение std::bad_alloc. В ином случае вызывает хэндлер.

С обработчиками есть один нюанс.

Если обработчик успешно заканчивает работу(то есть из него не вызван terminate или не брошено исключение), то operator new повторяет ранее неудачную попытку выделения и снова вызывает обработчик, если выделение снова не удается. Чтобы закончить цикл, new-handler может вызвать std::set_new_handler(nullptr): если после неудачной попытки new обнаружит, что std::get_new_handler возвращает нулевое значение указателя и выбросит std::bad_alloc.

Примерно так это работает:

void handler()
{
std::cout << "Memory allocation failed, terminating\n";
std::set_new_handler(nullptr);
}
 
int main()
{
std::set_new_handler(handler);
try
{
while (true)
{
new int [1000'000'000ul] ();
}
}
catch (const std::bad_alloc& e)
{
std::cout << e.what() << '\n';
}
}

Пользуйтесь фичей, чтобы оставлять на проде девопсерам сообщение: "А че так мало памяти в конфиге пода прописано, мм?"

Спасибо @Nikseas_314 за идею для поста)

Customize your tools. Stay cool.

#memory #cppcore
3.03.2025, 12:00
t.me/grokaemcpp/613
GR
Грокаем C++
8 216 подписчиков
58
18
1.8 k
Безопасный для исключений new
#опытным

Большинство приложений не могут физически жить без исключений. Даже если вы проектируете все свои классы так, чтобы они не возбуждали исключений, то от одного вида exception'ов вы вряд ли уйдете. Дело в том, что оператор new может бросить std::bad_alloc - исключение, которое говорит о том, что система не может выделить нам столько ресурсов, сколько было запрошено.

Однако мы можем заставить new быть небросающим! Надо лишь в скобках передать ему политику std::nothow. Синтаксис очень похож на placement new. Это в принципе он и есть, просто у new есть перегрузка, которая принимает политику вместо указателя.

MyClass * p1 = new MyClass; // привычное использование
MyClass * p2 = new (std::nothrow) MyClass; // небросающая версия

В первом случае при недостатке памяти выброситься исключение. А во втором случае - вернется нулевой указатель. Прям как в std::malloc.

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

#cppcore #memory
28.02.2025, 13:00
t.me/grokaemcpp/612
GR
Грокаем C++
8 216 подписчиков
34
13
1.9 k
Готовы к карьерному рывку? Станьте C++ Software Engineer в YADRO всего за 3 дня 🚀

Прямо сейчас российская технологическая компания YADRO проводит SPRINT OFFER для C++ Software Engineer.

🔵 Чтобы присоединиться к команде Telecom:

•‎ Оставьте заявку на сайте до 9 марта.
•‎ Пройдите скрининг с рекрутером. А после — техническое и менеджерское интервью.

YADRO ждёт кандидатов сразу в два направления:

→ Команда Telecom Platform разрабатывает полное платформенное решение для телекоммуникационных систем. На его основе строятся самые современные узлы сотовых сетей LTE- и GSM-стандартов — например, базовые станции и системы управления.

→ Инженеры разработки базовой станции LTE/GSM и 5G Core создают высоконагруженные системы, обеспечивающие связь как критически важных, так и новых поколений. А также разрабатывают надёжное и масштабируемое ПО для мобильных сетей.

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

Оставляйте заявку до 9 марта и станьте частью команды YADRO. Все подробности — по ссылке.
28.02.2025, 10:01
t.me/grokaemcpp/611
GR
Грокаем C++
8 216 подписчиков
109
40
2.5 k
Почему так смешно?😂😂😂
27.02.2025, 12:00
t.me/grokaemcpp/610
GR
Грокаем C++
8 216 подписчиков
65
47
2.5 k
new vs malloc

Чем отличаются new и malloc? Один из популярных вопросов на собеседованиях, которые проверяет, насколько хорошо вы знакомы с тонкостями работы с памятью в С/С++. Поэтому давайте сегодня это обсудим.

Не совсем корректно, наверное сравнивать фичи двух разных языков с разными доминантными парадигмами программирования. Но раз в стандарте есть std::malloc, а new тоже выделяет память, то можно попробовать.

👉🏿 new expression помимо аллокации памяти вызывает конструктор объекта. std::malloc только выделяет память.

👉🏿 std::malloc - совсем не типобезопасный. Он возвращает void * без какого-либо признака типа. Придется явно кастовать результат к нужному типу. new в свою очередь возвращает типизированный указатель.

👉🏿 При ошибке выделения памяти new бросает исключение std::bad_alloc, в то время как std::malloc возвращает NULL. Соответственно нужны разные способы обработки ошибочных ситуаций.

👉🏿 Поведение new может быть переопределено внутри кастомных классов, поведение std::malloc - неизменно.

👉🏿 Если вам не нужно конструирование объекта, то просто вызывайте operator new. Он делает то же самое, что и std::malloc(потенциально вызывает его внутри себя).

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

👉🏿 new плохо работает с реаллокациями. Нужно выделить новый сторадж, скопировать туда данные и вызвать delete. В то время, как malloc имеет функцию-партнера realloc, которая может изменить размер существующего куска памяти более эффективно, чем последовательность new-memcpy-delete.

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

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

Control your memory. Stay cool.

#cppcore #interview #memory
26.02.2025, 12:00
t.me/grokaemcpp/609
GR
Грокаем C++
8 216 подписчиков
56
40
2.4 k
Все грани new
#опытным

Не каждый знает, что в плюсах new - это как медаль, только лучше. У медали 2 стороны, а у new целых 3!

Сейчас со всем разберемся.

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

Выглядит оно так:

// creates dynamic object of type int with value equal to 42
int* p_triv = new int(42);
// created an array of 42 dynamic objects of type int with values equal to zero
int* p_triv_arr = new int[42];

struct String {std::string str};

// creates dynamic object of custom type String using aggregate initialization
String* p_obj = new String{"qwerty"};
// created an array of 5 dynamic objects of custom type String with default initialization
String* p_obj = new String[5];

Эта штука делает 2 дела одновременно. А что, если мне не нужно выполнять сразу 2 этапа? Что, если мне нужна только аллокация памяти?

За это отвечает operator new. Это оператор делает примерно то же самое, что и malloc. То есть выделяет кусок памяти заданного размера. Выражение new именно этот оператор и вызывает, когда ему нужно выделить память. Но его можно вызвать и как обычную функцию, а также перегружать для конкретного класса:

// class-specific allocation functions
struct X
{
static void* operator new(std::size_t count)
{
std::cout << "custom new for size " << count << '\n';
// explicit call to operator new
return ::operator new(count);
}
 
static void* operator new[](std::size_t count)
{
std::cout << "custom new[] for size " << count << '\n';
return ::operator new;
}
};
 
int main()
{
X* p1 = new X;
delete p1;
X* p2 = new X[10];
delete[] p2;
}

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

Для этого есть placement new. Это тот же самый new expression, только для этого есть свой синтаксис. Сразу после new в скобках вы передаете указатель на область памяти, достаточной для создания объекта.

alignas(T) unsigned char buf[sizeof(T)];
// after new in parentheses we specified location of future object
T* tptr = new(buf) T;
// You must manually call the object's destructor
tptr->~T();

В этом случае кстати вызывается специальная перегрузка operator new:

void* operator new  (std::size_t count, void* ptr);

Однако она не делает ничего полезного и просто возвращает второй аргумент наружу.

Вот так по разному в С++ можно использовать new. Главное - не запутаться!

Have a lot of sides. Stay cool.

#cppcore #memory
25.02.2025, 15:07
t.me/grokaemcpp/608
GR
Грокаем C++
8 216 подписчиков
43
15
2.0 k
​​Неинициализированные переменные

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

Кейс из ревью.

Вот у вас есть примерно такая функция и примерно так ее можно вызвать:

std::optional WaitForSomething(bool& success) {
SomeType result;
if (!SomeOperation()) {
// error
return std::nullopt;
}
if (condition) {
//fill result
}
success = (condition == 1);
return result;
}

bool success;
auto obj = WaitForSomething(success);
if (!obj) {
// handle error
}
if (success) {
// do something
}

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

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

А вот приходит другой программист и изменяет эту функцию примерно так:

std::optional WaitForSomething(bool& success) {
SomeType result;
if (!SomeOperation()) {
// error
return std::nullopt;
}
while (condition1 && condition2) {
//fill result
if (condition3)
success = true;
}
return result;
}

По логике реального кода было все четко. Кроме того, что теперь в ветке, где не выполняются одновременно condition1, condition2 и condition3 success остается тем же, что и передали в функцию.

Так конечно не нужно делать. Это хоть чуть-чуть, но изменяет логику поведения функции. Но я хочу тут другой момент затронуть.

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

В реальности привело к довольно долгому дебагу.

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

bool success = false;

то этой боли можно было бы избежать. А изменение функциональности самой функции выявят тесты.

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

Define your value. Stay cool.

#cppcore #goodpractice
24.02.2025, 13:00
t.me/grokaemcpp/606
GR
Грокаем C++
8 216 подписчиков
34
12
2.3 k
​​Пингвинячимся

На плюсах писать и линукс не знать - вы в своем уме?. Современная backend С++ разработка жестко привязана к этой системе. Отсутствие навыков работы с linux - гарантия ограниченных карьерных возможностей и технической экспертизы.

Но ладно, так и быть, вам я расскажу одно средство, которое поможет вам прокачаться в пингвине. Это канал LinuxCamp.

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

У него вы найдете уникальные гайды по администрированию Linux и подробные статьи о внутреннем устройстве операционных систем. А также крутые техники сочетания С++ и linux в разработке

Не первый и точно не последний раз рекомендую этот канал.
Так что подписывайтесь на LinuxCamp и становитесь частью сообщества истинных пингвинячих гуру!
24.02.2025, 10:00
t.me/grokaemcpp/605
GR
Грокаем C++
8 216 подписчиков
89
27
1.9 k
Как думаете, кто и когда отправит С++ на покой?
Или он будет жить вечно?
22.02.2025, 14:00
t.me/grokaemcpp/604
GR
Грокаем C++
8 216 подписчиков
33
15
1.8 k
​​Почему приватные методы находятся в описании класса?

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

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

Ну а приватные-то методы зачем раскрывать? Зачем их помещать внутрь класса и делать видимыми для клиентского кода?

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

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

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

В чем прикол того, что приватные методы участвуют в разрешении перегрузок?

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

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

Однако, такой протокол поведения влечет за собой различные сайд-эффекты.

struct DumbClass {
void DumbFunction(int param) {
// i do nothing because i’m dumb
}
private:
void DumbFunction(double param) = delete;
};

int main() {
DumbClass{}.DumbFunction(1.0);
}
// error: use of deleted function ‘void DumbClass::DumbFunction(double)’
Я могу удалить приватную перегрузку публичного метода, например какую мы обсуждали выше. И вызвать публичный метод опять с дробным числом. Но компилятор на меня наругается, потому что я попытался вызвать удаленную перегрузку метода. Хотя она вообще-то объявлена, как приватная! А то, что я ее удалил - это детали реализации. Получается раскрытие деталей реализации.
Не знаю, как оценивать все перечисленные причины. Напишите свое мнение в комментариях.

Stay aware. Stay cool.
#design #howitworks #cppcore #hardcore
20.02.2025, 14:00
t.me/grokaemcpp/603
GR
Грокаем C++
8 216 подписчиков
20
7
2.0 k
Ваш код тормозит под нагрузкой?

Не справляется с задачами параллельного вычисления?

💡 Приходите на открытый урок по многопоточному программированию C++!

▫️Вместе напишем многопоточную реализацию задачи подсчёта суммы чисел.
▫️Разберём примитивы синхронизации.
▫️Ускорим код с помощью стандартной библиотеки.

🎯 Вы начнёте осваивать многопоточное программирование и сделаете первый шаг к позициям middle+ в C++!

А ещё получите скидку на большое обучение «C++ Developer».

📅 Успейте записаться на открытый вебинар 25 февраля в 20:00 (мск)!

🔗 Регистрируйтесь прямо сейчас и узнайте, как эффективно управлять потоками в C++: https://otus.pw/Gf9B0/

Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
20.02.2025, 11:00
t.me/grokaemcpp/602
GR
Грокаем C++
8 216 подписчиков
39
40
2.1 k
​​Как вызвать методы класса напрямую через vtable?
#опытным

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

Полиморфизм - там штука, которая сделала ООП и ОО дизайн такими мощными и широкоиспользуемыми инструментами. За счет чего он реализован в С++?

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

struct Base {
virtual void func1() {
std::cout << i+1<< std::endl;
}
virtual void func2() {
std::cout << i+2 << std::endl;
}
};

struct Derived: Base {
void func1() override {
std::cout << i+3 << std::endl;
}
void func2() override {
std::cout << i+4 << std::endl;
}
};

int main() {
auto basePtr = std::unique_ptr(new Derived);
uint64_t ** pVtable = (uint64_t **)basePtr.get();
printf("Vtable: %p\n", pVtable);
printf("First Entry of VTable: %p\n", (void*)pVtable[0][0]);
printf("Second Entry of VTable: %p\n", (void*)pVtable[0][1]);

using VoidFunc = void (*) (void*);
VoidFunc firstMethod = (VoidFunc) pVtable[0][0];
firstMethod(basePtr.get());
VoidFunc secondMethod = (VoidFunc) pVtable[0][1];
secondMethod(basePtr.get());
}

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

Далее нужно найти указатель на vtable. Обычно он всегда лежит в первых 8 байтах объектов полиморфных классов. Так как у нас двухуровневая косвенность (указатель на объект, а в нем указатель на таблицу), мы объявляем двойной указатель на 8-мибайтный инт и он и будет нашим vtable_ptr. Почему uint64_t? Да на самом деле косвенность трехуровненая, потому что в ячейках vtable лежат указатели на методы, а указатель легко представить в виде 8-байтного числа.

Самое сложное позади, мы нашли все, что нужно. Осталось только кастануть инты, лежащие в ячейках vtable к указателям на функцию правильного типа. И вуаля. Готово. Возможный вывод:

Vtable: 0x5a7841237eb0
First Entry of VTable: 0x5a781fd28ae2
Second Entry of VTable: 0x5a781fd28b22
3
4

Где это знание нужно?

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

Знание внутреннего устройства инструмента позволить вам понимать его преимущества и недостатки. Возможно в проектах с высокими требованиями к производительности(слабое железо как в embedded или супер-ультра-гипер low latency как в hft), вам предстоит выбирать между динамическим полиморфизмом и статическим полиморфизмом aka шаблонами. Чтобы сделать выбор качественно и аргументировать его, нужно знание базы.

А как говорится, пока сам не потрогаешь ручками - не поймешь.

Только не нужно так писать в продовом коде! А то будет, как на картинке внизу.

Stay hardwared. Stay cool.
19.02.2025, 12:00
t.me/grokaemcpp/601
GR
Грокаем C++
8 216 подписчиков
60
36
2.0 k
​​No new line

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

Небольшой пример:

Файлик foo.hpp:

// I love code
// I love C++

Файлик bar.cpp:

#include "foo.hpp"
#include "baz.hpp"


А теперь вспоминаем, что препроцессор вставляет все содержимое хэдера на место инклюда И(!) не вставляет после него символ конца строки. То есть спокойно может получится следующее:

// I love code
// I love C++#include "baz.hpp"

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

Стандарт нам говорит:
... If a source file that is not empty does not end in a new-line character,
or ends in a new-line character immediately preceded by a backslash
character before any such splicing takes place, the behavior is undefined.

Так что ub без кода - вполне существующая вещь.

Или уже нет?

На самом деле приведенная цитата была из стандарта 2003 года.

С++11 пофиксил эту проблему и обязал препроцессоры вставлять new line в конце подключаемых файлов:

A source file that is not empty and that does not end in a new-line character,
or that ends in a new-line character immediately preceded by a backslash
character before any such splicing takes place, shall be processed
as if an additional new-line character were appended to the file.

Так что теперь проблемы нет.

Решил написать об этом, просто потому что очень весело, что в плюсах можно было такими неочевидными способами отстрелить себе конечность.

Ну и хорошо, что стандарт все-таки не только новую функциональность вводит, а фиксит вот такие вот недоразумения.

Fix your flaws. Stay cool.

#compiler
18.02.2025, 15:00
t.me/grokaemcpp/600
GR
Грокаем C++
8 216 подписчиков
21
10
1.8 k
С++ Книги - канал для скачивания книг по C++

Что в нашем канале:
1. Книги по C++
2. Много книг на русском языке
3. Описания книг, автор, год выпуска
4. Все книги можно скачать в 2 клика.
5. Всё, никакой другой воды.

Подписывайтесь на нас: @download_c_books
18.02.2025, 14:00
t.me/grokaemcpp/599
GR
Грокаем C++
8 216 подписчиков
1
С++ Книги - канал для скачивания книг по C++

Что в нашем канале:
1. Книги по C++
2. Много книг на русском языке
3. Описания книг, автор, год выпуска
4. Все книги можно скачать в 2 клика.
5. Всё, никакой другой воды.

Подписывайтесь на нас: @download_c_books
18.02.2025, 12:00
t.me/grokaemcpp/598
GR
Грокаем C++
8 216 подписчиков
29
10
1.7 k
​​Дедлокаем один поток
#опытным

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

Дедлок - это ситуация в коде, когда одновременно выполняются все следующие условия:

А ну, мальчики, играем поочереди. Только один поток может получить доступ к ресурсу в один момент времени.

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

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

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

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

Но это такая общая теория работы с многозадачностью в программах.

Определение оперирует общим термином ресурс. И не учитывает поведение конкретного ресурса и деталей его реализации. А они важны!

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

std::mutex mtx;
mtx.lock();
mtx.lock();

Стандарт говорит, что будет UB. То есть поведение программы неопределено, возможно она заставит Ким Чен Ира спеть гангам стайл.

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

1️⃣ Компилятор имплементировал умный мьютекс, который может задетектить double lock и, например, кинуть в этом случае исключение.

2️⃣ Мьютекс у нас обычный, подтуповатый и он делает ровно то, что ему говорят. А именно пытается залочить мьютекс. Конечно у него ничего не получится и он вечно будет ждать его освобождения. Результат такого сценария - дедлок одного потока одним мьютексом!

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

Be self-sufficient. Stay cool.

#concurrency #cppcore #compiler
16.02.2025, 12:00
t.me/grokaemcpp/597
GR
Грокаем C++
8 216 подписчиков
37
7
1.6 k
Ответ
#опытным

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

Вообще говоря, в определении дедлока не звучит слово "мьютекс". Потоки должны ждать освобождения ресурсов. А этими ресурсами может быть что угодно.
Для организации дедлока достаточно просто, чтобы 2 потока запустились в попытке присоединить друг друга. Естественно, что они будут бесконечно ждать окончания работы своего визави.

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

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

std::thread * t_ptr = nullptr;

void func1() {
std::this_thread::sleep_for(std::chrono::seconds(1));
t_ptr->join();
std::cout << "Never reached this point1" << std::endl;
}

void func2(std::thread& t) {
t.join();
std::cout << "Never reached this point2" << std::endl;
}

int main() {
std::thread t1{func1};
t_ptr = new std::thread(func2, std::ref(t1));
while(true) {
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}

Все просто. Вводим глобальный указатель на поток. В функции первого потока мы даем время инициализировать указатель и присоединяем поток по указателю. А тем временем в main создаем динамический объект потока и записываем его по указателю t_ptr. Таким образом первый поток получает доступ ко второму. В функцию второго потока передаем объект первого потока по ссылке и присоединяем его. Обе функции после инструкции join выводят на консоль запись.

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

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

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

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

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

Stay surprised. Stay cool.

#concurrency #cppcore
14.02.2025, 12:00
t.me/grokaemcpp/596
GR
Грокаем C++
8 216 подписчиков
23
10
1.7 k
🔥 Самые нужные каналы для C/C++ разработчика, чтобы расти в доходе 💸

C/C++ | Вопросы собесов
C/C++ | Вакансии с удаленкой
C/C++ | LeetCode
C/C++ | Тесты

Подпишись, чтобы не потерять ☝️
14.02.2025, 10:00
t.me/grokaemcpp/595
GR
Грокаем C++
8 216 подписчиков
7
8
2.2 k
Сколько минимально нужно мьютексов, чтобы вызвать дедлок?
#опытным

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

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

А что если мы не будем ориентироваться на общую универсальную для всех языков теорию многопоточного программирования и сфокусируется чисто на С++ и средствах, которые он предоставляет?

Какой тогда будет ответ?

#quiz
13.02.2025, 12:00
t.me/grokaemcpp/593
GR
Грокаем C++
8 216 подписчиков
37
8
1.8 k
​​Бросаем число
#новичкам

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

А что если мы попытаемся бросить что-то совсем несвязанное с иcключениями? Например, какой-нибудь тривиальный тип вроде int. Это вообще законно?

Абсолютно законно. В С++ можно бросать все, что угодно, кроме объектов неполных типов, абстрактных классов и указателей на неполный тип. Даже указатель на void можно.

Как и число.

Поймать число примерно также просто, как его бросить:

void foo() {
throw 1;
}

int main() {
try {
foo();
}
catch(int i) {
std::cout << i << std::endl;
}
}

// OUTPUT: 1

Это кстати один из любимых вопросов у интервьюеров.

"А можно ли кидать число вместо исключения?"

Теперь вы с полной уверенностью ответите "можно".

Но вот зачем это может быть нужно? Оставьте ваши мысли в комментариях

Make sense. Stay cool.

#interview #cppcore
11.02.2025, 12:00
t.me/grokaemcpp/592
GR
Грокаем C++
8 216 подписчиков
168
31
2.3 k
Сколько вам было лет, когда вы узнали, что название бинарного файла, который по умолчанию генерирует GCC - a.out - это сокращение от assembler output(вывод ассемблера)? 🤯

Я вот узнал в толькочтогодиков.
9.02.2025, 20:09
t.me/grokaemcpp/591
GR
Грокаем C++
8 216 подписчиков
50
31
1.8 k
​​Потокобезопасный интерфейс
#новичкам

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

Возьмем максимально простую реализацию самой простой очереди:

struct Queue {
void push(int value) {
storage.push_back(value);
}
void pop() {
storage.pop_front();
}
bool empty() {
return storage.empty();
}
int& front() {
return storage.front();
}
private:
std::deque storage;
};

Она конечно потокоНЕбезопасная. То есть ей можно адекватно пользоваться только в рамках одного потока.

Как может выглядеть код простого консьюмера этой очереди?

while(condition)
if (!queue.empty()) {
auto & elem = queue.front();
process_elem(elem);
queue.pop();
}

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

Бабахаем везде лок гард на один мьютекс и дело в шляпе!

struct Queue {
void push(int value) {
std::lock_guard lg{m};
storage.push_back(value);
}
void pop() {
std::lock_guard lg{m};
storage.pop_front();
}
bool empty() {
std::lock_guard lg{m};
return storage.empty();
}
int& front() {
std::lock_guard lg{m};
return storage.front();
}
private:
std::deque storage;
std::mutex m;
};

Все доступы к очереди защищены. Но спасло ли реально это нас?

Вернемся к коду консюмера:

while(true)
if (!queue.empty()) {
auto & elem = queue.front();
process_elem(elem);
queue.pop();
}


А вдруг у нас появится еще один консюмер? Тогда в первом из них мы можем войти условие, а в это время второй достанет последний элемент. Получается, что мы получим доступ к неинициализированной памяти в методе front.

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

Что делать? Не только сами методы класса должны быть потокобезопасными. Но еще и комбинации использования этих методов тоже должны обладать таким свойством. И с данным интерфейсом это сделать просто невозможно.

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

struct ThreadSafeQueue {
void push(int value) {
std::lock_guard lg{m};
storage.push_back(value);
}
std::optional pop() {
std::lock_guard lg{m};
if (!storage.empty()) {
int elem = storage.front();
storage.pop_front();
return elem;
}
return nullopt;
}
private:
std::deque storage;
std::mutex m;
};

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

Теперь консюмер выглядит так:

while(true) {
auto elem = queue.pop();
if (elem)
process_elem(elem.value());
}

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

Stay safe. Stay cool.

#concurrency #design #goodpractice
7.02.2025, 12:00
t.me/grokaemcpp/590
GR
Грокаем C++
8 216 подписчиков
22
7
1.7 k
📌 55% кандидатов валятся на этих трёх задачах. Разбери их до собеседования!

🔥 Бинарный поиск — один из самых частых алгоритмов на собеседованиях.
Но половина кандидатов (55%) делает ошибки или не может решить даже базовые задачи.

На бесплатном онлайн-уроке ты:
✅ Решишь 3 реальные задачи с собеседований в Яндекс, Озон и Сбер
✅ Раз и навсегда освоишь бинарный поиск, чтобы не ошибаться на собесе
✅ Поймёшь, как интервьюер оценивает твой код

Разбираем решения на 6 ЯП: 🖥 🖥 🖥 👣 🖥 🖥

📅 Когда: 8 февраля (суббота), 12:30 по МСК
📍 Где: Онлайн

🔗 Регистрируйся: https://clck.ru/3GC47R

⏳ Не откладывай — разберись в бинарном поиске не просто быстро, а навсегда!

(Это часть большой программы подготовки к собеседованиям. Если хочешь получить оффер в компанию мечты, приходи — всё покажу!)
7.02.2025, 10:00
t.me/grokaemcpp/589
GR
Грокаем C++
8 216 подписчиков
41
20
1.9 k
Смешиваем std::visit и std::apply
#опытным

Подумал об интересном сочетании функций std::visit и std::apply. В прошлом посте про паттерн overload мы в цикле проходились по вектору вариантов и к каждому элементу применяли std::visit. Но прикольно было бы просто взять и за раз ко всем элементам коллекции применить std::visit. Ну как за раз. Без явного цикла.

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

Давайте попробуем на примере std::array запихать все его элементы в функтор и лаконично вызвать std::visit.

template
struct Visitor : Lambdas...
{
Visitor(Lambdas... lambdas) : Lambdas(std::forward(lambdas))... {}
using Lambdas::operator()...;
};

using var_t = std::variant;

int main(){
std::array arr = {1.5, 42, "Hello"};
Visitor vis{[](int arg) { std::cout << arg << ' '; },
[](double arg) { std::cout << std::fixed << arg << ' '; },
[](const std::string& arg) { std::cout << std::quoted(arg) << ' '; } };

std::apply([&](auto&&... args){(std::visit(vis, std::forward(args)), ...);}, arr);
}

Начало в целом такое же, только теперь у нас std::array. Нам интересна последняя строчка.

В std::apply мы должны передать функтор, который может принимать любое количество параметров. Благо у нас есть вариадик лямбды, которые позволяют сделать именно это. Компилятор сам сгенерирует структуру, которая сможет принимать ровно столько аргументов, сколько элементов в массиве arr.

Дальше мы все эти аргументы распаковываем в серию вызовов std::visit так, чтобы каждый элемент массива передавался в отдельный std::visit. Естественно, все делаем по-красоте, с perfect forwarding и fold-expression на операторе запятая.

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

Выглядит клёво!

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

Look cool. Stay cool.

#template #cpp17
6.02.2025, 13:10
t.me/grokaemcpp/588
GR
Грокаем C++
8 216 подписчиков
35
6
2.0 k
Привет, друзья!
Владимир любезно предоставил мне трибуну, чем я с удовольствием и воспользуюсь!

Меня зовут Александр Логач, я IT- руководитель, с 17 годами опыта в индустрии.
Если вам интересны темы роста, лидерства, креативности и высоких технологий, то мне есть о чём рассказать.

О чём я пишу:

💡 Реальные практические рекомендации:
Как увереннее выступать – практические советы для публичных выступлений.
Начни с малого – как закрыть ментальные долги и сосредоточиться на главном.

🧠 Истории CTO, который за более чем 17 лет повидал всякого:
Три вопроса – методика, которая меняет всё.
Процесс или результат – как не дать клиенту управлять вами.
Идеальное резюме - пошаговая инструкция

🤔 Заставляю задуматься:
Грейды в IT: можно ли стать CTO в 24 года? – о быстром карьерном росте и его реальных перспективах.
6 часов в неделю на работе – как эффективность отличается от иллюзии продуктивности.

Подписывайтесь на мой канал, нам есть о чем поговорить!
6.02.2025, 10:07
t.me/grokaemcpp/587
GR
Грокаем C++
8 216 подписчиков
31
18
2.0 k
​​Как header only либы обходят ODR
#новичкам

В С++ есть одно очень важное правило, которое действует при компиляции и линковке программы. Это правило одного определения. Или One Definition Rule(ODR). Оно говорит о том, что во всей программе среди всех ее единиц трансляции должно быть всего одно определение сущности.

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

Тогда встает вопрос: А как тогда header-only библиотеки обходят это требование? Сами посудите, подключаем какую-нибудь json заголовочную либу, везде ее используем, линкуем программу и все как-то работает. Хотя во многих единицах трансляции есть определение одних и тех же сущностей.

В чем подвох?

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

Сами посмотрите на некоторые примеры: cereal для сериализации, nlohmann для json'ов, почти весь Boost. Там все жестко шаблонами и измазано.

А там, где шаблоны неприменимы можно использовать inline|static функции и поля класса, а также анонимные пространства имен .

В общем, в С++ есть много средств обхода ODR и ими всеми активно пользуются header-only библиотеки.

Bypass the rules. Stay cool.

#compiler #design
5.02.2025, 12:00
t.me/grokaemcpp/586
GR
Грокаем C++
8 216 подписчиков
31
14
1.8 k
​​Header only либы. Pros and cons
#новичкам

Мы живем в мире С++, где почти нет ничего общепринятого. Часто даже не для очень специфичной задачи, типа сериализации, приходится искать какие-то сторонние решения и сравнивать их. Давайте отложим в сторону вопросы о качестве кода и дизайне и сконцентрируемся вот на чем. Какую библиотеку выбрать: уже скомпилированную или полностью header-only? Будем рассматривать вопрос с точки зрения header-only либ. Какие у них преимущества и недостатки?

Преимущества:

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

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

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

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

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

Недостатки:

⛔️ Крупные объектные файлы. Раз нам доступно определение сущностей, то мы можем попробовать их встроить. Это само по себе увеличивает размер бинарного файла. Так еще и на каждую единицу трансляции будет свое определение слабого символа этой сущности. Да, в конце они объединятся, но в объектные файлы все еще будут повторения.

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

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

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

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

Make the right choice. Stay cool.
4.02.2025, 10:10
t.me/grokaemcpp/585
GR
Грокаем C++
8 216 подписчиков
9
3
1.8 k
Все надоело и пропал интерес, чувствуешь себя амебой и хочется только залипать в телефоне. Бывает?

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

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

Подписывайтесь на канал @vadimpetrov_psy и научитесь работать без упахивания, выгорания и ущерба для личной жизни!

👨🏻‍💻 Псс. Заходите в закреп канала - там много полезного, и даже бесплатный мини-курс.
4.02.2025, 09:10
t.me/grokaemcpp/584
GR
Грокаем C++
8 216 подписчиков
44
22
1.8 k
int count_ones(unsigned num)
{
num = (num & 0x5555555555555555 ) + ((num >> 1) & 0x5555555555555555 );
num = (num & 0x3333333333333333 ) + ((num >> 2) & 0x3333333333333333 );
num = (num & 0x0f0f0f0f0f0f0f0f ) + ((num >> 4) & 0x0f0f0f0f0f0f0f0f );
num = (num & 0x00ff00ff00ff00ff ) + ((num >> 8) & 0x00ff00ff00ff00ff );
num = (num & 0x0000ffff0000ffff ) + ((num >> 16) & 0x0000ffff0000ffff);
return num;
}
Выглядит все так, как будто кот случайно блеванул на экран, но естественно у этой каши есть логичное объяснение, которое можете найти тут.

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

Honorable mentions:
Питонисты часто в этой задаче упоминают, что они одной левой могут превратить число в бинарное представление, потом в строку и просто посчитать одной функцией количество вхождений единички. А мы вообще-то ничем не хуже! И даже лучше! Нам не нужно конвертировать бинарное представление в строку. Достаточно std::bitset и его метода count:

int count_ones(unsigned num) {
return std::bitset<32>{num}.count();
}

Solve your problems. Stay cool.
1.02.2025, 17:59
t.me/grokaemcpp/583
GR
Грокаем C++
8 216 подписчиков
19
17
1.4 k
Считаем единички. Решения

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

int count_ones(unsigned num) {
int result = 0;
while(num > 0) {
result += num & 1;
num >>= 1;
}
return result;
}

Алгоритмическая сложность этого решения - О(log(num)).
Что может быть интереснее? Например, знали ли вы, что выражение num & (num - 1) лишает число его самой правой единички? Посмотрите сами:

10 = 1010
1010 & (1010 - 1) = 1010 & 1001 = 1000

118 = 111011
111011 & (111011 - 1) = 111011 & 111010 = 111010

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

int count_ones(unsigned num) {
int result = 0;
for(; num > 0; num &= (num - 1), ++result);
return result;
}

А что насчет самого короткого решения? Зачем писать велосипед, если можно просто воспользоваться встроенной функцией компилятора(или С++20 фичей):

int count_ones(unsigned num) {
return std::popcount(num);
// or compiler extension
// return __builtin_popcount(num);
}


А что, если мы хотим константную сложность? Такое вообще возможно?

Конечно. Нам потребуется всего sizeof(num)* 8 итераций цикла и проверки последнего бита, чтобы найти нужное число. Константа? Да. Эффективно ли это? Это даже медленнее, чем самое первое решение.

Однако давайте подумаем еще чуть-чуть. Комбинаций битов в инте на самом деле не такой уж и и много. Всего 2^32. Можно создать массив байтов на 2^32 элементов и в каждой ячейке хранить количество единичек для числа равного индексу этой ячейки. Мы это как-то можем заранее нагенерить(или при первом вызове функции) и потом все вызовы функции count_ones будут занимать константное время. Правда памяти сожрется на это предостаточно.

static std::array::max()> ones;
// somehow fill array
int count_ones(unsigned num) {
return ones[num];
}

Кстати полезный ход. Иногда из-за сильных ограничений по входным данным задачи ее можно решить намного более оптимальным способом.

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

Но чтобы было прям наглядно понятна логика, то массив можно сделать еще меньше, если брать по 4 бита(тетрад). Различных тетрадов всего 16 штук, поэтому и нужно будет всего 16 байт доп памяти и 8 итераций цикла. Спасибо, @tutralex, за решение)

int count_ones(unsigned num)
{
static unsigned char c[16]={0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4};
int count=0;
while (num)
{
count+=c[num&0x0F];
num>>=4;
}
return count;
};

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

Если вы еще не устали, то у меня для вас есть banger. Нахождение количества единичных бит в числе - это не просто задача с литкода. У нее есть практическое применение. Есть такая штука, как расстояние Хемминга для двоичных чисел. Это количество символов, которое отличает данную строку битов от строки, заполненной нулями. То есть это и есть наше количество единичек. Эта штука используется во многих дисциплинах, в том числе и криптографии. Не удивительно, что много народу совершенствовало решение этой задачи. На мой взгляд, самое мозгодробительное решение выглядит примерно так:
1.02.2025, 17:59
t.me/grokaemcpp/582
GR
Грокаем C++
8 216 подписчиков
49
16
1.9 k
Считаем единички
#задачки

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

Вроде бы простая и популярная задача: посчитать количество единиц в битовом представлении числа. Уверен, что большинство из вас решали эту задачу.

Однако популярный подход - не самый эффективный, элегантный и интересный.

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

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

А чтобы умудренным опытом людям было немного интереснее, давайте ограничим условия. Задачу надо решить либо за константное время(в среднем), либо за наименьшее количество строчек. Выражения вне цикла for разделенные символом ; считаются разными строчками.

int count_ones(unsigned num) {
// Here your code
}

Challenge yourself. Stay cool.
31.01.2025, 12:00
t.me/grokaemcpp/581
GR
Грокаем C++
8 216 подписчиков
32
42
1.9 k
Перфорируем код

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

Кто бы что ни говорил, а Яндекс делает крутые продукты для разработчиков. Это огромная компания с большой экспертизой. И если чего-то хорошего нет на рынке, то они это делают сами. И нередко Яндекс выкладывают свои наработки в опенсорс. Чего стоит только userver, без которого писать микросервисы, мягко говоря, неудобно.

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

B вот недавно они выкатили на гитхаб свой инструмент Perforator, который используют для анализа производительности большинства своих сервисов. Причем тулза настолько крутая, что на ее работу уходит примерно 0.2% CPU и 1% RAM. Поэтому они буквально каждый свой сервис профилируют этим перфоратором прям в проде.

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

Тулза поддерживает поддерживает нативные языки (C++, C, Go, Rust), а также экспериментально Python и Java. Ещё его можно развёртывать на Kubernetes и локально. В общем, раздолье для экспериментов.

🌐 Уже сейчас Perforator можно скачать и протестировать самому, подробности — в статье на Хабре. Исходный код доступен под лицензией MIT (и GPL — для eBPF-программ) и запускается под x86-64 Linux. Визуализацию работы сервиса можно глянуть тут.

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

Measure your performance. Stay cool.

Реклама. ООО "Яндекс", ИНН 7736207543.
30.01.2025, 19:00
t.me/grokaemcpp/580
GR
Грокаем C++
8 216 подписчиков
79
7
1.8 k
Итоги конкурса

Мы долго ждали и, наконец, дождались. Вчера я честно взял генератор случайных чисел и нашел победителя и будущего счастливого обладателя книжки "С++. Практика многопоточного программирования" Энтони Уильямса. Ботов розыгрышей не хотелось использовать, без души все это. Надеюсь, вы доверяете моей непредвзятости)

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

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

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

Be lucky. Stay cool.
28.01.2025, 12:00
t.me/grokaemcpp/579
Результаты поиска ограничены до 100 публикаций.
Некоторые возможности доступны только премиум пользователям.
Необходимо оплатить подписку, чтобы пользоваться этим функционалом.
Фильтр
Тип публикаций
Хронология похожих публикаций:
Сначала новые
Похожие публикации не найдены
Сообщения
Найти похожие аватары
Каналы 0
Высокий
Название
Подписчики
По вашему запросу ничего не подошло