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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
Привычно возводим в степень

Спасибо, @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.

#fun
24🤣22👍12🔥6😁3🤯1🗿1
Забавный факт про 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
🔥40😁178👍7🤯3❤‍🔥1
​​Стандартные пользовательские литералы. Строковые
#новичкам

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

Первая особенность - для их использования не нужно подчеркивание впереди суффикса. Стандарт может позволить зарезервировать для себя такой формат, чтобы не было коллизий с нашими кастомными операторами. Ну и без 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 #cpp17
30🔥12👍8😁7🤯2🤔1💯1
​​Стандартные пользовательские литералы. Числовые
#новичкам

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

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

Мы все привыкли писать: или 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.

#cpp14
27😁17🔥12👍31🤯1
​​Доступ к приватным членам. Макросы
#новичкам

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

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

Но!

Врага надо знать в лицо!

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

И на завтрак мы разберем самый простой способ. Макросы.

Есть у нас хэдэр:

// 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 #badpractice
37😁12👍11🤯6😭6🔥5😱4🤣2
​​Доступ к приватным членам. Указатали
#новичкам

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

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

То есть банально

class MyClass {
private:
int secret = 42;
};

void illegalAccess(MyClass &obj) {
int *ptr = (int *)&obj; // assume that secret is first member
std::cout << "Illegal: " << *ptr << std::endl;
}


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

Но здесь есть целых 2 проблемы.

1️⃣ Нарушение strict aliasing. Мы интерпретируем указатель на объект, как указатель на другой тип. Это UB по стандарту. Это значит, что ваше решение непереносимо и результат может отличаться в зависимости от компилятора и опций компиляции.

2️⃣ Вторая еще серьезнее. Цитата из стандарта:

The interpretation of a given construct is established without regard to access control. If the interpretation established makes use of inaccessible members or base classes, the construct is ill-formed.

Если ваша кодовая конструкция интерпретируется, как использование недоступных вам мемберов, то конструкция ill-formed. Не сказано, что сама программа ifndr, но это все равно значит, что код выше не соответствует правилам языка.

Первую проблему можно обойти с помощью дыры в стандарте memcpy:

void AccessWithMemcpy(MyClass& obj) {
int value;
std::memcpy(&value, &obj, sizeof(int));
std::cout << value << std::endl;
}


Но вторую проблему никак не убрать. Если вы получаете доступ к недоступным вам в текущем контексте полям через такие низкоуровневые инструменты, ваш код is dog shit.

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

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

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

Be legal. Stay cool.

#cppcore #badpractice
23👍17😁9🔥7❤‍🔥1😭1
Квиз
#новичкам

Буквально на секундочку вернемся к теме пользовательских литералов.

Со строковыми литералами всегда какая-то беда происходит. То тип путает карты, то этот null-terminator комом в горле встает, то чтобы вычислить длину надо быть кмс по приседаниям.

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

А разбирать надо для понимания процессов. Поэтому сегодня проверим ваши интуицию/знания в рамках небольшого #quiz 'а.

Какой результат попытки компиляции и запуска кода ниже под С++23?


#include <iostream>
#include <iomanip>
#include <type_traits>
#include <cmath>

int operator ""_length(const char*, std::size_t length) { return length; }

int main()
{
auto s = "A" "B"_length "C"
"D"
"E"
"FGH";

std::cout << s << "\n";
}
🤔13👍74🔥2🤯1
​​WAT
#новичкам

Спасибо, @Ivaneo, за любезно предоставленный примерчик в рамках рубрики #ЧЗХ.

Ответ на квиз из поста выше - на экран выведется 8.

WAT? Строковые литералы конкатенируются? Да еще и пользовательский суффикс между двух литералов применяется к конкатенации?

Вообще, да. Сейчас во всем разберемся.

Для начала. Да, c-style строки конкатенируются(склеиваются). И это бывает очень полезно, особенно при работе с длинными строками.

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

Можно это делать с помощью символов экранирования, например так:

auto str = "Suuuuuuuuuuuuuuupppeeeeeeeeeeeeeeeeeeeeeeeeeeerrrr
loooooooooooooooooooooooooooooong \ striiiiiiiiiiiiiiiiiiiiiiiiiiiiiing";


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

Чтобы этих проблем не было, существует конкатенация строковых литералов. Буквально:

auto str = "Hello "
// void
"World!";
std::cout << str << std::endl;

// OUTPUT
// Hello World!


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

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

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

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

int num1 = 123; // OK
int num2 = 12 23 // ERROR
int num3 = 1'234; // if you want to logicaly devide large number


Если вы хотите как-то сгруппировать цифры в числе, то можете использовать бинарные литералы(вот этот штрих в num3).

Don't break into pieces. Be whole. Stay cool.

#cppcore #cpp11
🔥25👍7🤯65❤‍🔥1
​​Шо там не так с union?
#опытным

Объединения в С++, мягко говоря, не любят. И сегодня мы обсудим почему.

Юнион - это такой специальный класс, который хранит только одно из своих полей. Любое, но одно. Объект объединения может хранить разные поля в одной и той же памяти. А размер union равен размеру максимального поля.

union U {
int integer;
float floating;
char character;
};

U u;
u.integer = 42; // now u hold int
u.floating = 3.14; // now u hold float
u.character = 'a'; // now u hold char


U может хранить или целое число, или число с плавающей точкой, или символ. Последнее записанное поле становится "активным" полем.

Объединение позволяет, например, хранить несвязанные типы в одном массиве.

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

Никак. Такого функционала нет. Поэтому приходится делать разные обвязки:

struct Helper {
enum class Type { Int, Float, Char };

union U {
int integer;
float floating;
char character;
};

Type objectType;
U value;

Helper(int i) : objectType(Type::Int), value{.integer = i} {}
Helper(float f) : objectType(Type::Float), value{.floating = f} {}
Helper(char c) : objectType(Type::Char), value{.character = c} {}
};


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

Helper h{42};

h.value.floating = 3.14;


Мы изменили активное поле без изменения objectType. А теперь где-то в другом месте кода:

if (h.objectType == Helper::Type::Int) {
std::cout << h.value.integet << std::endl;
}


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

Доступ до неактивного члена объединения - UB.

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

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

В С++ давно уже есть прекрасная типобезопасная альтернатива - std::variant. При доступе не к тому типу он хотя бы выкинет исключение std::bad_variant_access. И будет понятно хотя бы, где собака зарыта. Точнее не всегда понятно где конкретно, но точно понятно, что зарыта именно собака. А это уже облегчает поиски.

Be safe. Stay cool.

#cppcore
27🔥12👍11
​​union class
#опытным

В прошлом посте мы упомянули, что union - это такой специальный класс. Это что значит, объединение может иметь методы?

Представьте себе, да!

Начиная с С++11 union'ы могут иметь полноценные конструкторы, деструкторы и другие методы.

Но есть ограничения:

👉🏿 не должно быть виртуальных методов

👉🏿 юнион не может быть наследником

👉🏿 юнион не может быть базовым классом

👉🏿 юнион не может хранить ссылочные типы

Во всем остальном - такой же класс!

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

Конструкторы и деструкторы нужны, чтобы union мог хранить объекты классов с нетривиальными дефолтными конструкторами и деструкторами.

Например:

union U {
int i;
float f;
std::string s;
};

U u;


Попытка скомпилировать это дело приведет к ошибкам:
error: use of deleted function 'U::U()'
error: union member 'U::s' with non-trivial
'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::basic_string()
requires is_default_constructible_v<_Alloc>

error: use of deleted function 'U::~U()'
error: union member 'U::s' with non-trivial
'constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::~basic_string()


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

union U {
U() {}
~U() {}
int i;
float f;
std::string s;
};

U u; // ОК


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

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

Expand your horizons. Stay cool.

#cppcore #cpp11
17👍8🔥8
​​Доступ к приватным членам. Интерпретация.
#опытным

Продолжаем исследовать грязь и разврат. Предыдущий пост тут.

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

Программисты - люди ленивые, поэтому давайте как-то упрощать.

Допустим, у меня есть класс Target с множеством полей. А что, если где-то будет структурка Hack, идентичная по расположению полей? Может быть как-то можно интерпретировать данные Target, как данные Hack?

Конечно можно. В плюсах можно ВСЕ.

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

struct Target {
private:
int x = 42;
double y = 3.14;
};

union HackUnion {
HackUnion() {}

Target target;
struct {
int stolen_x;
double stolen_y;
} hacker;
};

int main() {
HackUnion u;
u.target = Target();

std::cout << "Stolen x: " << u.hacker.stolen_x << std::endl;
std::cout << "Stolen y: " << u.hacker.stolen_y << std::endl;
}
// OUTPUT:
// Stolen x: 42
// Stolen y: 3.14


Внутри union определяет структуру с точно таким же layout'ом, как у Target, только члены у нее публичные. И интерпретируем данные Target как набор полей этой структуры. Так мы можем достать stolen_x и stolen_y.

И это даже не UB, а вполне валидное поведение! Стандарт гарантирует, что обе структуры layout-compatible и имеют общую начальную последовательность полей. И в этом случае union может их читать без угроз со стороны библии С++.

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

Кстати, если уж мы хотим "интерпретировать" один объект как другой, можно использовать приведение типов реинтерпретацией aka reinterpret_cast:

struct Target {
private:
int x = 42;
double y = 3.14;
};

struct Hacker {
int stolen_x;
double stolen_y;
};

Target target;

Hacker* hacker = reinterpret_cast<Hacker*>(&target);

std::cout << "Stolen x: " << hacker->stolen_x << std::endl;
std::cout << "Stolen y: " << hacker->stolen_y << std::endl;


Тут уже без сомнения UB, потому что нарушаем strict aliasing, но это не мешает программе успешно отрабатывать.

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

#cppcore #badpractice
23👍15🔥6😁2
​​Доступ к приватным членам. Други-функции.
#новичкам

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

С++ - вообще амбассадор языковых средств обходов ограничений. Есть константный объект, которых хочется изменить? Пожалуйста, const_cast. Застряли где-то в пучинах кода и не можете красиво выйти? Пожалуйста, goto.

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

struct Foo {
friend void foo(Foo& foo) {
std::cout << "I've stolen your secret: " << foo.a << std::endl;
}
private:
int a = 42;
};


Завести друга очень просто(по крайней мере в С++). Вам нужно объявить или определить функцию внутри скоупа класса и пометить ее ключевым словом friend. Тогда класс начинает доверять вашей функции настолько, что раскрывает ей все свои секретики.

Можно также просто объявить функцию внутри класса, а определить ее вне:

struct Foo {
friend void foo(Foo& foo);
private:
int a = 42;
};

void foo(Foo& foo) {
std::cout << "I've stolen your secret: " << foo.a << std::endl;
}


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

В чем особенность френдов? Их недолюбливают, но иногда без них никуда.

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

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

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

class Complex {
private:
double real, imag;
public:
Complex(double r, double i) : real(r), imag(i) {}

// It works only for complex * double
Complex operator*(double scalar) const {
return Complex(real * scalar, imag * scalar);
}

// This is for double * complex
friend Complex operator*(double scalar, const Complex& c);
};

Complex operator*(double scalar, const Complex& c) {
return Complex(c.real * scalar, c.imag * scalar);
}


Честный метод-operator* будет работать только в том случае, когда объект комплексного числа стоит слева. А за умножение double * complex отвечает глобальный оператор*. И конкретную его перегрузку нужно сделать другом, чтобы он имел доступ к приватным полям.

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

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

class Complex {
// ...
friend std::ostream& operator<<(std::ostream& os, const Complex& obj);
};
std::ostream& operator<<(std::ostream& os, const Complex& obj) {
os << "Real: " << obj.real << ", Imaginary: " << imag;
return os;
}


Тоже пример дружественной перегрузки вывода из Eigen.

Страуструп кстати дает один интересный иллюстрационный пример: в рэнджах момент окончания последовательности задается в более общем виде, чем итератор - sentinel(ограничитель). Это по сути объект любого типа, главное чтобы итератор мог уметь сравниться с ним. И вот для сравнения итератора и ограничителя может понадобиться определить дружественные операторы == и !=.

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

Value your friends. Stay cool.

#cppcore
22🔥8👍6😁2
​​Доступ к приватным членам. Други-классы.
#новичкам

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

class Matrix {
private:
int data[10][10];
public:
friend class MatrixAnalyzer;
};

class MatrixAnalyzer {
public:
int sum(const Matrix& m) {
int sum = 0;
for (int i = 0; i < 10; i++)
for (int j = 0; j < 10; j++)
sum += m.data[i][j]; // Access private data
return sum;
}
};


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

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

Для любой гаечки в С++ найдется свое применение. И для другов-классов тоже:

1️⃣ У вас есть класс, который содержит в своем публичном скоупе еще один класс. Но у этого вложенного класса есть какие-то приватные члены, к которым хотелось бы получить доступ.

Например, есть класс контейнера-связного списка. В него вложен публичный класс итератора и приватный класс ноды.
template <typename T>
class LinkedList {
private:
struct Node {
T data;
Node *next;
public:
Node(const T &value, Node *n = nullptr) : data(value), next(n) {}
};
Node *head = nullptr;
public:
class Iterator {
private:
Node *current;
Iterator(Node *node) : current(node) {}
friend class LinkedList<T>;
public:
// ...
};

// ...
};


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

2️⃣ Юнит-тестирование. Обычно конечно тестируют только публичный интерфейс класса и стараются покрыть все пути исполнения и кейсы использования, чтобы абсолютно вся логика, включая приватные методы, была протестирована. Это называется black-box тестирование.

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

И вот для белого ящика нужно пометить класс тестов другом тестируемого класса.

class Database {
private:
bool validateConnectionString(const std::string& connStr) {
// Very complicated logic
return true;
}
friend class DatabaseTest; // HERE
};

class DatabaseTest {
public:
static bool testValidation() {
Database db;
return db.validateConnectionString("valid string");
}
};


А какой вид тестирования предпочитаете вы? Почему?

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

Have friends. Stay cool.

#cppcore
23😁13👍9🔥3
​​CRTP + friend
#опытным

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

Зачем тратить драгоценные миллинаносекунды, если можно воспользоваться compile-time полиморфизмом, а конкретно паттерном CRTP?

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

template <typename Derived>
class ClientBase {
protected:
int sock_ = -1;
sockaddr_in serv_addr_{};
std::string server_ip_;
uint16_t port_;

Derived& derived() { return static_cast<Derived&>(this); }

public:
void send_message(const std::string& message) {
ssize_t bytes_sent = derived().send_impl(message);
if (bytes_sent < 0) {
throw std::runtime_error("Send failed");
}
}
};

class TcpClient : public ClientBase<TcpClient> {
public:
static constexpr int PROTOCOL_TYPE = SOCK_STREAM;

ssize_t send_impl(const std::string& message) {
return send(sock_, message.c_str(), message.length(), 0);
}

};

class UdpClient : public ClientBase<UdpClient> {
public:
static constexpr int PROTOCOL_TYPE = SOCK_DGRAM;

ssize_t send_impl(const std::string& message) {
return sendto(sock_, message.c_str(), message.length(), 0,
reinterpret_cast<sockaddr>(&serv_addr_), sizeof(serv_addr_));
}
};


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

Но как? База CRTP должна иметь доступ к этим методам.

Вот тут-то мы и используем трюк. Сделаем все члены наследников приватными и дадим базе CRTP доступ к кишкам наследников через friend:

template <typename Derived>
class ClientBase {
// ...
};

class TcpClient : public ClientBase<TcpClient> {
private:
// ...
friend class ClientBase<TcpClient>;
};

class UdpClient : public ClientBase<UdpClient> {
private:
// ...
friend class ClientBase<UdpClient>;
};


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

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

Hide your secrets. Stay cool.

#template
33👍18🔥14😁6🤯1
Доступ к приватным членам. Sutter hack
#опытным

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

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

Возьмем простой класс:

class X {
public:
X() : private_(1) {}

template <class T>
void f(const T &t) {}

int Value() { return private_; }

private:
int private_;
};


Чтобы трюк сработал, в классе должен быть шаблонный метод.

Теперь следите за руками.

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

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

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

И тогда класс будет себя вести именно так, как мы ему скажем. А скажем мы ему пару ласковых:

struct Y {};

template <>
void X::f(const Y &) {
private_ = 2;
}

int main() {
X x;
std::cout << x.Value() << std::endl; // prints 1
x.f(Y());
std::cout << x.Value() << std::endl; // prints 2
}


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

Этот трюк был описан Гербом Саттером, поэтому и называется Sutter hack.

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

The behavior of a C++ program is undefined if it declares

- an explicit specialization of any member function of a standard library class template, or
- an explicit specialization of any member function template of a standard library class or class template, or
- an explicit or partial specialization of any member class template of a standard library class or class template, or
- a deduction guide for any standard library class template.


потому что явные специализации методов из STL приводят к ub.

В общем, интересно, как на стыке двух концепций - ООП и шаблонов - появляются такие интересные спецэффекты)

Hack the life. Stay cool.

#cppcore #template #fun
👍28🔥12❤‍🔥5🤯43
​​Доступ к приватным членам. Явная инстанциация
#опытным

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

Но!

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

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

С++ и это может воплотить в реальность.

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

[temp.spec.partial.general]/10

The usual access checking rules do not apply to non-dependent names used to specify template arguments of the simple-template-id of the partial specialization.

[temp.spec.general]/6
The usual access checking rules do not apply to names in a declaration of an explicit instantiation or explicit specialization

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

class Foo {
private:
int data = 42;
};

template <auto V>
struct Bar {};
template struct Bar<&Foo::data>;


Этот код вполне влиден. Здесь мы используем указатель на поле класса Foo::data в качестве NTTP. Это валидно, потому что указатель на поле класса - это по сути смещение от начала объекта и оно известно на момент компиляции.

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

Bar<&Foo::data> b;
// error: 'int Foo::data' is private within this context


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

template <typename PtrType>
struct Storage {
inline static PtrType ptr;
};

template <auto V>
struct PtrTaker {
struct Transferer {
Transferer() {
Storage<decltype(V)>::ptr = V;
}
};
inline static Transferer tr;
};

template struct PtrTaker<&Foo::data>;


Когда вы явно инстанцируете PtrTaker&lt;&amp;Foo::data&gt;, его статический член tr будет инициализирован, и в его конструкторе Storage&lt;PtrType&gt;::ptr получит значение. Теперь вы можете получить доступ к нему через:

Foo foo;
std::cout << foo.Storage<int Foo::>::ptr;


Это работает!

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

Еще один раз шаблоны сломали инкапсуляцию. Да что ж это такое!

Спасибо, @SoulslikeEnjoyer, за материалы для поста)

Exploit loopholes. Stay cool.

#cppcore #template #fun
11👍8🔥6🤯6❤‍🔥4
​​Доступ к приватным членам. Явная инстанциация и друзья
#опытным

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

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

Но давайте посмотрим на следующий пример:

template <typename T>
struct Foo {
friend void bar() { cout << "Got it!" << endl; }
};

void bar();
template struct Foo<int>;

bar();


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

Давайте проследим, что произошло. Функция bar - по сути свободная функция, которая может использовать все члены Foo. Но не только их. Она еще может использовать шаблонные параметры конкретной инстанциации.

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

Давайте сделаем шаблонный параметр Foo указателем на поле и инстанцируем этот шаблон с указателем на приватное поле класса:

class Private {
private:
int data{};
};

template<int Private::* Member> // pointer to data member
struct Stealer {
friend int& dataGetter(Private& iObj) {
return iObj.*Member;
}
};

template struct Stealer<&Private::data>; // explicit instantiation
int& dataGetter(Private&);

int main() {
Private obj;
dataGetter(obj) = 42;
}


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

И еще один из шаблоны сломали инкапсуляцию. Да что ж это такое!

Спасибо, @SoulslikeEnjoyer, за материалы для поста)

Exploit loopholes. Stay cool.

#cppcore #template #fun
17🔥15❤‍🔥6👍3😱1
​​Бросаем дичь
#новичкам

В С++ есть исключения. Вы можете их любить или ненавидеть, но от этого не сбежать(почти).

Обычно как происходит. Есть стандартный std::exception или любой кастомный базовый класс исключения my_exception::BaseException. У них куча наследников и вот вы их бросаете в подходящих ситуациях.

Но это же С++: "Вы думали, что бросать можно только исключения? Пфф. Не смешите мои подковы и подержите мое пиво."

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

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

throw 1;


Бросаем число. А что, какие-то проблемы?

Или вот так:

throw nullptr;
throw "This is the end!";

void panic() { std::cout << "PANIC!" << std::endl; }
throw static_cast<void(*)()>(panic); // Указатель на функцию


Кидаю указатели: на ничто, на c-style строку и на функцию. Не ожидали? Все легально.

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

throw []{std::cout << "Things are going really bad...\n"; };


Работает вся это свистопляска с раскруткой стека ровно так же, как и при работе с std::exception.

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

Be amazed. Stay cool.

#cppcore
🔥3913😁8👍6🤯1
Квиз
#опытным

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

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

Ну раз просто, тогда поучаствуйте в #quiz'е: какой будет результат попытки компиляции и запуска этого кода на С++23?


#include <iostream>
#include <string>
#include <string_view>
#include <vector>


int main()
{
std::vector<std::pair<std::string_view, std::string_view>> pairs
{
{{"one", "two"}, {"three", "four"}}
};

for (const auto & [f, s] : pairs)
{
std::cout << f << " and " << s << std::endl;
}
}
👍8🔥54🤔1😭1
Какой результат попытки компиляции и запуска кода выше под С++23?
Anonymous Poll
18%
Ошибка компиляции
30%
one and two\nthree and four
9%
one and three
3%
two and four
41%
Где-то здесь, рядом с собакой, уб зарыто...
7😁7👍3🔥3