Грокаем C++
9.37K 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
​​Самая надежная гарантия отсутствия исключений
#опытным

Исключения не любят не только и не столько потому, что они нарушают стандартный поток исполнения программы, могут привести к некорректному поведению системы и приходится везде писать try-catch блоки. Исключения - это не zero-cost абстракция. throw требуют динамические аллокации, catch - RTTI, а в машинном коде компилятор обязан генерировать инструкции на случай вылета исключений. Плюс обработка исключений сама по себе медленная.

Поэтому некоторые и стараются минимизировать использование исключений и максимально использовать noexcept код.

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

Есть такой флаг компиляции -fno-exceptions. Он запрещает использование исключений в программе. Но что значит запрет на использование исключений?

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

int main() {
throw 1; // even this doesn't compile
}


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

int main() {
// even this doesn't compile
try {
} catch(...) {
}

}


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

std::map<int, int> map;
std::cout << map.at(1) << std::endl;


Моментальное завершение работы
. Оно как бы и понятно. Метод мапы at() кидает std::out_of_range исключение, если ключа нет в мапе. Обрабатывать исключение нельзя, поэтому чего вола доить, сразу терминируемся. И никакой вам раскрутки стека и graceful shutdown. Просто ложимся и умираем, скрестив ручки.

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

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

Как тогда код писать? А об этом через пару постов.

Handle errors. Stay cool.

#cppcore #compiler
👍2511🔥6😁3❤‍🔥2🤔1
​​Как стандартная библиотека компилируется с -fno-exceptions?
#опытным

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

Ответ прост. Макросы, товарищи. Вся магия в них. Вот на что заменяется обработка исключений:

#if __cpp_exceptions
# define __try try
# define __catch(X) catch(X)
# define __throw_exception_again throw
#else
# define __try if (true)
# define __catch(X) if (false)
# define __throw_exception_again
#endif


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

Ну и для большинства классов, унаследованных от exception, существуют соответствующие функции с C-линковкой:

#if __cpp_exceptions
void __throw_bad_exception()
{ throw bad_exception(); }
#else
void __throw_bad_exception()
{ abort(); }
#endif


Тогда любая функция, которая бросает исключения должна триггерить std::abort. Или нет?

Нет. Вот примерчик.

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

Чтобы это исправить, можно собрать ее с запретом исключений. Примерно так:

git clone git://gcc.gnu.org/git/gcc.git
cd gcc
git checkout <target_release_tag>
./configure
--disable-libstdcxx-exceptions
CXXFLAGS="-fno-exceptions <all_flags_that_you_need>"

make -j$(nproc)
make install


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

Extend your limits. Stay cool.

#compiler
14👍9🔥9❤‍🔥3🤔2
Создавайте технологии, которые меняют мир

В команду Яндекса нужны бэкенд-разработчики с опытом от 3 лет на C++, Python, Java/Kotlin, Go — строить полезные сервисы для миллионов пользователей.

Как получить офер за неделю?

• До 12 ноября оставить заявку на участие и пройти предварительный этап.
• 15–16 ноября решить задачи на технических секциях.
• 17–21 ноября прийти на финальную встречу.

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

Читайте подробности и оставляйте заявку на сайте.
8🔥7👎6👍4😁1
​​Bad practice. Возврат ошибки. Кастомная структура
#новичкам

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

Обернем это все в класс и сделаем его типом возвращаемого значения!

template<typename T>
struct Result {
T value;
std::string error;

static Result ok(T val) {
return Result{std::move(val), {}};
}

static Result fail(std::string err_msg) {
return Result{T{}, std::move(err_msg)};
}

operator bool() const { return error.empty(); }
};

Result<double> safe_divide(double a, double b) {
if (b == 0.0) {
return Result<double>::fail("Division by zero");
}
return Result<double>::ok(a / b);
}

auto div_result = safe_divide(10.0, 2.0);
if (div_result) {
std::cout << "Result: " << div_result.value << std::endl;
} else {
std::cout << "Error: " << div_result.error << std::endl;
}


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

Этот подход работает, но у него есть несколько весомых недостатков:

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

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

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

You can do better. Stay cool.

#badpractice
👍2210🔥6
Возврат ошибки. std::variant
#новичкам

Если у вас есть С++17, то поздравляю, у вас есть std::variant, который решает проблему суперпозиции полей из прошлого поста.

По сути, std::variant - это типобезопасный юнион, который хранит только один тип из списка шаблонных параметров. Объект варианта можно проверить на наличие нужного типа и есть способы no-exceptions сообщения об ошибке, если вы хотите получить доступ не к тому типу. Обычный std::get кидает исключение при неправильном доступе, но std::holds_alternative или std::get_if предоставляют небросающий апи:

struct Error {
std::string message;
};

std::variant<double, Error> safe_divide(double a, double b) {
if (b == 0.0) {
return Error{std::string{"Division by zero"}};
}
return a / b;
}

auto div_result = safe_divide(10.0, 2.0);
if (std::holds_alternative<double>(div_result)) {
std::cout << "Result: " << std::get<double>(div_result) << std::endl;
} else {
std::cout << "Error: " << std::get<Error>(div_result).message << std::endl;
}


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

К этому привело то, что семантика "результат или ошибка" не заложена в самой идее std::variant. Для него все типы изначально равнозначны и равноожидаемы. Нет плохого и хорошего типа.

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

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

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

В небросающем коде нужно обязательно проверять каждый доступ к объекту варианта, потому что std::get кидает исключение(вообще говоря, в любом коде). Ну или сразу используйте std::get_if, если точно знаете, какой должен быть тип, но нужно подстраховаться от ошибок.

Это хороший вариант, но больно пользоваться. Хочется более элегантного решения решения для этой проблемы. И оно есть! О этом в следующем посте.

Use a right semantic. Stay cool.

#cpp17
17👍10🔥7
Сколько инструментов для уменьшения бинарного файла вы знаете? А если стоит задача не повредить функциональность?
Решить такую проблему вызвался инженер YADRO. Он нашел несколько способов, которые помогут отсечь лишнее:

• Bloaty — инструмент для профилирования размера бинарных файлов;
• флаги компилятора и линковки;
• дешаблонизация и оптимизация кода.

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

Читайте статью на Хабре →
🔥10👍54
Возврат ошибки. std::expected
#опытным

В С++23 появился практически идеальный класс для работы с объектами ошибки - std::expected.

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

struct Error {
std::string message;
};

std::expected<double, Error> safe_divide(double a, double b) {
if (b == 0.0) { // здесь нужна нормальная проверка на равенство с epsilon
return std::unexpected(Error{"Division by zero"});
}
return a / b;
}

auto div_result = safe_divide(10.0, 2.0);

if (div_result.has_value()) {
std::cout << "Result: " << div_result.value() << std::endl;
} else {
std::cout << "Error: " << div_result.error().message << std::endl;
}
// или с операторами
if (div_result) { // operator bool
std::cout << "Result: " << *div_result << std::endl; // operator*
} else {
std::cout << "Error: " << div_result.error().message << std::endl;
}


По сути у std::expected в базовом интерефейсе 3 метода и пара операторов. Методы has_value(), value() и error() для проверки и доступа к значению или ошибке. И operator bool, operator*, operator-> для ленивых со сточенными пальцами;

Преимущества нового типа std::expected по сравнению с std::variant:

Хранит только два типа: значение и ошибка.

Делает код интуитивно понятнее, поскольку для создания ошибки нужно использовать std::unexpected. Это особенно удобно, когда тип ошибки std::string. В этом случае использование std::unexpected{ "Something bad happens" } позволяет явно обозначить в коде, что мы не просто строку возвращаем, а сообщение об ошибке.

Предоставляет простой и лаконичный базовый интерфейс: 3 метода и пара операторов. Методы has_value(), value() и error() для проверки и доступа к значению или ошибке. И operator bool, operator*, operator->, кому лень писать названия методов.

С std::expeсted удобно работать, если есть всего один тип результата и один тип ошибки. Работать с std::expected<std::variant<Type1, Type2>, Error> или std::expected<Type, std::variant<Error1, Error2>> не так удобно, как просто с вариантом из трех типов. Если нужно возвращать больше ошибок, то можно пользоваться разными вариантами кодов ошибки от enuma'а до std::error_code или даже просто строкой.

Must have при работе без исключений.

Use a right semantic. Stay cool.

#cpp23
🔥2313👍11
​​Возврат ошибки. std::optional
#опытным

У std::variant довольно громоздкий интерфейс при возврате ошибки вместе с результатом работы функции. Но в С++17 появился еще один класс, который имеет семантику "Или" для типов + более дружелюбный интерфейс.

Это std::optional. Этот шаблонный класс либо содержит нужный тип, либо не содержит его. Вот так может выглядеть код:

struct Error {
std::string message;
};

std::optional<double> safe_divide(double a, double b) {
if (b == 0.0) { // здесь нужна нормальная проверка на равенство с epsilon
return std::nullopt;
}
return a / b;
}

auto div_result = safe_divide(10.0, 2.0);

if (div_result.has_value()) {
std::cout << "Result: " << div_result.value() << std::endl;
} else {
std::cout << "Error: there is no value" << std::endl;
}
// или с операторами
if (div_result) { // operator bool
std::cout << "Result: " << *div_result << std::endl; // operator*
} else {
std::cout << "Error: there is no value" << std::endl;
}


Для того, чтобы вернуть пустой optional, используется константа std::nullopt. А в остальном интерфейс очень похож на std::expected за исключением доступа к ошибке.

Но на мой взгляд, std::optional не очень подходит для обработки ошибок.

👉🏿 Он имеет семантику наличия или отсутствия значения. Отсутствие значения - это в принципе нормальная ситуация в программировании. Вы сделали Select к базе и получили пустоту, запросили что-то по апи и получили пустоту - вот самое место для std::optional.

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

👉🏿 Если вам нужно специфицировать, какая конкретно ошибка произошла, то std::optional умывает руки. Нужно либо output параметры использовать, либо в принципе другой класс.

Если есть 23-й стандарт или доступ к бусту, то лучше использовать std::expected или boost::outcome.

Use the right tool. Stay cool.

#cpp17
14🔥8👍5😁2👎1
​​Что не так с модулями?
#опытным

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

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

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

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

Короткий пример:

// math.cppm - файл модуля
export module math; // Объявление модуля

import <vector>; // Импорт, а не включение

// Макросы НЕ экспортируются!
#define PI 3.14159

// Явный экспорт - только то, что нужно
export double calculate_circle_area(double radius);

// Внутренние функции скрыты
void internal_helper();


и его использование:

// main.cpp - обычный С++ файл
import math; // Импорт интерфейса, не всего кода

// Используем экспортированную функцию
double area = calculate_circle_area(10);

// internal_helper(); // ERROR! функция скрыта
// double x = PI; // ERROR! макросы не экспортируются


Модули призваны решать следующие проблемы:

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

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

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

На словах - прекрасные плюсы будущего. Но на словах мы все Львы Толстые, а на деле...

А на деле это все до сих пор работает довольно костыльно. До 23, а скорее 24 года использовать модули было совсем никак нельзя. Сейчас все немного лучше, но реализации все еще пропитаны проблемами. А проекты не спешат переходить на модули. Но почему?

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

😡 Бинарный формат модулей нестандартизирован. Каждый компилятор выдумывает свое представление, которое несовместимо между компиляторами или даже версиями одного компилятора.

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

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

😡 Ускорение компиляции может неоправдать затрат. В среднем ускорение составляет порядка 30%. И это просто не стоит усилий.

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

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

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

Use new features. Stay cool.

#cppcore #compiler #tools
16👍6🔥6
Вы используете модули С++20 в проде? Какой у вас компилятор?
Anonymous Poll
76%
Не использую
15%
Использую. GCC
10%
Использую. Clang
7%
Использую. MSVC
😁9🔥43👎1
Output параметры
#новичкам

Если у вас нет std::variant, std::expected, std::optional, вам лень засовывать ошибки в объекты, то у вас остается не так много вариантов. Один из них - output параметры функции.

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

Но конфигурация возвращаемого значения и параметров функции может быть разная:

👉🏿 С-style. Возвращаем int, который каким-то образом кодирует ошибку + потенциально какую-то полезную информацию, а результат работы функции записывается в выходной параметр. Например системный вызов read:

ssize_t read(size_t count, int fd, void buf[count], size_t count);


Возвращает количество прочитанных байт, либо -1, если произошла ошибка. Конкретная ошибка передается через errno. Сами данные записываются в buf.

Так как это не С++ подход, то он содержит все недостатки отсутствия ООП.

👉🏿 Код результата 1. Возвращаем enum успешности операции, а сам результат возвращаем в одном или нескольких выходных параметрах.

enum Code {
Success,
DivisionByZero,
NegativeNumber,
Overflow
};

Code safe_sqrt(double x, double& result) {
if (x < 0) {
return Code::NegativeNumber;
}
result = std::sqrt(x);
return Code::Success;
}

double result = 0.;
auto code = safe_sqrt(-4.0, result);
if (code != Code::Success) {
process_error(code);
} else {
std::cout << "Result: " << result << std::endl;
}


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

👉🏿 Код результата 2. Наоборот, возвращаем результат, а в выходные параметры передаем потенциальную ошибку:

double safe_sqrt(double x, Code& code) {
if (x < 0) {
code = Code::NegativeNumber;
return {};
}
code = Code::Success;
return std::sqrt(x);
}

Code code = Code::Success;

auto result = safe_sqrt(-4.0, code);
if (code != Code::Success) {
process_error(code);
} else {
std::cout << "Result: " << result << std::endl;
}


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

Этот подход используется в том числе в стандартной библиотеке:

bool exists( const std::filesystem::path& p, std::error_code& ec ) noexcept;


Теперь уже заранее надо код результата инициализировать.

👉🏿 Универсальный. Можно ввести и универсальный кодстайл: возвращаем bool в качестве индикатора успешности выполнения функции, а результаты и ошибки возвращаем в output параметрах.
bool parse_coordinates(const std::string& input, 
double& x, double& y, double& z,
std::string& error_message);
double x, y, z;
std::string error_msg;

if (parse_coordinates("10.5,20.3,30.7", x, y, z, error_msg)) {
std::cout << "Coordinates: " << x << ", " << y << ", " << z << std::endl;
} else {
std::cout << "Error: " << error_msg << std::endl;
}


Здесь сигнатура явно форсит проверить результат(особенно с nodiscard). Но еще больше данных нужно объявлять до вызова функции и код становится все менее декларативным.

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

Prevent misuse. Stay cool.

#design
3👍1613🔥7
Обработка ошибок Шердингера
#опытным

Мы уже поговорили о том, что есть 2 подхода к обработке ошибок - исключения и возврат кода ошибки(std::expected или output параметры).

И хоть стандартная библиотека насквозь пропитана исключениями, она все-таки иногда, очень редко предоставляет альтернативные варианты. Например std::from_chars или std::to_chars.

Интересно, что в библиотеке std::filesystem очень многие функции и методы имеют две перегрузки работы с данными: одна с исключениями, другая - без. Например:

bool exists( const std::filesystem::path& p );
bool exists( const std::filesystem::path& p, std::error_code& ec ) noexcept;

// or

bool remove( const std::filesystem::path& p );
bool remove( const std::filesystem::path& p, std::error_code& ec ) noexcept;


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

Однако выше приведены "образцово показательные" перегрузки. Посмотрите вот на это:

directory_iterator& operator++();
directory_iterator& increment( std::error_code& ec );


Есть класс std::filesystem::directory_iterator и эти итераторы нужно уметь инкрементировать, чтобы двигаться по элементам директории. Так как сигнатура операторов в С++ не поддерживает лишние параметры, то для варианта с кодом ошибок приходится определять именованный метод.

Обратите внимание, что increment не объявлен как noexcept!

То есть используя increment, вы не можете гарантировать отсутствие исключений. Да, ошибки при работе с файловой системой ОС передаются в качестве кодов ошибок. Но тот же std::bad_alloc increment кинуть может.

По всей видимости, мотивация не выбрасывать исключения связана с тем, что вызывающие стороны, использующие версию с исключениями, часто замусорены локальными блоками try/catch для обработки «рутинных» событий. Условно: при работе с файлами может оказаться, что у программы нет прав доступа для них. Это в целом нормальная ситуация в файловой системе, но в первой перегрузке эти ситуации репортятся через исключения, как исключительные ситуации.

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

Don't be confused. Stay cool.

#cpp17
👍1710🔥6😁2
Всегда приятно, когда громко рассказывают об успехах отечественных технологий. Я к этому хоть отношения никакого не имею, но все равно на душе как-то приятно становится.

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

Сегодня хочется рассказать об одном необычном для русского айти событии. Программист-любитель просто взял и организовал соревнование по алгоритмическому программированию на C/C++ под «Эльбрусы» (e2k). Соревы собрали 31 студентов со всей России в онлайн-формате, призовым фондом были личные 215 тысяч рублей организатора.

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

Обязательно зайдите и поддержите автора.

А еще в марте следующего года будет еще одно такое мероприятие. Так что если вы студент и знакомы с Эльбрусами, вас будут там ждать)
16👍14🔥7👎3🗿1
​​Уплощаем многомерный массив
#опытным

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

std::vector<int> Process(const std::string& str);

std::vector<std::string> elems = ...;

auto result_view = elems | std::views::transform([](const std::string& str) {
return Process(str);
})


Итоговое отображение result_view - это по факту набор векторов. Чтобы сложить это все в один массив нужен двойной цикл. А можно как-то удобно и лаконично получить плоский вектор интов?

С помощью С++20 отображения std::views::join:

std::vector<int> Process(const std::string& str);

std::vector<std::string> elems = ...;

auto result = elems | std::views::transform([](const std::string &str) {
return Process(str);
}) |
std::views::join | std::ranges::to<std::vector>();

std::print("{}", result);


Это все сработает и на экране появлятся заветные чиселки.

Здесь используется std::ranges::to и std::print, которые добавлены в 23-м стандарте

Если у вас элементы, которые хотелось бы переместить, а не скопировать, то можно добавить еще с++23 отображение as_rvalue:

auto result = elems | std::views::transform([](const auto & elem) {
return Process(elem);
}) |
std::views::join | std::views::as_rvalue |
std::ranges::to<std::vector>();


Если хочется чистого кода без циклов, то рэнджи для этого и сделаны.

Don't stuck in a loop. Stay cool.

#cpp20 #cpp23
19👍13🔥7
Константная мапа
#новичкам

Определяете вы какое-нибудь отображение в коде:

using CommandCreator = std::function<std::unique_ptr<ICommand>(const std::vector<std::string>&)>;
const std::unordered_map<std::string, CommandCreator> commands = {
{"create", [](const std::vector<std::string>& vec){return std::make_unique<CreateCommand>(vec)}},
{"delete", [](const std::vector<std::string>& vec){return std::make_unique<DeleteCommand>(vec)}},
{"save", [](const std::vector<std::string>& vec){return std::make_unique<SaveCommand>(vec)}}
};


Никаких больше команд вы не обрабатываете, отображение константно.

И вот вы хотите получить доступ к элементам мапы:

auto command = commands[command_name](vec);
command->Execute();


И тут бац! И ошибка компиляции о том, что нет такого оператора[], который бы принимал константную std::unordered_map.

Почему так? У вектора же есть.

Проблема тут комплексная.

Здесь мы рассказали о том, что operator[] у мапы имеет одну особенность. Если вы ему передаете новый ключ, то он изменяет мапу и вставляет в нее элемент с этим новым ключом и дефолтным значением.

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

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

Вот и решили конструировать объект налету.

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

Поэтому решили проблему гениально: вообще не вводить эту версию оператора.

В условиях отсутствия константного operator[] для std::map и std::unordered_map вы можете использовать либо метод at(), который бросает std::out_of_range, если ключа нет. Или find(), который вернет итератор на конец мапы:

auto command = commands.at(command_name)(vec);
command->Execute();
// or
if (auto it = commands.find(command_name); it != commands.emd()) {
auto command = it->second();
command->Execute();
} else {
std::cout << "ERROR" << std::endl;
}


Find compromis. Stay cool.

#STL
20👍14🔥8😱1
​​pointer to data member
#опытным

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

struct Payment {
double amount;
std::string category;
};
auto max = *std::ranges::max_element(payments, {}, &Payment::amount);


max в этом случае будет транзакцией с максимальным размером платежа.

В последней строчке используется &Payment::amount - указатель на поле amount в классе Payment. Но если это параметр функции, то это значение какого-то типа. Но какой тип у этого указателя?

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

Явный тип указателя на поле класса используется так:

struct Payment {
double amount;
std::string category;
};

double Payment::*ptr = &Payment::amount; // Here!

Payment payment{3.14, "Groceries"};
std::cout << payment.*ptr << std::endl;
// OUTPUT:
// 3.14


double Payment::*       ptr         = &Payment::amount;
// тип указателя имя указателя инициализатор


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

Мы обязательно должны указать, на какой тип полей этот указатель может указывать. Таким образом указатель ptr может указывать на любое поле класса Payment, имеющее тип double. То есть:

struct Type {
int a;
int b;
float c;
};

int Type::*p = nullptr;

p = &Type::a; // OK, a is int
p = &Type::b; // OK, b is int

p = &Type::c; // ERROR! c is float


Указателю на интовое поле нельзя присвоить указатель на флотовое. И наоборот, указатель p работает с любыми полями типа int.

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

// Takes pointer to any data member for any type
template<typename T, typename FieldType>
void print_field(const T& obj, FieldType T::*field) {
std::cout << obj.*field << std::endl;
}

Payment payment{3.14, "Groceries"};
Type t(42, 69, 3.14);

print_field(payment, &Payment::amount);
print_field(payment, &Payment::category);

print_field(t, &Type::a);
print_field(t, &Type::b);
print_field(t, &Type::c);
// OUTPUT
// 3.14
// Groceries
// 42
// 69
// 3.14


print_field может печатать значение любого поля любого класса по его указателю. Обратите внимание на шаблонную сигнатуру.

Walk through the nooks and crannies. Stay cool.

#cppcore #memory
322🔥14👍11
​​WAT
#опытным

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

Всегда ли nullptr указатель равен нулю?

Казалось бы в названии дан ответ:

int * p = nullptr;
std::cout << std::boolalpha << (p == nullptr) << "\n";
std::cout << std::hex << std::bit_cast<std::uintptr_t>(p) << "\n";
// OUTPUT
// true
// 0


Но в общем случае это неправда! Смотрим на пример:

struct A {
int i;
};

int main() {
int A::* p = 0;

std::cout << std::boolalpha << (p == nullptr) << "\n";
std::cout << std::hex << std::bit_cast<std::uintptr_t>(p) << "\n";
std::cout << std::boolalpha << (std::bit_cast<std::uintptr_t>(p) == 0xffffffffffffffff) << "\n";
}
// OUTPUT:
// true
// ffffffffffffffff
// true


nullptr указатель равен совсем не нулю, как декларировалось в начале main.

WAT? Что за фокусы с пропажей нуля?

Во вчерашнем посте мы рассказывали об особом типе указателя - pointer to data member. Этот указатель, которым и является p из примера, по сути хранит информацию о том, как в объекте найти нужное поле класса.

И в большинстве случаев эта информация представляет собой просто смещение поля относительно начала объекта в байтах.

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

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

std::cout << std::hex << static_cast<long long int>(-1) << "\n";
// OUTPUT:
// ffffffffffffffff


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

struct Type {
double a;
char b;
float c;
long long d;
short e;
unsigned f;
};

std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::a) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::b) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::c) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::d) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::e) << "\n";
std::cout << std::dec << std::bit_cast<std::uintptr_t>(&Type::f) << "\n";

// OUTPUT:
// 0
// 8
// 12
// 16
// 24
// 28


Опять же, интересный уголок плюсов.

Walk through the nooks and crannies. Stay cool.

#cppcore #memory
20👍11🔥8🤯4❤‍🔥2
​​Сколько весит объект полиморфного класса?
#новичкам

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

У нас уже был пост про размер объекта пустого класса.

А что если это будет класс с виртуальными методами? Сколько тогда будет весить этот класс?

struct SomeClass {

virtual ~SomeClass() = default;

virtual void Process() {
std::cout << "Process" << std::endl;
}
};


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

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

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

Значит надо ее класть в каждый объект. И самое простое - положить в них указатель на свою таблицу виртуальных функций. Так и делают на самом деле. Этот указатель называют vptr.

Соответственно размер класса зависит от битности системы. Для 64-бит указатель имеет размер 8 байт(64 бита) поэтому и размер класса SomeClass будет 8 байт.

std::cout << sizeof(SomeClass) << std::endl;
// OUTPUT
// 8


Пустые наследники SomeClass кстати тоже будут иметь размер 8 из-за того, что им нужно лишь другое значения указателя.

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

Be lightweight. Stay cool.

#cppcore #interview
119👍14🔥7😁7🆒1
Комьюнити, полезное для всех бекенд-разработчиков

Как работает VK изнутри? Что происходит за интерфейсами, когда миллионы пользователей одновременно отправляют сообщения, загружают фото и смотрят клипы?
В канале Backend VK Hub мы рассказываем о работе всех наших сервисах: от VK Play до Tarantool. Делимся подходами к масштабированию, оптимизации и новым архитектурным решениям. Открыто дискутируем, а также регулярно публикуем вакансии в нашу команду.

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

Подписывайся!
4🔥4👍3👎3
join
#опытным

Как прекрасно сделан в питоне метод join у строки. Чтобы соединить список строк разделителем нужно просто написать:

my_list = ["John", "Peter", "Vicky"]
x = " ".join(my_list)
print(x)
# OUTPUT
# John Peter Vicky


И как же сложно того же результата достичь в плюсах!

То делают через потоки:

std::string join(const std::vector<std::string>& vec, const std::string& delimiter) {
if (vec.empty()) return "";

std::ostringstream oss;
oss << vec[0];

for (size_t i = 1; i < vec.size(); ++i) {
oss << delimiter << vec[i];
}

return oss.str();
}


то через std::accumulate:

std::string join(const std::vector<std::string>& vec, const std::string& delimiter) {
if (vec.empty()) return "";

return std::accumulate(
std::next(vec.begin()), vec.end(),
vec[0],
[&delimiter](const std::string& a, const std::string& b) {
return a + delimiter + b;
}
);
}


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

Но в С++23 наконец-то появилось хоть что-то похожее на адекватное решение. Используем std::views::join_with:

std::string join(const std::vector<std::string> &vec,
const std::string &delimiter) {
return vec | std::views::join_with(delimiter) |
std::ranges::to<std::string>();
}


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

И жизнь стала чуть-чуть счастливее...

Make thing simple. Stay cool.

#cpp23
24👍9🔥9😁5