Грокаем C++
Открыть в Telegram
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов. По всем вопросам (+ реклама) @ninjatelegramm Менеджер: @Spiral_Yuri Реклама: https://telega.in/c/grokaemcpp Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Больше9 374
Подписчики
-224 часа
+27 дней
+1030 день
Архив постов
9 374
Доступ к приватным членам. Макросы
#новичкам
Доступ к приватным членам? Фуфуфу, это грязь! Да как вы смеете?! Хорошие люди старались, инкапсуляцию изобретали, а вы надругаться над ними хотите? Не по-славянски это, не по-православному...
Не далеки от правды слова выше. Если у вас уже есть какой-то работающий класс и вы хотите ужом извернуться, чтобы вытащить его кишки наружу - надо задуматься. О степени своей маниакальности, но главное - над архитектурой вашего кода. Потому что в большинстве случаев вы будете делать какое-то безобразие и разного рода хаки, чтобы подсмотреть в приватные поля. Лучше чуть подольше подумать и переработать целиком решение с учетом новых вводных.
Но!
Врага надо знать в лицо!
Поэтому в течение нескольких следующих постов мы будем обсуждать варианты инвазивного и неинвазивного доступа к приватным членам класса. Как говорится: не повторяйте в проде, чревато говнокодом по теории разбитых окон.
И на завтрак мы разберем самый простой способ. Макросы.
Есть у нас хэдэр:
// header.cpp
#pragma once
class X {
public:
X() : private_(1) { /.../
}
template <class T>
void f(const T &t) { /.../
}
int Value() { return private_; }
// ...
private:
int private_;
};
Подключаем его в цппшник, но перед этим делаем грязь:
#define private public
#include "source.h"
#include <iostream>
void Hijack( X& x )
{
x.private_ = 2;
}
int main() {
X x;
Hijack(x);
std::cout << "Hi, there! Hack has performed successfully" << std::endl;
}
Строчкой #define private public вы заменяете все нижележащие по коду вхождения слова private на public. Таким образом вы не трогаете хэдэр, но все равно имеете доступ к абсолютно всем его полям и методам.
И это работает! Да, стандартом запрещено заменять макросами ключевые слова. Но это вообще не волнует компиляторы. gcc даже c флагами -pedantic -Wall не выдает никаких предупреждений. clang только с флагом -pedantic генерирует варнинг.
Конечно же за такой макрос надо не то что по рукам бить. Надо их из жопы вырывать без анастезии и вставлять в нормальное место.
Пожалуй, это самый дурнопахнущих из всех способов, потому что ломает инкапсуляцию прям везде. Так сказать начали с вкуснятины. Но оставайтесь на свзяи, продолжение тоже будет вкусным.
Be legal. Stay cool.
#NONSTANDARD #badpractice9 374
🛠 Каждый C++-разработчик знает: здесь нельзя писать «на удачу». Одно неверное обращение к памяти — и всё падает. Если вам не хватает системности, уверенности в многопоточности, работы с сетью или просто хочется наконец разобраться, почему код ведёт себя именно так — это повышение квалификации станет апгрейдом вашего уровня.
👩💻 На курсе «C++ Developer. Professional» вы изучите язык до мельчайших деталей: от шаблонов и паттернов проектирования до принципов эффективного многопоточного кода и стандартов C++20 и 23. 14 практических работ, эксперты из индустрии и живая поддержка менторов помогут вам вывести свои проекты на уровень, который требует рынок.
Если вы хотите писать стабильный, быстрый и понятный код — научитесь мыслить как инженер, а не просто программист.
➡️ Пройдите короткое вступительное тестирование и получите скидку на обучение, старт совсем скоро: https://otus.pw/svusx/
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
9 374
Стандартные пользовательские литералы. Числовые
#новичкам
Числа подходят для инициализации многих сущностей. Метры, килограммы, градусы и тд. В стандартной библиотеке не так уж и много классов, значения которых можно представить числами. Но тем не менее они есть и литералы становятся довольно полезными при использовании.
Наибольшее распространение числовые пользовательские литералы получили при работе со временем.
Мы все привыкли писать:
5ч или 34мин. И начиная с С++14 мы примерно так и можем оперировать временем. Есть операторы преобразования целых и дробных чисел в годы, дни, часы, минуты, секунды, миллисекунды, микросекунды и наносекунды:
using namespace std::literals;
auto ns = 100ns; // наносекунды
auto us = 100us; // микросекунды
auto ms = 100ms; // миллисекунды
auto s = 100s; // секунды
auto min = 100min; // минуты
auto h = 24h; // часы
auto d = 42d; // дни
auto y = 12y; // года
Вы также можете верхнюю шестерку операторов использовать совместно в операциях:
auto time = 1h + 30min + 90s;Они совместимы и в результате получается объект общего типа(какой конкретно зависит от реализации, но скорее всего std::chrono::seconds в данном случае) В крутящемся на проде коде нечасто можно увидеть использование какого-то захардкоженного промежутка времени. Обычно такие штуки уносят в конфигурацию, чтобы иметь возможность подкрутить эти параметры без изменения кода. Однако литералы времени отлично можно применить в тестах. Например, хотите вы протестировать свой многопоточный шедулер. В качестве простого теста можно запихать в него лямбду, в которой установить значение промиса. А снаружи явно ждать установки значения:
std::promise<void> promise;
auto future = promise.get_future();
dispatcher_->schedule([&] { promise.set_value(); });
EXPECT_EQ(future.wait_for(100ms), std::future_status::ready);
если по истечению 100ms у фьючи не будет статуса "готово", то тест падает.
Интересный факт: оператор суффикс s конфликтует своим именем с оператором преобразования к строке. Но проблема решается автоматически разным типом аргументов. Никаких реальных конфликтов, обычная перегрузка:
std::string_literals::operator"" s(const char*, size_t)
std::chrono_literals::operator"" s(unsigned long long)
Ну и еще есть литералы для комплексных чисел:
using namespace std::literals;
auto c1 = 1.0 + 2.0i; // std::complex<double>(1.0, 2.0)
auto c2 = 3.0i; // std::complex<double>(0.0, 3.0)
auto c3 = 4.0if; // std::complex<float>(0.0f, 4.0f)
auto c4 = 5.0il; // std::complex<long double>(0.0L, 5.0L)
Работает там примерно так же, как в математике.
Не уверен, что кто-то этим пользуется. Но если пользуетесь, расскажите над каким проектом работаете, интересно же.
Be useful. Stay cool.
#cpp149 374
Стандартные пользовательские литералы. Строковые
#новичкам
Невзначай мы уже упоминали в предыдущих постах о существовании стандартных пользовательских литералов. Сегодня же плотнее о них поговорим и об их особенностях.
Первая особенность - для их использования не нужно подчеркивание впереди суффикса. Стандарт может позволить зарезервировать для себя такой формат, чтобы не было коллизий с нашими кастомными операторами. Ну и без underscore'а приятнее визуально.
Вторая особенность - нужно обязательно указывать
using namespace std::literals помимо включения нужных хэдэров. Кастомный оператор - это по сути обычная функция. И при вызове функции из какого-то пространства имен(а все стандартное лежит как минимум в неймспейсе std) мы должны перед именем функции указать это пространство. Но как вы это сделаете с оператором? Да никак. Поэтому явно нужно использовать в своем коде неймспейс. Он общий для всех стандартных операторов, но есть еще и подпространства под конкретные их группы.
В остальном, это те же кастомные литералы, только для стандартных типов. Подразделяются они по базовому типу литерала, к которому приписывается суффикс.
Строковые кастомные литералы
Интересно, что для них операторы принимают 2 параметра: указатель и длину:
( const char*, std::size_t )
Длина здесь без учета null-terminator'а. Компилятор при вызове оператора сам подставляет размер.
Есть всего 2 стандартных оператора, преобразующих c-style строку в объекты:
1️⃣ std::string:
constexpr std::string operator""s(const char* str, std::size_t len);
using namespace std::literals;
auto str = "Hello, World!"s;
static_assert(std::is_same_v<typename std::decay_t<decltype(str)>,
std::string>);
2️⃣ std::string_view:
constexpr std::string_view
operator ""sv(const char* str, std::size_t len) noexcept;
using namespace std::literals;
auto str = "Hello, World!"sv;
static_assert(std::is_same_v<typename std::decay_t<decltype(str)>,
std::string_view>);
Второй оператор вообще стоит применять примерно со всеми c-style строками в вашем проекте, чтобы они были обернуты в понятные объекты и можно было пользоваться адекватным интерфейсом.
У них у обоих есть одна особенность. Так как размер строки передается в оператор и этот размер потом используется для создания объекта, то есть некоторые отличия при создании объектов через конструктор и через оператор:
void print_with_zeros(const auto note, const std::string& s) {
std::cout << note;
for (const char c : s)
c ? std::cout << c : std::cout << "₀";
std::cout << " (size = " << s.size() << ")\n";
}
int main() {
using namespace std::string_literals;
std::string s1 = "abc\0\0def";
std::string s2 = "abc\0\0def"s;
print_with_zeros("s1: ", s1);
print_with_zeros("s2: ", s2);
}
// OUTPUT:
// s1: abc (size = 3)
// s2: abc₀₀def (size = 8)
Во втором случае получилась строка длиннее, чем в первом. Почему?
Для s1 вызывается конструктор от одного аргумента:
basic_string( const CharT* s, const Allocator& alloc = Allocator() );Он конструирует строку из c-style строки и не знает ее настоящий размер. Поэтому он считает null-terminator концом строки. Для
s2 вызывается конструктор от двух аргументов:
basic_string( const CharT* s, size_type count,
const Allocator& alloc = Allocator() );
Теперь конструктор знает реальную длину строки и аллоцирует столько памяти, сколько нужно, чтобы поместить весь литерал в строку.
Для обычных строк, типа "Hello, World!" разницы не будет. Но если вы используете какие-то бинарные данные, то разница существенна.
Остальные стандартные литералы не уместились в ограничения телеги, поэтому будет вторая часть.
See the difference. Stay cool.
#cpp11 #cpp179 374
👩💻 Всем программистам посвящается!
Вот 14 авторских обучающих IT каналов по самым востребованным областям программирования:
Выбирай своё направление:
👩💻 C/C++ — https://t.me/cpp_ready
👩💻 C# & Unity — t.me/csharp_ready
👩💻 Linux — t.me/linux_ready
👩💻 Python — t.me/python_ready
🤔 InfoSec & Хакинг — t.me/hacking_ready
🖥 SQL & Базы Данных — t.me/sql_ready
🤖 AI & ML — t.me/neuro_ready
👩💻 Frontend — t.me/frontend_ready
👩💻 IT Новости — t.me/it_ready
👩💻 Java — t.me/java_ready
📖 IT Книги — t.me/books_ready
📱 JavaScript — t.me/javascript_ready
🖼️ DevOps — t.me/devops_ready
🖥 Design — t.me/design_ready
📌 Гайды, шпаргалки, задачи, ресурсы и фишки для каждого языка программирования!
9 374
Забавный факт про std::unordered_map
#опытным
std::unoredered_map обязана работать на базе хэш-таблицы, чтобы удовлетворить требованиям по ассимптотическое сложности ее операций.
А хэш-таблицы обязаны использовать какой-либо механизм разрешения коллизий, которые случаются, когда хэш для двух ключей получается одинаковым. Они могут быть разные: линейное пробирование, двойное хэширование, round robin hashing и тд. Стандарт обычно описывает только требования к контейнерам, не погружаясь в детали реализации. Но в случае std::unordered_map он четко зафиксировал использование метода бакетов, когда каждая ячейка таблицы хранить связный список элементов, у которых одинаковый ключ.
При обычном итерировани по неупорядоченной мапе мы используем всем знакомый range-based for и обычные итераторы(под капотом этого форика):
std::unoredered_map<std::string, int> map = ...;
for (const auto& [key, value]: map) {
...
}
Но это не единственный способ итерироваться по мапе!
У нее есть пара перегрузок методов begin() и end(), который принимают индекс бакета. И они позволяют итерироваться четко внутри него:
local_iterator begin( size_type n ); local_iterator end( size_type n );Количество бакетов мы получаем через метод bucket_size и готово, мы получили альтернативную итерацию по контейнеру!
std::unordered_map<std::string, int> word_count = {
{"AI", 5}, {"evil", 7}, {"banana", 3},
{"date", 2}, {"elderberry", 4}
};
// Iterate over backets
for (size_t i = 0; i < word_count.bucket_count(); ++i) {
std::cout << "Bucket " << i << " ("
<< word_count.bucket_size(i) << " elements): ";
// Iterate inside certain backet
for (auto it = word_count.begin(i); it != word_count.end(i); ++it) {
std::cout << "[" << it->first << ":" << it->second << "] ";
}
std::cout << std::endl;
}
Вывод:
Bucket 0 (0 elements): Bucket 1 (0 elements): Bucket 2 (2 elements): [date:2] [evil:7] Bucket 3 (0 elements): Bucket 4 (0 elements): Bucket 5 (2 elements): [elderberry:4] [banana:3] Bucket 6 (0 elements): Bucket 7 (0 elements): Bucket 8 (0 elements): Bucket 9 (0 elements): Bucket 10 (0 elements): Bucket 11 (1 elements): [AI:5] Bucket 12 (0 elements):Пользы в этом немного, но может помочь, например, в отладке своей кастомном хэш-функции, чтобы добиться равномерного распределения. Inspect your solutions. Stay cool. #cpp11
9 374
+4
⚡️ Linux теперь в Telegram!
Ребята сделали крутейший канал про Linux, где на простых картинках и понятном языке обучают работе с этой ОС, делятся полезными фишками и инструментами
Подписывайтесь: @linuxos_tg
9 374
Привычно возводим в степень
Спасибо, @Ivaneo, за любезно предоставленную идею для поста.
Как же бесило в универе, что в С/С++ нет нормального оператора возведения в степень, приходится использовать библиотечную std::pow. В других же языках такое есть. Например в питоне и рубях это оператор
**(2 ** 3), а в Lua и Julia - оператор ^(2 ^ 3).
В С++ мы такого в общем виде получить, к сожалению, не можем. Но можем сделать даже лучше в очень определенном сценарии.
И в этом нам помогут пользовательские литералы. Смотрите сами:
long double operator ""_²(long double d)
{
return d * d;
}
int main()
{
auto d = 2.0_²;
std::cout << d << "\n";
}
// OUTPUT:
// 4
Берем уникод символ двойки верхнего регистра и делаем его суффикстом пользовательского литерала. И получаем почти привычную работающую версию возведения в квадрат! Это конечно не совсем стандарт, но на основных компиляторах работает.
А если еще и суффикс убрать:
long double operator ""²(long double d)
{
return d * d;
}
int main()
{
auto d = 2.0²;
std::cout << d << "\n";
}
То будет вообще огонь! Прям как в школе учили.
Да, суффиксы без андерскора запрещено использовать, так как они зарезервированы для стандарта. Но тем не менее это работает с варнингами на gcc и msvc, но уже не собирается на кланге.
Забавный примерчик. Жаль, что это может работать только для литералов и не распространяется на все переменные.
Provide better solutions. Stay cool.
#fun9 374
Пользовательские литералы. А зачем?
#опытным
В прошлый раз мы поговорили о том, что такое пользовательские литералы. Сегодня поговорим о плюшках, которые могут дать user defined literals.
Поехали:
🥨 Они позволяет ввести адекватные легкочитаемые преобразование литералов в объекты классов. Не оборачивать все в конструкторы классов с кучей неймспейсов впереди, а просто добавив короткий суффикс. Тут все зависит от прикладной области, но можно легко придумать что-то вот такое:
сломаться вывести не тот тип, который вы ожидаете, если вы работаете с сырыми литералами. Пользовательский литерал же сразу на месте конструирует нужный объект и компилятор будет правильно интерпретировать его тип.
Особое внимание касательно этого пункта стоит обратить на строковые литералы. Их в 100% случаев нужно оборачивать в string_view. А с пользовательскими литералами это дело несложное:
auto color1 = Color::from_html("#FF8800");
auto color2 = "#FF8800"_color;
Меньше деталей, больше фокуса на происходящем.
🥨 Предотвращают сочетание несочетаемого. Иногда в коде сложно определиться с типами переменных, особенно при обильном использовании auto. Поэтому легко может произойти такая ситуация, что вы возьмете и будете совместно оперировать синтаксически одинаковыми типами, но на деле они будут обозначать разные вещи. Условно, будем складывать градусы и радианы:
double quadrant = math_constants::Pi / 2;
SomeMathCalculation(quadrant + 30.); // 30 is arc degree
Получится неожиданный результат, даже если функция работает верно.
Вот шобы такого не было, можно использовать соответствующие литералы:
class Radian {...};
Radian operator ""_deg(long double d)
{
return Radian{d*M_PI/180};
}
SomeMathCalculation(radian + 30._deg); // OK
SomeMathCalculation(radian + 30.); // Compiler error
🥨 Автоматический вывод типов может легко using namespace std::literals::string_view_literals;
constexpr std::array array1 = {"I", "love", "C++"};
static_assert(std::is_same_v<typename std::decay_t<decltype(array1[0])>,
const char >);
constexpr std::array array2 = {"I"sv, "love"sv, "C++"sv};
static_assert(std::is_same_v<typename std::decay_t<decltype(array2[0])>,
std::string_view>);
Дописываем в конце строкового литерала sv и вот у вас в руках вьюха на строку. И компилятор корректно определяет тип элемента массива как вьюху.
🥨 Если вы хотите передать вашу строку, как NTTP в шаблон и что-то посчитать с ней в компайл-тайме - удачи, дело это нетривиальное. Но с С++20 это можно сделать через прокси класс:
template<size_t N>
struct FixedString {
char data[N];
constexpr FixedString(const char (&str)[N]) {
std::copy_n(str, N, data);
}
constexpr const char c_str() const { return data; }
constexpr size_t size() const { return N - 1; }
};
template <FixedString str>
class Class {};
Class<"Hello World!"> cl;
И тут уже открываются просторы для реальных компайл-тайм вычислений над строками. И в этом также могут помочь кастомные литералы. Для примера можете посмотреть на видео от think-cell, как они работают со строковыми user-defined litarals: жмак.
В общем, крутая штука и нужно пользоваться. Если у вас есть свои примеры, пишите в комментах, интересно будет посмотреть.
Be useful. Stay cool.
#cppcore #cpp11 #cpp209 374
👩💻 Открытый урок «Lock-free в C++: Без блокировок к высокой производительности».
🗓 09 февраля в 20:00 МСК
🆓 Бесплатно. Урок в рамках старта курса «C++ Developer. Professional».
Мьютексы и блокировки долгое время считались стандартом синхронизации в C++. Но по мере роста нагрузки именно они всё чаще становятся узким местом и тормозят масштабирование многопоточных систем.
Что будет на вебинаре:
На открытом уроке разберём, как устроено lock-free программирование и почему современные C++-приложения всё чаще отказываются от классических блокировок. Поговорим о том, какие задачи действительно выигрывают от lock-free подхода и какие механизмы предоставляет стандарт C++ для работы с атомарными операциями и памятью.
Кому будет интересно:
• Начинающим программистам C++
• Программистам на других языках, которые хотят сравнить подходы к многопоточности в их технологии с подходами в C++
🔗 Ссылка на регистрацию: https://otus.pw/F2yoq/
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
9 374
Пользовательские литералы
#новичкам
Последние несколько постов прям намекали, чтобы мы рассказали про пользовательские литералы(да и в комментах о них много говорили), поэтому here we are.
В наследство от С плюсам достались тривиальные типы и их литералы. Литералы - это способ записать готовое значение типа в коде. Литералы бывают:
👉🏿 Целочисленные:
5, 42, 0xFF.
👉🏿 С плавающей точкой: 3.14, 6.02e23.
👉🏿 Символьные: 'a', '\n'.
👉🏿 Строковые: "Hello, world!".
👉🏿 Логические: true, false.
👉🏿 Мало кто про это знает, но есть еще и литерал типа указателя - nullptr.
Литералы также имеют фиксированный набор суффиксов, которые определяют их итоговый тип. Например, суффикс 'u' или 'U' для беззнакового целого, 'l' или 'L' для long, 'll' или 'LL' для long long, 'f' или 'F' для float. Суффикс также является полноправной частью литерала.
Прекрасная история, но эта история про тривиальные базовые типы. Никаких объектов.
А мы живем все-таки в мире объектов. И на стыке мира объектов и литералов тривиальных типов могу возникать конфузы, как в последнем WAT'е.
Но смотрите, что мы имеем. Число 42 в зависимости от суффикса может представлять разный числовой тип. Базовый тип целочисленного литерала - int. Но приписав U, получим unsigned int и тд.
То есть в С++ давно был механизм, с помощью которого можно было изменять тип литерала через суффикс. Стоит лишь дать возможность программистам самостоятельно определять свои суффиксы, чтобы по-своему интерпретировать литерал.
Это и сделали с С++11. Теперь мы можем определять свои пользовательские литералы с помощью нового оператора определения суффикса!
Допустим, моя программа много работает с градусами температуры. Мне нужно уметь работать с кельвинами, цельсиями и фаренгейтами. Для единообразия и точности для температуры у меня будет один класс и мне надо его научить работать с разными единицами изменения. Я конечно могу оборачивать чиселки в промежуточные классы, чтобы различать разные системы, или постоянно использовать фабричные функции, типа Temperature::from_kelvin. Но это прям больно как-то. Вместо этого можно определить пользовательские литералы:
class Temperature {
private:
double kelvin; // for precicion and consistency
explicit Temperature(double k) : kelvin(k) {
if (k < 0) {
throw std::invalid_argument("Temperature cannot be below zero");
}
}
public:
static Temperature FromKelvin(double k) {
return Temperature(k);
}
static Temperature FromCelsius(double c) {
return Temperature(c + 273.15);
}
static Temperature FromFahrenheit(double f) {
return Temperature((f - 32.0) * 5.0/9.0 + 273.15);
}
// a bit more member functions for making it works
};
Temperature operator"" _kelvin(long double value) {
return Temperature::FromKelvin(static_cast<double>(value));
}
Temperature operator"" _celsius(long double value) {
return Temperature::FromCelsius(static_cast<double>(value));
}
Temperature operator"" _fahrenheit(long double value) {
return Temperature::FromFahrenheit(static_cast<double>(value));
}
{
auto t1 = Temperature::FromKelvin(0);
auto t2 = Temperature::FromCelsius(25);
auto t3 = Temperature::FromFahrenheit(98.6);
auto avg_temp = (Temperature::FromKelvin(20) + Temperature::FromCelsius(30)) / 2.0;
}
{
auto t1 = 0._kelvin;
auto t2 = 25._celsius;
auto t3 = 98.6_fahrenheit;
auto avg_temp = (20_kelvin + 30_celsius) / 2.0;
}
Обратите на форму operator"". Он может возвращать что угодно и принимать какой-то из базовых типов литералов. Операторы различаются суффиксами. Пользовательские суффиксы обязаны начинаться с подчеркивания, потому что суффиксы без подчеркивания зарезервированы для стандарта.
Просто посмотрите, насколько сократился код, уменьшилось количество скобок и увеличилась читаемость. Выглядит круто.
Это было небольшое интро, в следующий раз рассмотрим кейсы, когда пользовательские литералы могут принести реальную пользу.
Extend your capabilities. Stay cool.
#cppcore #cpp119 374
Как получить длину строкового литерала?
#опытным
Казалось бы, довольно простой вопрос. Обернем в строку и вызовем метод size:
size_t length = std::string("Hello, subscribers!").size();
Ну или на худой конец вызовем strlen:
size_t length = strlen("Hello, subscribers!");
Но я считаю, это не по-современному.
С++ давно идет в сторону расширения возможностей вычислений в compile-time. Поэтому если что-то можно вычислить во время компиляции, то это нужно сделать именно там! Ни грамма лишнего времени вычислений не потратим.
Давайте посмотрим, как можно найти длину строкового литерала во время компиляции.
1️⃣ Кастомщина. Хочешь что-то сделать хорошо, сделай это сам. Не факт, что получится хорошо, но ты старался:
template<size_t N>
constexpr size_t string_length(const char (&str)[N]) {
return N - 1; // do not count null terminator
}
constexpr size_t len = string_length("Hello, subscribers!");
Реальный тип строкового литерала не const char *, а константный массив символов. Поэтому через шаблон мы можем подтянуть размер массива через NTTP-параметр шаблона и вернуть его наружу.
2️⃣ Используем sizeof. Этот оператор возвращает длину массива во время компиляции. Единственное, что он считает терминирующий символ, поэтому все равно вокруг него надо обертку писать, чтобы единичку нигде не потерять:
template<size_t N>
constexpr size_t string_length(const char (&str)[N]) {
return sizeof(str) - 1; // do not count null terminator
}
constexpr size_t len = string_length("Hello, subscribers!");
Эх, а так хотелось готового get-to-go решения. Погодите...
3️⃣ Обернуть не в строку, а в string_view и вызвать метод size(). Конструкторы вьюхи изначально с С++17 были constexpr, как и сам метод size(), поэтому просто берем и пишем:
constexpr size_t len = std::string_view("Hello, subscribers!").size();
Просто, работает из коробки и знакомо всем.
4️⃣ Да зачем что-то менять в коде, это для слабаков. Поменяем стандарт и все заработает в compile-time! Ну точнее конструктор std::string и метод size() в С++20 теперь тоже constexpr:
constexpr size_t len = std::string("Hello, subscribers!").size();
Пысы: я не просто вызываю какие-то функции в надежде, что они выполнятся в compile-time. Тот факт, что len - constexpr переменная, требует, чтобы компилятор вычислил выражение справа во время компиляции.
5️⃣ Тот пункт, который и вдохновил на написание этого поста. Все пункты выше либо надо было самим реализовывать, либо вот какие-то обертки, чтобы хакнуть систему и на самом деле не работать с литералами.
Но не так плюсы бедны на стандартные решения. Есть стандартная С++17 функция std::char_traits<char>::length. Она может работать в compile-time, имеет явную семантику вычисления длины и работает чисто с c-style строками:
constexpr size_t len = std::char_traits<char>::length("Hello, subscribers!");
Красиво? Ну а что вы от плюсов хотели?) Зато из коробки работает.
6️⃣ Пользовательские литералы. Еще один неординарный способ. С С++11 мы имеем возможность превращать численные и строковые литералы в пользовательские объекты с помощью дописывания суффикса. Прикольно же писать:
constexpr auto length = "Hello, subscribers!"_len;
Коротко и понятно. Для этого нужно лишь определить оператор преобразования:
constexpr size_t operator"" _len(const char* str, size_t n) {
return n;
}
и теперь вы свободны от угнетения оберток.
Если есть еще идеи, кидайте в комменты, будет интересно.
Don't be oppressed. Stay cool.
#cpp11 #cpp17 #cpp209 374
WAT
#опытным
Спасибо, ₿ Satoshic, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.
Можно ли сравнить одинаковые объекты и получить результат, что они не равны? В С++ можно все.
Делаем вот так:
школьную математеку CTAD. Какой тип элементов массива выведется?
Правильно, const char *.
А как std::ranges::find сравнивает такие элементы?
Правильно, по правилам сравнения указателей. Не по содержимому объектов, а по их адресам. Если адреса одинаковые, то два указателя равны. Нет - не равны.
Первый ассерт не сработал, потому что в массиве и при поиске стоит один и тот же строковый литерал "C++", на место которого компилятор подставит один и тот же адрес.
Второй ассерт не срабатывает, потому что мы явно сравниваем сишные строки через strcmp, то есть их содержимое.
А вот последний ассерт просто говорит о том, что указатель
constexpr std::array array = {"I", "love", "C++"};
int main() {
if (auto iter = std::ranges::find(array, "C++"); iter == std::end(array)) {
assert(false && "comptime arg");
}
// let's go with runtime now
if (setenv("RUNTIME", array[2], 0) != 0) {
assert(false && "setenv");
}
char *runtime_str = getenv("RUNTIME");
assert(strcmp(runtime_str, array[2]) == 0 && "equal strings");
if (auto magick_iter = std::ranges::find(array, runtime_str);
magick_iter == std::end(array)) {
assert(false && "runtime arg");
}
}
Определяем массив строк и в начале ищем в нем элемент, значение которого известно на момент компиляции.
Дальше определяем переменную окружения RUNTIME со значением третьего элемента массива.
После получаем значение этой переменной и сравниваем ее с оригиналом.
Ну и в конце ищем среди массива строку эту runtime_str.
Казалось бы, никакие ассерты не должны выстрелить. Мы просто занимается типичной программерской работой - перекладываем одно и то же значение в разные места и сравниваем. Одинаковые объекты должны быть равны.
Но нет! Не равны. Программа зафейлится с ассертом "Assertion `false && "runtime arg"' failed."
WAT?! Почему мы не можем найти строку "С++" в массиве, если она там очевидно есть?
Дьявол кроется в деталях.
Вспоминаем runtime_str не был найден в массиве, потому что там нет такого адреса.
И это нормально, ведь когда мы получаем указатель на значение переменной окружения - этот указатель указывает на динамически выделенную память в окружении процесса. А литерал "С++" указывает на секцию read-only данных.
В общем, суть в том, что эти указатели имеют просто разные адреса, поэтому они и не одинаковы.
Так что аккуратно используйте CTAD с сишными строками, может привести к интереснейшему каскаду удивительнейших багов.
Express your wishes precisely. Stay cool.
#cppcore #cpp179 374
std::to_array
#опытным
std::array - прекрасная бесплатная обертка над сишными массивами. Но с ними есть один нюанс. При определении объекта массива нужно либо передавать оба шаблонных параметра(тип и размер):
std::array<int, 4> arr = {1, 2, 3, 4};
либо надеяться на CTAD и не передавать никаких параметров:
std::array arr = {1, 2, 3, 4};
Если так сложились звезды, что у вас тип инициализатора совпадает с желаемым типом элементов массива, то вам прекрасно подойдет последний вариант.
И вот здесь всплавает тот самый нюанс.
Не всегда тип инициализатора совпадает с типом элемента массива. Например я инициализирую строковыми литералами, а в массиве храню string_view. То есть мне нужно явно указать первый шаблонный параметр. Но второй я указывать не хочу! Я не хочу отбирать у компилятора работу по автоматическому выводу размера массива по количеству аргументов, которое я передал. А то вдруг передам меньше чем нужно и получу автозаполнение нулями. Пишу:
std::array<std::string_view> arr = {"La", "Bu", "Bu"};
и получаю ошибку компиляции. Раз уж начал указывать шаблонные параметры, изволь и указать все.
Мне конечно не жалко написать эту жалкую тройку, пальцы не сотрутся. Но зачем? Компилятор же может определить размер и это уменьшит количество потенциальных ошибок.
У проблемы есть решение и начиная с С++20 оно является стандартом. Это функция std::to_array:
namespace detail {
template <class T, std::size_t N, std::size_t... I>
constexpr std::array<std::remove_cv_t<T>, N>
to_array_impl(T (&a)[N], std::index_sequence<I...>) {
return {{a[I]...}};
}
} // namespace detail
template <class T, std::size_t N>
constexpr std::array<std::remove_cv_t<T>, N> to_array(T (&a)[N]) {
return detail::to_array_impl(a, std::make_index_sequence<N>{});
}
Идея такая: передаем в функцию элементы будущего массива с синтаксисом std::initializer_list. Комилялятор это парсит в сишный массив и автоматически выводит его длину в шаблонном параметре N. А дальше с помощью шаблонной магии с вариадик шаблонами правильно раскрываем сишный массив в инициализацию std::array.
Применяется std::to_array так:
constexpr auto names = std::to_array<std::string_view>({"Goku", "Luffy", "Ichigo", "Gojo", "Joseph", "L"});
Да, пишем на пару символов больше, зато размер будет четко соблюдаться.
Automate your tools. Stay cool.
#cpp209 374
Если бы рост в IT был лестницей, большинство было бы Senior.
Но на собеседованиях выясняется, что опыт, стаж и “я уже Middle” почти ничего не решают.
Илья Шишков 11 лет работал в Яндексе и провёл 250+ интервью и видел это постоянно. В канале @imhired разбирает, по каким признакам кандидатов относят к Junior, Middle и Senior - и почему многие готовятся совсем не к этому.
Начни с первого файла👇
(руководство по решению любой алгори...)
9 374
Прелести рэнджей
#опытным
Рэнджи - это не просто сахар и "уродливые" палки pipe-синтаксиса, как думает некоторая часть плюсовиков. Это еще и в том числе про скорость и выразительность.
Возьмем пример из прошлого поста:
std::string text = "one two three four";
std::vector<std::string_view> strs;
boost::split(strs, text, boost::is_any_of(" "));
Чтобы разделить всю строку на по пробелам, нужно создавать отдельный вектор. А это как минимум одна аллокация. Плюс на расширение вектора уйдут аллокации, плюс алгоритм сам может выделять память.
И на самом деле мы получаем кучу аллокаций.
Ну а что если нам нужна только третья подстрока? Каким бы красивым и проверенным не был бы вызов boost::split, он будет делать лишнюю работу + выделять память.
Можно конечно самим написать решение с циклом или подряд использовать std::string::find и это будет работать. Но там нужно аккуратно работать с индексами, желательно еще и отдельно тестировать этот код.
Но можно поступить проще и воспользоваться ренджами!
Рэнджи ленивые. Прям как студенты: всем на последнем курсе заранее дают задачу написать диплом, но обычно его начинают писать, когда научрук уже дает последнее китайское предупреждение вслед за пендалем, что нужно сделать работу уже вчера.
Но у рэнджей это скорее преимущество.
Пока у диапазона не спросишь следующий элемент - он его не вычислит. И в этом прелесть.
Мы просто пишем:
auto range = text | std::views::split(' ');
И абсолютно ничего не происходит! Но мы декларируем, что хотим получить поддиапазоны исходного текста, которые получены разбиением по разделителям.
С помощью ренджей мы даже можем оставить конкретный интересующий нас поддиапазон:
auto range = text | std::views::split(' ') | std::views::drop(2) | std::views::take(1);
Отбрасываем первые два элемента и берем только первый оставшийся. И опять же, ничего не происходит.
Вычисления происходят, когда мы пытаемся что-то узнать о результирующем диапазоне, например, пустой ли он:
if (range.empty()) {
std::cerr << "There are less than three words in text";
}
Если итоговый поддиапазон непустой, то в нем должен быть лишь один элемент, являющийся поддиапазоном оригинальной строки. То есть по сути легковесный view на нужную подстроку:
std::string_view str{*range.begin()};
std::cout << str << std::endl;
// OUTPUT: three
И здесь нет ни одной алллокации! Чисто работа с поддиапазонами. Это ровно то, чтобы мы бы делали руками через find, только в понятном декларативном стиле.
Возможно find работал бы и быстрее(а это еще проверить надо), но рэнджи предлагают более понятную альтернативу самописному коду.
Be expressive. Stay cool.
#cpp209 374
split
#опытным
Продолжаем рассказывать, как в плюсах можно делать то же самое, что всегда можно было делать в питоне и во всех современных языках.
Вот надо мне разделить слова в предложении по пробелам. Я просто беру и пишу:
простейшую вещь разделить строку на С++?
Стандартных алгоритмов, делающих split нам не завезли, поэтому можно воспользоваться нестандартными. Например, бустом:
text2 = "one two three four"
parts2 = text2.split()
print(parts2) # ['one', 'two', 'three', 'four']
А как сделать std::string text = "one two three four";
std::vector<std::string> strs;
boost::split(strs, text, boost::is_any_of(" "));
for (const auto &item : strs) {
std::cout << item << " ";
}
// OUTPUT: one two three four
Это прекрасно работает. Но кто-то не хочет тянуть к себе буст, кому-то не нравятся output параметры. В общем решение неидеальное, как пицца без мяса.
Поэтому люди городили свои огороды через find, стримы и прочее.
Но хочется чего-то родного.. Чего-то стандартного...
И аллилуя! В С++20 появились рэнджи, вместе с std::views::split:
auto range = text | std::views::split(' ');
for (const auto &item : range) {
std::cout << item << " ";
}
// OUTPUT: one two three four
Если вам нужен вектор значений, чтобы по индексам получать доступ, можно сделать так:
auto strs = text
| std::views::split(' ')
| std::ranges::to<std::vector<std::string>>();
std::print("{}", strs);
// OUTPUT: ["one", "two", "three", "four"]
Правда здесь уже нужен С++23 с его std::ranges::to.
У всех примеров есть особенность: если в исходной строке подряд идут несколько разделителей, то в результат попадают пустые строчки. Если вам так не нравится, то используйте std::views::filter:
std::string text = "one two three four";
auto strs = text | std::views::split(' ') |
std::views::filter(
[](auto &&sub_range) { return !sub_range.empty(); }) |
std::ranges::to<std::vector<std::string>>();
std::print("{}", strs);
// OUTPUT: ["one", "two", "three", "four"]
Все примеры можете найти здесь.
И жить стала еще чуть прекрасней)
Never too late. Stay cool.
#cpp20 #cpp239 374
👩💻 Паттерны проектирования на С++
Приглашаем на открытый урок.
🗓 28 января в 20:00 МСК
🆓 Бесплатно. Урок в рамках старта курса «C++ Developer. Professional».
На вебинаре сформируем чёткое понимание паттернов проектирования в C++, покажем их реальную ценность в разработке и продемонстрируем, как применять их в повседневных задачах.
Разберём несколько интересных и полезных паттернов для повседневности:
✔ Строитель (Builder)
✔ Адаптер (Adapter)
✔ Легковес (Flyweight)
✔ Команда (Command)
✔ Мементо (Memento)
🔗 Ссылка на регистрацию: https://otus.pw/YR3R/
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
9 374
Предотвращаем висячие ссылки
#опытным
Давайте снова взглянем на этот пример:
struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() { return items_; }
~Foo() { std::cout << "delete" << std::endl; }
private:
std::vector<int> items_;
};
Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}
for (int x : generateData().items()) {
process(x);
}
Проблема ведь тут не то, чтобы в цикле. Если я сделаю вот так:
auto& vec = generateData().items();
Я тоже получу висячую ссылку. И здесь уже никакой С++23 не поможет, будет UB, не сомневайтесь.
Можно, конечно, сказать: "не пишите такой код". Но это совет из оперы "нормально делай - нормально будет". Программисты часто косячат и, хоть пальцы им ломай, ничего вы с этим не сделаете.
Хотя кое-что сделать можно. Есть хорошая фраза: "код надо проектировать так, чтобы им нельзя было неправильно воспользоваться". А у нас как раз такая ситуация: для lvalue объекта все будет работать, а для rvalue - уже нет.
Благо в С++ есть возможность исправить этот косяк дизайна несколькими способами.
Например, использовать С++11 ref-qualified перегрузки методов. Вы можете определить 2 метода: один будет вызываться на lvalue объектах, другой на rvalue:
struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
std::vector<int>& items() & { return items_; }
std::vector<int> items() && { return std::move(items_); }
~Foo() { std::cout << "delete" << std::endl; }
private:
std::vector<int> items_;
};
На lvalue метод будет возвращать обычную ссылку. А для rvalue - вектор по значению, в который мувнет свой items_.
Объект все равно скоро разрушиться. Зачем ему до последнего вздоха хранить вектор и никому его не отдавать, если он может позволить ему дальше жить эту прекрасную жизнь?
И это действительно решает проблему.
Второй способ из той же оперы, но в модной обертке. В С++23 завезли deducing this, который позволяет определить один метод, который по-разному будет работать для lvalue и rvalue объектов. Единственное, что останавливает - такой метод должен возвращать один и тот же тип на все случаи жизни, а мы здесь возвращаем по ссылке и по значению. Обойти это можно с использованием C++20 отображений ranges:
struct Foo {
Foo(std::vector<int> && vec) : items_{std::move(vec)} {}
// deducing this
auto items(this auto&& self) {
return std::views::all(std::forward<decltype(self)>(self).items_);
// if self is lvalue std::views::all is non-owning view,
// and if self is rvalue then std::views::all is owning view
}
~Foo() { std::cout << "delete" << std::endl; }
private:
std::vector<int> items_;
};
std::views::all внутри себя умеет решать, становиться ей владеющей вьюхой или нет. Нам лишь нужно добавить deducing this и правильный форвард, чтобы пробросить тип.
Это также прекрасно решает проблему.
Prevent misuse. Stay cool.
#cpp11 #cpp20 #cpp23 #goodpractice9 374
Продлеваем жизнь временного объекта range based for
#опытным
На самом деле у проблемы в этом коде:
Foo generateData() {
return Foo{std::vector{1, 2, 3, 4, 5}};
}
for (int x : generateData().items()) {
process(x);
}
есть еще более простое решение.
Просто перейдите на С++23 и в коде не будет UB! Теперь время жизни всех временных объектов, которые нужны для получения итерируемой коллекции, продлеваются до конца цикла.
Стоит обратить внимание, что даже в C++23 параметры-не-ссылки промежуточных вызовов функций не получают продления времени жизни (поскольку в некоторых ABI они уничтожаются в вызываемой функции, а не в вызывающей), но это является проблемой только для функций, которые и так содержат ошибки:
using T = std::list<int>;
const T& f1(const T& t) { return t; }
const T& f2(T t1) { return t1; } // всегда возвращает висячую ссылку
T g();
void foo()
{
for (auto e : f1(g())) {} // OK: время жизни возвращаемого значения g() продлено
for (auto e : f2(g())) {} // UB: локальный объект t1 функции f2 все равно разрушается при выходе из скоупа f2
}
Можно сказать, что продлевается жизнь только тех объектов, которые созданы в скоупе функции, содержащей сам цикл.
Вроде круто, но задумайтесь на секунду.
UB для обычного С++ программиста со стороны выглядит вот так: он меняет компилятор, какие-то флаги компилятора или компилирует на другой платформе и поведение программы меняется.
То есть с точки зрения стандарта UB ушло, но код меняет поведение в зависимости от флагов, что делает его менее предсказуемым и нужно знать все эти детали.
А как вы думаете: полезное изменение?
👍 если полезное, ☃️ если лучше бы оставили уб и не давали расслабиться программистам.
Solve problems. Stay cool.
#cpp23
Уже доступно! Исследование Telegram 2025 — ключевые инсайты года 
