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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Mutable. А зачем?
#опытным

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

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

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

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

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

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

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

class ThreadSafeLogger {
explicit ThreadSafeLogger(std::shared_ptr<CallCountMetric> metric) : call_count{metric} {}
std::shared_ptr<CallCountMetric> 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
🔥26👍1361😁1
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
23👍15🔥12💯2
Обзор книжки #2

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

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

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

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

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

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

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

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

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

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

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

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

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

Be lucky. Stay cool
322👍14🔥6😁4👎2
Удобно сравниваем объекты
#опытным

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

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.iss.one/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
1👍9517🔥125❤‍🔥2🥱2
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>& t ) noexcept;

template< class T >
constexpr T&& forward(std::remove_reference_t<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<decltype(obj)>(obj).member;


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

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

} };


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

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

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

Follow the head. Stay cool.

#cpp23 #template
4🔥22👍74😁2
Идеальная передача из лямбды
#опытным

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

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<decltype(self)>(message));
};


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

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

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

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

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

Be a major figure. Stay cool.

#template #cpp23
2🔥21👍85😁1
Макросы
#новичкам

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

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


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

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


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

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

int x = 5;
int y = 10;

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


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

Result: 12
x: 6
y: 12


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

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


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

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

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

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

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

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

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

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

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

Don't be confusing. Stay cool.

#cppcore
1👍33😁114🔥3❤‍🔥1
Итоги конкурса

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

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

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

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

Be lucky. Stay cool.
🎉28👍9👏96🗿1
Сравниваем производительности оператора<
#опытным

В этом посте я рассказал об отличном способе лексикографического сравнения набора объектов с помощью 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<int> dist(1,100);

template <typename TimeClass>
static void time_comparison_experiment(benchmark::State& state) {
std::vector<TimeClass> 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<std::chrono::duration<double>>(
end - start);
state.SetIterationTime(elapsed_seconds.count());
std::shuffle(v.begin(), v.end(), rng);
}
}

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

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

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

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


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

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

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

Compare things. Stay cool.

#performance #cpp20
👍137🔥61
Виртуальные функции в 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
1126🔥12👍9🤯4🤔2🐳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<class ErrorCodeEnum>
constexpr error_code(ErrorCodeEnum e) noexcept;

constexpr void assign(int val, const error_category& cat) noexcept;
template<class ErrorCodeEnum>
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
18👍13🔥101
Еще одно отличие С и С++
#опытным

Продолжаем рубрику, где мы развеиваем миф о том, что С - это подмножество С++. Вот предыдущие части: тык, тык и тык.

В С давно можно инициализировать структуры с помощью так называемой designated initialization. Эта фича позволяет при создании массива или экземпляра структуры указать значения конкретным элементам и конкретным полям с указанием их имени!

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

int array[100] = {[13] = 1, [45] = 1, [79] = 1};


В плюсах такое можно сделать только с помощью нескольких инструкций.

int array[100] = {};
array[13] = array[45] = array[79] = 1;


Не так удобно.

Можно даже задавать рэндж значений. Но это правда GNU расширение.

int array[100] = {[13] = 1, [30 ... 40] = 1, [45] = 1, [79] = 1};


Теперь элементы с 31 по 41 будут инициализированы единичками. Очень удобно!

Для структур задавать значения полям можно вот так:

struct point { int x, y, z; };

struct point p1 = { .y = 2, .x = 3 };
struct point p2 = { y: 2, x: 3 };
struct point p3 = { x: 1};


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

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

Так что вот вам еще один пример, которым вы сможете парировать интервьюера на вопрос: "верно ли что С - подмножество С++?". Иначе где вам это еще пригодится?

Be different. Stay cool.

#goodoldc #cppcore #cpp20 #interview
1🔥29👍106❤‍🔥1
Designated initialization
#новичкам

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

Эта фича С++20, которая позволяет явно указывать поля, которым присваиваются значения, при создании объекта.

struct Person {
std::string name;
std::string surname;
std::string id;
};

struct Item {
std::string name;
double price;
std::string id;
};

struct Order {
Person person;
Item purchase;
std::string pick_up_address;
};


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

Order order{.person = {.name = "Golum",
.surname = "Iz shira",
.id = "666"},
.purchase = {.name = "Precious",
.price = 9999999.9,
.id = "13"},
.pick_up_address = "Mordor"};


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

Если хотите использовать наследование, то синтаксис такой:

struct Person
{
std::string name;
std::string surname;
unsigned age;
};

struct Employee : Person
{
unsigned salary;
};

Employee e1{ { .name{"John"}, .surname{"Wick"}, .age{40} }, 50000 };


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

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

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

Point p{.x = 2, .z = 3}; // y is not mentioned, but it will have value of 0

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

Правда у фичи есть определенные ограничения:

👉🏿 Поля должны идти по порядку их объявления в классе. out-of-order инициализация, как в сишке, запрещена. То есть нельзя делать так:

struct Point{
int x, y;
};

Point p{.y = 2, .x = 3}; // not valid in C++!


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

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

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

👉🏿 Если используете designated инициализаторы для одних полей, то нужно в этом же формате задавать значения другим полям. Смешанный формат запрещен:

struct Point{
int x, y;
};

Point p{2, .y = 3}; // Not allowed


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

Have a clear intentions. Stay cool.

#cpp20 #cppcore
2🔥34👍1510
Гайд на тестовое задание
#новичкам

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

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

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

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

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

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

Не знаете, как оформлять тестовое? Тогда Грокаем С++ идет к вам!

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

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

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

Обязательно пишите в комментах свои дополнения/замечания. Если удобнее иметь pdf-ку, тоже пишите. Если много людей захочет - сделаем.

ЗЫ: Как только решите сдавать тестовое на проверку - пройдитесь по этому чеклисту. Возможно в ходе разработки вы на что-то не обратили внимание и сможете своевременно исправить эти недостатки.

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

The end.

Provide quality. Stay cool.
1🔥3111👍11❤‍🔥3😁1
Тип возвращаемого значения тернарного оператора
#опытным

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

struct PersonalData {
std::string name;
std::string surname;
std::string patronymic;
std::optional<std::string> address;
std::optional<std::string> email;
std::optional<std::string> phone;
};


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

Берем джейсон и перекладываем(то есть занимаемся тем, чему 6 лет учат в тех вузах):

PersonalData person{
.name = json["name"],
.surname = json["surname"],
.patronymic = json["patronymic"],
.address = json.HasMember("address") ? json["address"] : std::nullopt,
.email = json.HasMember("email") ? json["email"] : std::nullopt,
.phone = json.HasMember("phone") ? json["phone"] : std::nullopt};


Просто, чтобы кучу if'ов не плодить, воспользуемся тернарным оператом. Если в json'е есть данное поле, то инициализируем опциональное поле им, если нет, то std::nullopt'ом.

Ничего криминального не сделали. Вдобавок использовали designated initialization из с++20.

Компилируем ииииииии..... Ошибка компиляции.

Пишет, что тернарный оператор не может возвращать разные типы.

Дело в том, что std::nullopt - это константа типа nullopt_t. А поле джейсона имеет тип строки. Конечно, из обоих типов можно сконструировать объект std::optional. Но тернарный оператор не знает, что мы хотим. Ему просто не разрешается возвращать разные типы.

Но почему? Это же так удобно.

С++ - это не всегда про удобство)

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

auto try_stoi(const std::string& potential_num) {
if (can_be_converted_to_int(potential_num)) {
return std::stoi(potential_num);
} else {
return potential_num;
}
}


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

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

Придется явно оборачивать все в std::optional:

PersonalData person{
.name = json["name"],
.surname = json["surname"],
.patronymic = json["patronymic"],
.address = json.HasMember("address") ? std::optional(json["address"]) : std::nullopt,
.email = json.HasMember("email") ? std::optional(json["email"]) : std::nullopt,
.phone = json.HasMember("phone") ? std::optional(json["phone"]) : std::nullopt};


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

Be flexible. Stay cool.

#cppcore #cpp17 #cpp20
🔥37👍2310😁1
Особый день

Хоть весна нас особо не греет, сегодня очень важный и теплый для нашей страны праздник - День Победы.

К нему можно по-разному относиться, непростая ситуация сейчас в мире. Но, на наш взгляд, это не имеет значения.

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

Любой знаковый день - повод сделать что-то. Накидываем беспроигрышный вариант.

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

С праздником, дорогие подписчики! Благодарность свернет горы.

Tip your hat to your ancestors. Stay cool.
1147❤‍🔥31👍15🤬6💯6👎1
Частичная специализация шаблонов функций
#опытным

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

Частичной специализации шаблонов функции не существует. И точка!

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

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

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

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

void f(int) { std::cout << "int-overload" << std::endl; }; 
void f(int*){ std::cout << "int-p-overload" << std::endl; }

template<class T> void f(T) { std::cout << "T-overload" << std::endl; };
template<class T> void f(T*){ std::cout << "T-p-overload" << std::endl; }


Заметьте, что синтаксис одинаковый с точностью до появления template<class T> и замены конкретного типа на шаблонный параметр.

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


template<typename T>
class Foo {};

template<typename T>
class Foo<T*> {};


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

template <typename T1>
struct Foo<T1> {};
// Так нельзя делать, это несвязанные шаблоны
template <typename T1, typename T2>
struct Foo<T1,T2> {};


С этим разобрались. Давайте пофантазирует, что будет, если бы мы могли частично специализировать шаблон функции, при этом оставили бы дефолтный механизм перегрузки:

template<class T> void f(T) { std::cout << "T-overload" << std::endl; }; 
template<class T> void f(T*){ std::cout << "T-p-overload" << std::endl; }

template<class T> void f<T*>(T*){std::cout << "T-p-specialization" << std::endl;}


Ну и как компилятору выбирать между T-p-overload и T-p-specialization?

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

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

Don't be confused. Stay cool.

#template #cppcore
122👍12🔥11
Перегружаем шаблоны классов
#опытным

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

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

Но как и практически любое ограничение в С++, его можно хакнуть.

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

template <typename... T>
struct Foo;

template <typename T1>
struct Foo<T1> {};

template <typename T1, typename T2>
struct Foo<T1,T2> {};


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

Вот такие фокусы.

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

template <typename T, int N>
struct Foo<T, N> {}; // forbidden


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

Hack the boundaries. Stay cool.

#template #cppcore
1🔥22👍12❤‍🔥32
Квиз

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

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

У меня к вам всего один вопрос. Каков результат попытки компиляции и запуска следующего кода под С++20?

#include <iostream>
struct base {
virtual void foo(int a = 0) { std::cout << a << " "; }
virtual ~base() {}
};

struct derived1 : base {};

struct derived2 : base {
void foo(int a = 2) { std::cout << a << " "; }
};

int main() {
derived1 d1{};
derived2 d2{};
base & bs1 = d1;
base & bs2 = d2;
d1.foo();
d2.foo();
bs1.foo();
bs2.foo();
}


Challenge your life. Stay cool.
15👍7🔥4
Какой результат попытки компиляции и запуска кода выше?
Anonymous Poll
12%
Ошибка компиляции
6%
UB
8%
0 0 0 0
37%
0 2 0 0
3%
0 0 0 2
25%
0 2 0 2
8%
S O S
👍1