Грокаем 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
Помогите Доре найти проблему в коде
#опытным

Наткнулся на просторах всемирной сети на интересный пример:

#include <cstdio>

void bar(char * s) {
printf("%s", s);
}

void foo() {
char s[] = "Hi! I'm a kind of a loooooooooooooooooooooooong string myself, you know...";
bar(s);
}

int main() {
foo();
}


Код работает и пример довольно игрушечный. Однако в этом С++ коде есть проблема/ы. Сможете ли вы их найти?

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

Critique your solutions. Stay cool.

#fun
🤔19👍6🔥43😁1😎1
​​Ответ

Поговорим о том, что не так в коде из предыдущего поста:

#include <cstdio>

void bar(char * s) {
printf("%s", s);
}

void foo() {
char s[] =
"Hi! I'm a kind of a loooooooooooooooooooooooong "
"string myself, you know...";
bar(s);
}

int main() {
foo();
}


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

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

🔞 В bar() нет никакой проверки границ. Почему-то функция надеется, что когда-нибудь она встретит null-terminator. Но этого спокойно может и не быть: передадим туда обычный массив символов и будет UB.

🔞 Каждый раз при вызове foo() мы кладем на стек то, что должно храниться в сегменте данных, где обычно хранятся строковые литералы. То есть вместо того, чтобы по указателю ссылаться на строку, foo копирует ее на стек и дальше использует. Это ненужные действия, которые негативно сказываются на производительности. Если конечно мы вообще можем говорить о производительности в рамках этого кода.

Как мог бы выглядеть бы код на современных плюсах?

#include <print>
#include <string_view>

void bar(std::string_view s) {
std::println("{}", s);
}

void foo() {
constexpr std::string_view s =
"Hi! I'm a kind of a loooooooooooooooooooooooong "
"string myself, you know...";
bar(s);
}

int main() {
foo();
}


Всего 2 простых улучшения:

Использование легковестного std::string_view из С++17. Это по сути просто указатель + размер данных, так что накладные расходы на этот объект минимальны. А еще его даже рекомендуют передавать в функции по значению.

Вместо сишной вариабельной нетипобезопасной функции printf используем типобезопасную плюсовую std::println на вариабельных шаблонах из С++23.

Простые улучшения, но в итоге все неприятности пофиксили. Магия С++.

Believe in magic. Stay cool.

#cppcore #cpp23 #cpp17
27👍17🔥7👎4🤷‍♂1
Квиз
#опытным

В тему std::invoke закину вам интересный #quiz. Будем вызывать ведьм мемберы класса.

Что выведется на консоль в результате попытки компиляции и запуска следующего кода?

#include <functional>
#include <iostream>

struct Data {
int memberFunction(int value) {
return value;
}
int field = 42;
};

int main() {
Data data;
auto methodPtr = &Data::memberFunction;
auto fieldPtr = &Data::field;
std::cout << std::invoke(methodPtr, data, std::invoke(fieldPtr, data)) << std::endl;
}


Explore details. Stay cool.
12🔥6👍2
Квиз

Сегодня #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
11👍6🔥52
Почему еще важен 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
🔥47🤯308👍7❤‍🔥2
Ревью
#новичкам

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

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

Сегодняшний лот:
#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.
1030🔥15👍10😱2
Динамический полиморфизм: разделяемые библиотеки
#опытным

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

Обычно разделяемые библиотеки загружаются на самом старте программы(какие-нибудь libc и libstdc++ например неявно подгружаются на старте). Основную часть таких библиотек мы прописываем в опциях линковки.

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

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

Тут пример будет довольно длинный, поэтому начнем с начала.

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

class PluginInterface {
public:
virtual int method() = 0;
};

extern "C" PluginInterface* create_plugin();

extern "C" void destroy_plugin(PluginInterface* obj);


Эта команда берет и реализует этот интерфейс:

#include "PluginInterface.hpp"
#include <iostream>

class MyPlugin : public PluginInterface {
public:
virtual void method() override;
};

int MyPlugin::method() {
std::cout << "Method is called\n";
return 42;
}

extern "C" PluginInterface* create_plugin() {
return new MyPlugin();
}

extern "C" void destroy_plugin(PluginInterface* obj) {
delete obj;
}


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

Функции create_plugin и destroy_plugin обязаны иметь сишную линковку, чтобы достать указатели на них по их имени из библиотеки с помощью dlsym:

#include "PluginInterface.hpp"
#include <dlfcn.h>
#include <iostream>

typedef PluginInterface *(*creatorFunction)();
typedef void (*destroyerFunction)(PluginInterface *);

int main() {
void *handle = dlopen("myplugin.so", RTLD_LAZY);
if (!handle) {
std::println("dlopen failure: {}", dlerror());
return 1;
}
creatorFunction create = reinterpret_cast<creatorFunction>(dlsym(handle, "create_plugin"));
destroyerFunction destroy = reinterpret_cast<destroyerFunction>(dlsym(handle, "destroy_plugin"));

PluginInterface *plugin = (*create)();
std::println("{}", plugin->method());
(*destroy)(plugin);
dlclose(handle);
}


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

Разве по имени функции можно получить указатель на нее? Похоже на какую-то рефлексию с первого взгляда.

Тут дело в именах функций и отображении их в символы бинарного файла при компиляции. В С нет никакого манглинга имен, поэтому в готовом бинарном файле можно найти символ, соответствующий названию функции, и связанный с ним адрес этой фукнции. Именно поэтому create_plugin и destroy_plugin помечены extern "C", чтобы их имена обрабатывались по правилам С.

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

Choose the right name. Stay cool.

#cppcore #OS #compiler
1029👍9🔥8
​​WAT
#новичкам

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

Дан простой кусок кода:

#include <array>
#include <cstring>
#include <iostream>

int main(int argc, char *argv[]) {
const char *string{nullptr};
std::size_t length{0};
if (const bool thisIsFalse = argc > 100000;
thisIsFalse) {
string = "ABC";
length = 3;
}

std::array<char, 128> buffer;
std::memcpy(buffer.data(), string, length);

if (string == nullptr) {
std::cout
<< "String is null, so cancel the launch.\n";
} else {
std::cout << "String is not null, so launch the "
"missiles!\n";
}
}


Единственный вопрос: что выведется на экран при запуске программы без аргументов?

Подумайте несколько секунд.

"Да все очевидно же. string не меняется, поэтому сообщение об этом и выведется на экран".

Но мы же на плюсах пишем, тут невозможное становится возможным.

Например, при компиляции на gcc на О3 оптимизациях выводится String is not null, so launch the missiles!

"WAT? Где пруфы?"

А вот они.

Виновато конечно во всем ненавистное UB. Все грязные тряпки кидайте в него.

По стандарту, если в memcpy передать нулевой указатель, то поведение неопределено. Может случиться все, что угодно.

Это может произойти, только если количество аргументов запуска программы меньше 100000. То есть одна ветка приводит к UB, а вторая нет. И на основании этого gcc делает вывод, что порченная ветвь кода никогда не должна выполняться (так как UB означает, что поведение программы не определено, то компилятор может предполагать, что UB не должно происходить) и просто выкидывает эту ветку из ассемблера.

Уберите условие, либо memcpy, то вывод будет ожидаемым. Либо UB не будет, либо эвристики компилятора по-другому заработают.

Пишите качественный и безопасный код, чтобы не было таких неожиданностей.

Be safe. Stay cool.

#cppcore
26🔥10👍7❤‍🔥3
​​std::getenv
#новичкам

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

- Конфигурации приложений
- Хранения чувствительных данных (паролей, ключей API)
- Управления поведением программ

Самый банальный пример - PATH. В этой переменной окружения находится список путей для поиска исполняемых файлов системой. Добавив путь к своей утилите в эту переменную, вы сможете ее запускать без указания полного пути.

Или например, более приближеный к плюсам пример, LD_LIBRARY_PATH. Это список путей, в котором линкер ищет указанные при линковке библиотеки.

И мы можем прочитать из плюсового кода переменные окружения с помощью С++11 функции std::getenv:

#include <cstdlib>

char* std::getenv(const char* name);


Это скоммунизженная из Сей функция, которая принимает имя переменной окружения и возвращает ее содержимое. Если искомой переменной не существует, возвращается nullptr.

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

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

Допустим, пишите вы какой-нибудь свой клиент-сервер на Boost.Asio. Хочется конфигурировать клиента адресом и портом сервера извне, чтобы иметь возможность по-разному запускать клиента локально и, допустим, через docker-compose. Конфиг и его парсилку писать довольно муторно, а адекватную парсилку аргументов командной строки - еще сложнее. Даже если использовать готовые решения в виде json парсера и boost.program_options.

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

#include <boost/asio.hpp>
#include <iostream>
#include <cstdlib>

using boost::asio::ip::tcp;

int main() {
try {
// HERE
const char* host = std::getenv("SERVER_HOST");
const char* port = std::getenv("SERVER_PORT");

if (!host || !port) {
std::cerr << "Please set SERVER_HOST and SERVER_PORT environment variables\n";
return 1;
}

boost::asio::io_context io_context;

// Создаем и соединяем сокет
tcp::socket socket(io_context);
tcp::resolver resolver(io_context);
boost::asio::connect(socket, resolver.resolve(host, port));

// Отправляем тестовое сообщение
std::string message = "Hello from Boost.Asio client!\n";
boost::asio::write(socket, boost::asio::buffer(message));

// Читаем ответ (до символа новой строки)
boost::asio::streambuf response;
boost::asio::read_until(socket, response, '\n');

// Выводим ответ
std::istream is(&response);
std::string reply;
std::getline(is, reply);
std::cout << "Server replied: " << reply << std::endl;

} catch (std::exception& e) {
std::cerr << "Exception: " << e.what() << "\n";
return 1;
}

return 0;
}


Всего две строчки и никакой мороки! Очень удобная и полезная функция.

Explore your enviroment. Stay cool.

#cpp11
27👍3812🔥9❤‍🔥3
​​Третий аргумент main
#новичкам

Почти всегда вы пишите функцию main вот так:

int main() {
// some code
}


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

int main (int argc, char* argv[]) {
// argc - количество переданных аргументов
// argv - массив из переданных аргументов

// some parsing
}


Однако в стандарте описан еще один способ определения main:


int main(/ implementation-defined /) {body}


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

Самая часто встречающаяся нестандартная сигнатура main следующая:

int main(int argc, char*  argv[], char* envp[]) {}


Третий аргумент main - это массив строк переменных окружения в формате
"KEY=value". Массив завершается null pointer'ом.

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

Вот минимальный примерчик:

#include <iostream>

int main(int argc, char* argv[], char* envp[]) {
std::cout << "Environment variables:\n";

for (char** env = envp; *env != nullptr; env++) {
std::cout << *env << "\n";
}

return 0;
}


Возможный вывод:

PATH=/usr/local/bin:/usr/bin:/bin
USER=grokaem_cpp
...


Вы не так часто можете увидеть этот формат сигнатуры main по уже очевидным для вас причинам:

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

Рассказываю про это, чтобы вы при встрече в таким форматом аргументов main не думали, что что-то базовое упустили при изучении плюсов. Ну или просто для расширения кругозора)

Expand your horizons. Stay cool.

#NONSTANDARD #goodoldc
👍5419🔥4😱4