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

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

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
​​std::to_chars
#новичкам

В C++17 появилась не только функция для парсинга (std::from_chars), но и её обратная версия — std::to_chars, которая позволяет конвертировать числа (int, float, double и др.) в строки без дополнительных затрат на выделение памяти и поддержку исключений.

std::to_chars_result to_chars(char* first, char* last,
IntegerType value,
int base = 10); // (1)

std::to_chars_result to_chars(char* first, char* last,
FloatType value,
std::chars_format fmt); // (2)

std::to_chars_result to_chars(char* first, char* last,
FloatType value,
std::chars_format fmt,
int precision); // (3) (since C++23)
struct to_chars_result {
const char* ptr;
std::errc ec;
};


Единственный случай, когда эта функция может зафейлиться - если вы передали слишком маленький буффер. Тогда ec выставляется в std::errc::value_too_large, а ptr в last.

В чем преимущество функции по сравнению со старой-доброй std::to_string?


💥 Можно задать точность и основание системы счисления.

💥 Отсутствуют динамические аллокации внутри функции.

💥 Возможность использовать любые char буферы, а не только строки.

Вот вам пример работы:
char buffer[20];
int value = 12345;

auto result = std::to_chars(buffer, buffer + sizeof(buffer), value);
if (result.ec == std::errc()) {
size_t result_length = result.ptr - buffer;
std::string_view str_result(buffer, result_length);
std::cout << "Result: " << str_result << "\n"; // "12345"
}

// ------------------------

double pi = 3.1415926535;
char buf[20];

auto result = std::to_chars(buf, buf + sizeof(buf), pi, std::chars_format::fixed, 4);
if (result.ec == std::errc()) {
size_t result_length = result.ptr - buf;
std::string_view str_result(buf, result_length);
std::cout << "Result: " << str_result << "\n"; // "3.1416"
}


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

Be efficient. Stay cool.

#cpp17
22👍14🔥10
​​Разница между std::stoi+std::to_string и std::from_chars+std::to_chars
#опытным

В C++ есть два основных подхода к конвертации чисел в строки и обратно:

Старомодный  — std::stoi, std::to_string (C++11)

Модномолодежный — std::from_chars, std::to_chars (C++17)

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

Особенности старомодного подхода:

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

👉🏿 Работа в высокоуровневом ООП стиле. Используются классы и возвращаются классы, без всяких сырых буферов.

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

👉🏿 Поддержка локалей. Грубо говоря, это механизм для учёта региональных особенностей представления данных. То есть std::stoi, std::to_string реализованы с учетом возможности спецификации локалей и соотвественно изменения результатов конвертации. С локалями возможна такая штука:
// В США (локаль "en_US"):
std::to_string(3.14); // "3.14" (точка как разделитель)

// В Германии (локаль "de_DE"):
std::to_string(3.14); // Может вернуть "3,14" (запятая)!
Естественно, что поддержка такой фичи чего-то да стоит.

Особенности модномолодежного подхода:

👉🏿 Функции std::from_chars, std::to_chars спроектированы быть настолько легкими и быстрыми, насколько это возможно на таком уровне абстракции.

👉🏿 Отсутствие намеренных динамических аллокаций. Только вы решаете, где расположена память по данные.

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

👉🏿 Не проверяет локали.

👉🏿 Поддерживают частичный парсинг.

👉🏿 Поддержка явной гарантии round-trip. Если вы запишите в строку число с помощью std::to_chars и прочитаете его с помощью std::from_chars, то вы всегда получите изначальный результат. Главное, чтобы обе функции были вызваны с использованием одинаковой реализации стандартной библиотеки. Но у std::stoi, std::to_string и этого нет.


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

Возможность поэтапного парсинга также не оставляет выбора - используйте std::from_chars.

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

Choose the right tool. Stay cool.

#cppcore #cpp11 #cpp17
12🔥2012👍10😱1
Modern pimpl идиома
#новичкам

В одном из давнишних постов мы уже обсуждали базовую формулировку и реализацию идиомы pimpl. Это сырой указатель на forward-объявленную структуру.

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

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

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

С такими симптомами ваш терапевт прописал вам однократный прием pimpl идиомы. Выглядеть это может так:

// serializer.hpp
class Serializer {
public:
Serializer();
~Serializer();

// only move-semantic
Serializer(const Serializer&) = delete;
Serializer& operator=(const Serializer&) = delete;
Serializer(Serializer&&) noexcept;
Serializer& operator=(Serializer&&) noexcept;

std::vector<uint8_t> serializeOrder(const domain::Order& person);
domain::Order deserializeOrder(const std::vector<uint8_t>& data);
private:
std::unique_ptr<struct Impl> pimpl_; // Smart pointer on forward-declared struct
};

// serializer.cpp

struct Serializer::Impl {
avro::GenericRecord convertToAvro(const domain::Order&);
domain::Order convertFromAvro(const avro::GenericRecord&);
};

Serializer::Serializer()
: pimpl_(std::make_unique<Impl>()) {
}

Serializer::~Serializer() = default;
Serializer::Serializer(Serializer&&) = default;
Serializer& Serializer::operator=(Serializer&&) = default;

std::vector<uint8_t> Serializer::serializeOrder(const domain::Person& person) {
auto record = pimpl_->convertToAvro(person);
return {...};
}
// deserializeOrder


Основная идея - все упоминания и детали реализации avro находятся в cpp и не торчат наружу. Это достигается за счет использования forward-declared класса в хэдэре и определение этого класса в сорцах. За счет этого мы имеем стабильный интерфейс, стабильный ABI и сокращенное время линковки. А за счет использования std::unique_ptr у нас тривиальные реализации всех специальных методов.

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

Hide details. Stay cool.

#design
20🔥12👍6
Зачем в pimpl определять специальные методы в cpp?
#опытным

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

class Serializer {
public:
Serializer();
~Serializer();

// only move-semantic
Serializer(const Serializer&) = delete;
Serializer& operator=(const Serializer&) = delete;
Serializer(Serializer&&) noexcept;
Serializer& operator=(Serializer&&) noexcept;

//...
private:
std::unique_ptr<struct Impl> pimpl_;
};


Мы ведь привыкли либо вообще не определять никакие специальные методы по правилу нуля, либо просто помечать их default и все. Почему в этот раз так нельзя сделать?

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

Когда мы явно помечаем специальные методы класса как default, мы явно просим компилятор за нас сгенерировать определение этих методов.

class Serializer {
public:
Serializer();
~Serializer() = default; // Here

// only move-semantic
Serializer(const Serializer&) = delete;
Serializer& operator=(const Serializer&) = delete;
Serializer(Serializer&&) noexcept = default; // Here
Serializer& operator=(Serializer&&) noexcept = default; // Here

//...
private:
std::unique_ptr<struct Impl> pimpl_;
};


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

Но вот беда, в теле класса нет никакой информации о том, что из себя представляет класс Impl. Возьмем тот же деструтор. У std::unique_ptr тип делитера зашит в шаблонный параметр. По дефолту это default_deleter, который вызывает просто delete ptr . А у delete expression есть пометка, что при удалении указателя на неполный тип программа становится ill-formed. Поэтому компиляторы на случай, если вы захотите сгенерировать деструктор std::unique_ptr с неполным типом, ставят такой ассерт времени компиляции: static_assert(sizeof(_Tp)>0, "can't delete pointer to incomplete type");. А компиляция последнего примера крашнется с соотвествующей ошибкой.

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

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

Don't give impossible tasks. Stay cool.

#cppcore #design
🔥188👍7
Квиз

Сегодня #quiz на любимую тему всех плюсовиков - утечку памяти.

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

Спасибо Сергею Борисову за идею для поста)

Ну а сейчас у меня для вас всего один вопрос. Будет ли в этом коде утечка памяти или нет?

#include <iostream>
#include <stdexcept>

class Bar {
public:
Bar() {
throw std::runtime_error("Error");
}
};

int main() {
Bar *bar = nullptr;

try {
bar = new Bar();
} catch(...) {
std::cout << "Houston, we have a problem" << std::endl;
}
}


Challenge yoursef. Stay cool
10👍5🔥52
Будет ли в этом коде утечка памяти или нет?
Anonymous Poll
5%
Код даже не соберется
8%
В коде UB
34%
Будет утечка
53%
Не будет утечки
👍8🔥211
​​Ответ на квиз
#новичкам

В этом коде:

class Bar {
public:
Bar() {
throw std::runtime_error("Error");
}
};

int main() {
Bar* bar = nullptr;

try {
bar = new Bar();
} catch(...) {
std::cout << "Houston, we have a problem" << std::endl;
}
}

Не будет утечки памяти. Стандарт нам это гарантирует при использовании new expression.

Дело вот в чем. В этом посте мы поговорили о том, что есть 3 вида new:

👉🏿 operator new, который выделяет память.

👉🏿 placement new, который вызывает конструктор на заданной памяти.

👉🏿 new expression, который в начале выделяет память через operator new, а потом конструирует объект на этой памяти через placement new. Это именно то, что используется в коде выше.

Так вот, new expression заботится о своих пользователях и оборачивает в try-catch вызов конструктора объекта. В catch оно освобождает память и пробрасывает исключение наружу:

Bar* bar = new Bar();
// инструкция выше эквивалентнас следующему коду:
Bar* bar;
void* tmp = operator new(sizeof(Bar));
try {
new(tmp) Bar(); // Placement new
bar = (Bar*)tmp; // The pointer is assigned only if the ctor succeeds
}
catch (...) {
operator delete(tmp); // Deallocate the memory
throw; // Re-throw the exception
}


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

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

Спасибо, @PyXiion за предоставленную информацию)

Don't let your memory leak. Stay cool.

#cppcore #memory
👍39🔥1110
Capture this
#новичкам

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

Например, вы пишите класс приложения. Правила хорошего тона говорят вам, что нужно добавить поддержку graceful shutdown. Для этого получаем объект обработчика сигнала и регистрируем коллбэк на SIGINT и SIGTERM:

struct Application {
Application() {
SignalHandler::GetSignalHandler().RegisterHandler({SIGINT, SIGTERM}, [](){
Shutdown();
});
}

void Shutdown() {...}
};


Сработает ли такой код?

Он не соберется. Будет ошибка: 'this' was not captured for this lambda function. Методу Shutdown нужен объект, на котором его нужно вызвать.

Для этого в С++11 вместе с лямбдами ввели захват this. Это значит, что в лямбду сохраняется указатель на текущий объект. А синтаксис лямбды позволяет в таком случае не использовать явно this в ее теле:

struct Application {
Application() {
SignalHandler::GetSignalHandler().RegisterHandler({SIGINT, SIGTERM}, [this](){
// this->Shutdown(); - don't need this syntax
Shutdown();
});
}

void Shutdown() {...}
};


До С++20 кстати можно было захватывать this в лямбду с помощью дефолтного захвата по значению и по ссылке:

SignalHandler::GetSignalHandler().RegisterHandler({SIGINT, SIGTERM}, [= /*& also works*/](){ // works
Shutdown();
});


Однако в С++20 запретили неявный захват this по значению(что очень хорошо). Теперь либо явных захват this, либо через default capture by reference.

Be explicit. Stay cool.

#cppcore #cpp11 #cpp20
👍225❤‍🔥3🔥2👏1
Почему еще важен std::forward
#опытным

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

Также спасибо ему за предоставление следующего примера:

#include <iostream>

void bar(float&& x) { std::cout << "float " << x << "\n"; }
void bar(int&& x) { std::cout << "int " << x << "\n"; }

void foo(auto&& v) { bar(v); }

int main() {
foo(1);
foo(2.0f);
}


Как думаете, что выведется на консоль? Подумайте пару секунд.

Ну нормальный человек ответит:

int 1
float 2


Однако командная строка вам выдаст следующее:

float 1
int 2


Если не верите, по посмотрите в годболте. И можете уже сейчас написать в комментах: "ЧЗХ", "WTF", "WAT" и прочее.

А нам пораразбирацца.

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

#ifdef INSIGHTS_USE_TEMPLATE
template<>
void foo<int>(int && v)
{
bar(static_cast<float>(v));
}
#endif


#ifdef INSIGHTS_USE_TEMPLATE
template<>
void foo<float>(float && v)
{
bar(static_cast<int>(v));
}
#endif


Просто прекрасно. Какого черта компилятор кастит переменные к противоположным типам?

Первое, что важно понимать: внутри функции foo переменная v - это уже lvalue, так как имеет имя. Значит просто так вызвать перегрузки для правых ссылок он не может.

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

То есть происходит следующее: компилятор понимает, что подходящей перегрузки нет, поэтому начинает применять стандартные преобразования в другие типы. Любой каст дает временный объект. А временный объект типа int легко биндится к float&&, как и временный объект float легко биндится к int&&.

Вот и получается обмен вызовами.

Чтобы такого не происходило, применяйте перед сном std::forward. Если есть контекст вывода типов, то он помогает правильно передавать категорию выражения объекта во внутренние вызовы.

#include <iostream>

void bar(float&& x) { std::cout << "float " << x << "\n"; }
void bar(int&& x) { std::cout << "int " << x << "\n"; }

void foo(auto&& v) { bar(std::forward<decltype(v)>(v)); }

int main() {
foo(1);
foo(2.0f);
}


В этом случае вывод будет ожидаемым.

Be amazed. Stay cool.

#cppcore #cpp11 #template
🔥46🤯298👍7❤‍🔥2
Just throw it forward
#опытным

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

Пусть вы хотите работать с какой-то сущностью в коде эксклюзивно:

void func(std::shared_ptr<Resource> res) {
if (res->TryToAcquire()) {
// do dirty things
res->Release();
} else {
// you don't have permission to do dirty, so return
return;
}
}


Что будет, если в этом коде в результате обработки эксклюзивно захваченного ресурса произойдет исключение?

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

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

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

Автоматически освободить ресурс нам поможет RAII обертка, которая освободит ресурс в деструкторе, если его получилось захватить, а залогировать ошибку поможет совместное использование catch + простой вызов "throw".

void func(std::shared_ptr<Resource> res) {
auto raii_res = RaiiWrapper(res);
if (raii_res->TryToAcquire()) {
try {
// do dirty things
} catch (const std::exception& ex) {
Log("Houston, we have a problem");
throw;
}
} else {
// you don't have permission to do dirty, so return
return;
}
}


Инструкция throw делает единственную вещь - пробрасывает то же самое исключение выше по стеку вызовов.

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

Don't leak your resources. Stay cool.

#cppcore
19👍17🔥8
​​std::clamp
#новичкам

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

И чтобы не городить подобных конструкций:

if (value < min) {
value = min;
}
if (value > max) {
value = max;
}


есть замечательная стандартная функция std::clamp. Она полностью инкапсулирует логику ограничения значения сверху и снизу:

template<class T>
constexpr const T& clamp( const T& v, const T& lo, const T& hi );


Теперь ограничивать значения переменным проще, чем написать Hello, World.

double increment_speed(double curr_speed, double acceleration, double time_delta) {
curr_speed += acceleration * time_delta;
return std::clamp(curr_speed, kMinSpeed, kMaxSpeed);
}


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

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

Don't reinvent the wheel. Stay cool.

#STL
644👍18🔥18
​​Как определять константы локальных функций
#новичкам

Когда мы определяем константу в скоупе функции у нас есть несколько вариантов, как это сделать: пометить const, constexpr, const static, constexpr static. Какой вариант выбрать?

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

Особенности функционала диктуют кейсы применения.

1️⃣ Если константа зависит от входных данных функции, то ее нужно помечать, как const. constexpr здесь по определению не подойдет, не-consteval функции могут выполняться в рантайме, а в consteval функциях входные параметры не считаются за constexpr. static тоже, потому что один раз определив статическую константу, вы ее не сможете поменять.

std::pair<double, double> normalizeVector(double x, double y) {
const double LENGTH = std::sqrt(x * x + y * y); // Зависит от x и y
return {x / LENGTH, y / LENGTH};
}


2️⃣ Если константа не зависит от входных данных функции и она известна во время компиляции, то лучше ее определить, как constexpr static. Тогда такая локальная переменная гарантированно не будет занимать пространство на стеке при каждом вызове функции, ее инициализация точно пройдет в compile-time и об этом будет явно сказано в коде программы.

double metersToMiles(double meters) {
constexpr static double METERS_IN_MILE = 1609.34;
return meters / METERS_IN_MILE;
}


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

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

Be expressive. Stay cool
20👍14🔥6😁4
Ревью
#новичкам

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

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

Сегодняшний лот:
#include <cstring>
#include <iostream>

class Data {
public:
char* buffer;

Data(const char* input) {
buffer = new char[strlen(input)];
std::strcpy(buffer, input);
}

~Data() {
delete buffer;
}
};

bool compareData(Data data1, Data data2) {
return data1.buffer == data2.buffer;
}

int main() {
Data d("Hello");
bool result = compareData(d, d);

std::cout << "Result: " << result << std::endl;
}


Продуктивной прожарки всем!

Critique your solutions. Stay cool.
1029🔥15👍10😱2
Разбор ревью
#новичкам

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

Как всегда было сложно выбрать один комментарий, у каждого свое видение итогового решения и разные предложения. Решил отметить двух подписчиков: @Ivaneo(коммент), как наиболее продуктивного по количеству замечаний, и @monah_tuk(коммент), за более нативный формат. Давайте похлопаем им 👏👏👏.

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

Обычно при ревью мы пытаемся максимально сохранить идею кода вымышленного автора при обсуждении решении проблем. А то можно было бы сказать "надо использовать std::vector и мозг себе не блендерить". Так не интересно)

Поехали!

🔞 Создаем массив через new[], а удаляем через delete. Это UB, нужно использовать одинаковые операторы для создания и удаления объектов и массивов.

🔞 Автор кода хочет, чтобы его класс обладал всеми специальными методами, и при этом надеется, что компилятор сможет сгенерировать их так, как надо автору. Надеяться не надо, потому что компилятор довольно тупой и выполняет поверхностное копирование и перемещение в самосгенеренных методах. Он просто копирует значение указателя в другой объект. В compareData аргументы передаются по значению, значит будет копирование. Значит после выхода из скоупа функции будет double free при разрушении второго объекта. Надо писать свои специальные методы по правилу пяти. До кучи можно использовать swap idiom.

🔞 compareData сравнивает объекты по указателю. Это неверный подход, потому что такое сравнение не предполагает сравнение самих данных. Такое сравнение будет давать в результате true только для одного и того же объекта. Нужно сравнивать данные по этим указателям, а не сами указатели.

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

Тут вопрос на самом деле намного шире: а что мы вообще может принимать, в качестве аргументов конструктора? Туда спокойно можно передать указатель на обычный char массив, у которого нет null-terminator'а. Более того, в середине такого массива может быть валидный нулевой символ, просто как часть общего массива.

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

Плюс, если мы допускаем нулевой символ в середине, то функции типа std::str* нам не подходят, потому что ориентируются на терминатор. Лучше использовать std::copy.

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

template <size_t N>
explicit Data(const char (&str)[N])
: Data(str, N) {
}


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

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

🔞 Использование сырых указателей попахивает С-style'ом в плюсовом коде. У нас давно появились умные указатели и если уж так хочется пользоваться чаровскими буферами(ударение не на 3-й слог!) то можно использовать std::unique_ptr<char[]>. Тогда реализация всех специальных методов становится проще, а деструктор - вообще тривиальным.
7👏6👍4🔥3
🔞 Если принимаете сырой указатель, как параметр - не забудьте проверить его на nullptr. Меньше будете голову ломать при будущем дебаге.

🔞 compareData зачем-то принимает параметры по значению. В смысле, понятно зачем: чтобы было double free, а вам было интереснее ревьюить. Лучше все-таки принимать по константной ссылке аргументы.

🔞 Поле buffer публичное как раз для того, чтобы к нему доступ имела compareData. Мы все-таки за инкапсуляцию и мир во всем мире. Поле надо сделать приватным, а compareData другом. А еще лучше убрать compareData и определить нативный оператор сравнения.

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

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

🔞 Многие отметили, что класс стоит пометить как final. Видимо это какой-то кодстайл, чтобы запретить другим классам наследоваться от этого. Причина, как я понял, это отсутствие виртуального деструктора. Поэтому если кто-то хочет отнаследоваться от класса, то он явно видит, что пока классом нельзя пользоваться полиморфно, убирает final и добавляет виртуальный деструктор.

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

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

Фух, на этом вроде все.

Вот что вышло по итогу исправлений:

class Data final {
private:
std::unique_ptr<char[]> buffer;
size_t buf_size = 0;

public:
Data(const char* input, size_t size)
: buf_size(size) {
if (input == nullptr && size != 0) {
throw std::invalid_argument("Input cannot be null for non-zero size");
}

if (size > 0) {
buffer = std::make_unique<char[]>(size);
std::copy(input, input + size, buffer.get());
}
}

template <size_t N>
explicit Data(const char (&str)[N])
: Data(str, N) {
}

// Копирующий конструктор
Data(const Data& other)
: Data{other.buffer.get(), other.buf_size} {
}

// Универсальное присваивание
Data& operator=(Data other) {
swap(*this, other);
return *this;
}

// Перемещающий конструктор
Data(Data&& other) noexcept {
swap(*this, other);
}

~Data() = default;

// Оператор сравнения
bool operator==(const Data& rhs) const noexcept {
return std::string_view{buffer.get(), buf_size} == std:;string_view{rhs.buffer.get(), buf_size()};
}

// Вспомогательная функция для обмена
friend void swap(Data& first, Data& second) noexcept {
using std::swap;
swap(first.buffer, second.buffer);
swap(first.buf_size, second.buf_size);
}
};

[[nodiscard]] bool compareData(const Data& data1, const Data& data2) noexcept {
return data1 == data2;
}


Пишите, если что забыл.

Critique your solution. Stay cool.

#cppcore #OOP #goodpractice #design
34🔥10👍7❤‍🔥1👎1
Почему важно проверять входные данные
#новичкам

Вы делаете какие-то вычисления и среди них есть целочисленное деление:

void fun(int a, int b) {
// some calculations
auto res = a / b;
// some calculations
}


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

try {
fun(1, 2);
} catch(std::exception& ex) {
std::cout << "Calculation error: " << ex.what() << std::endl;
}


Однако помните, что вы пишите на С++. Здесь уровень заботы примерно, как у бати, который вывозит неумеющего плавать сына на середину реки и бросает его в воду с криками "Плыви, сынок!".

Если вдруг вы передадите в fun вторым параметром ноль:

try {
fun(1, 0);
} catch(std::exception& ex) {
std::cout << "Calculation error: " << ex.what() << std::endl;
}


то С++ не пошлет исключение об этой ситуации.

Стандарт явно говорит: целочисленное деление на ноль приводит к неопределенному поведению. У меня например программа просто падает с надписью: Floating point exception. Очень иронично со стороны компилятора обрабатывать эту ситуацию, выводя в консоль текст о появлении исключения, хотя его тут нет и его никак не отловить.

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

void fun(int a, int b) {
if (!b) {
throw std::runtime_error("Devision by zero!");
}
// some calculations
auto res = a / b;
// some calculations
}

try {
fun(1, 0);
} catch(std::exception& ex) {
std::cout << "Calculation error: " << ex.what() << std::endl;
}


Тогда на консоль явно выведется ожидаемое Calculation error: Devision by zero!.

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

Stay alert. Stay cool.

#cppcore
👍3212🔥11❤‍🔥4🤯3
​​std::to_address
#опытным

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

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

Дело это не совсем тривиальное. Со всеми стандартными классами, типа умных указателей и итераторов(которые называют общим выражением fancy pointer) может прокатить вот такое выражение: obj.operator->(). Однако для простых указателей это не прокатит: они не классы и у них нет методов. Да и не прокатит для любых других объектов, у которых не определен этот оператор. Что делать?

Использовать C++20 функцию std::to_address! Вот ее примерная реализация:

template<class T>
constexpr T* to_address(T* p) noexcept {
static_assert(!std::is_function_v<T>);
return p;
}

template<class T>
constexpr auto to_address(const T& p) noexcept {
if constexpr (requires{ std::pointer_traits<T>::to_address(p); })
return std::pointer_traits<T>::to_address(p);
else
return std::to_address(p.operator->());
}


То есть для указателей она просто вовращает их значения наружу, а для объектов fancy pointer'ов она спрашивает, определено ли свойство std::pointer_traits для этих типов. Если же не определено, то пытается достать указатель с помощью вызова метода operator->().

Обычно эта функция требуется для вызова сишного апи в обобщенном коде:

void c_api_func(const int*);

template<typename T>
void call_c_api_func(T && obj) {
c_api_func(std::to_address(obj));
}

std::vector<int> data{10, 20, 30};
call_c_api_func(data.begin()); // works
auto ptr = std::make_unique<int>(42);
call_c_api_func(ptr); // works
call_c_api_func(ptr.get()); // also works


Use the right tool. Stay cool.

#cpp20 #cppcore
223👍9🔥7😁3
Квиз

Сегодня потенциально самый необычный #quiz на канале. Воистину, C/С++ - это самые интересные языки из существующих. Столько безобразия можно с ними натворить. С одной стороны, это мешает языкам выходить в мейнстрим "патамушта небезопасна". С другой стороны, здесь максимальная свобода творчества и полета фантазии.

У меня для вас всего один вопрос: Что произойдет в результате попытки компиляции и запуска этого С-кода:

const int main[] = {
-98693133, -443987883, 440, 113408,
-1922629632, 4149, 1227264, 84869120,
15544, 266023168, 1970231557, 1701994784,
1701344288, 1936024096, -1878384268, 258392925
};


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

Сhoose with your heart. Stay cool.
18🤣3🤯21
Ответ
#опытным

Ну что. Пора раскрывать карты.

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

А получится вот что. Программа успешно скомпилируется и на консоли появится сообщение:

You are the best!


Да, да. Это она про вас, наши подписчики.

Дело вот в чем. Есть такой варнинг в gcc: warning: 'main' is usually a function [-Wmain].
Погодите, main "обычно" является функцией. Это что, может быть не так?

В С нет или не было прям жесткого требования на тип символа main. Он в целом может быть и массивом:
char main[10];

хоть указателем на функцию:
void (main)();


И вот здесь начинается пространство для экспериментов. Адрес main - это место, с которого код начинается исполняться. Если мы каким-то образом запихаем инструкции ассемблера в массив, то мы сможем выполнить код в такой программе!

Ну а дальше дело техники. Пишем прокси программу:

void main() {
__asm__ (
// print You are the best!
"movl $1, %eax;\n" /* 1 is the syscall number for write */
"movl $1, %ebx;\n" /* 1 is stdout and is the first argument */
// "movl $message, %esi;\n" /* load the address of string into the second argument*/
// instead use this to load the address of the string
// as 16 bytes from the current instruction
"leal 16(%eip), %esi;\n"
"movl $18, %edx;\n" /* third argument is the length of the string to print*/
"syscall;\n"
// call exit (so it doesn't try to run the string Hello World
// maybe I could have just used ret instead
"movl $60,%eax;\n"
"xorl %ebx,%ebx; \n"
"syscall;\n"
// Store the You are the best! inside the main function
"message: .ascii \"You are the best!\\n\";"
);
}


Компилируем ее и запускаем gdb для дизассемблирования main:
(gdb) disass main
Dump of assembler code for function main:
0x0000000000001129 <+0>: endbr64
0x000000000000112d <+4>: push %rbp
0x000000000000112e <+5>: mov %rsp,%rbp
0x0000000000001131 <+8>: mov $0x1,%eax
0x0000000000001136 <+13>: mov $0x1,%ebx
0x000000000000113b <+18>: lea 0x10(%eip),%esi # 0x1152 <main+41>
0x0000000000001142 <+25>: mov $0x12,%edx
0x0000000000001147 <+30>: syscall
0x0000000000001149 <+32>: mov $0x3c,%eax
0x000000000000114e <+37>: xor %ebx,%ebx
0x0000000000001150 <+39>: syscall
0x0000000000001152 <+41>: pop %rcx
0x0000000000001153 <+42>: outsl %ds:(%rsi),(%dx)
0x0000000000001154 <+43>: jne 0x1176 <__libc_csu_init+6>
...


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

Единственное, что осталось - вывести эти числа в десятиричной форме по 4 байта, как инты:

(gdb) x/16dw main
0x1129 <main>: -98693133 -443987883 440 113408
0x1139 <main+16>: -1922629632 4149 1227264 84869120
0x1149 <main+32>: 15544 266023168 1970231557 1701994784
0x1159 <main+48>: 1701344288 1936024096 -1878384268 258392925


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

В более новых версиях компиляторов эту лавочку прикрыли, потому что на gcc10 и более такая прога сегфолтится.

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

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

Благодарю @PyXiion за предоставление материалов для этого поста.

Be amazed. Stay cool.

#fun #OS
❤‍🔥52🤯16🔥118👍6