Динамический полиморфизм: разделяемые библиотеки
#опытным
В тему указателей на функции вкину еще один способ реализации полиморфизма в С++ - разделяемые или динамические библиотеки.
Обычно разделяемые библиотеки загружаются на самом старте программы(какие-нибудь libc и libstdc++ например неявно подгружаются на старте). Основную часть таких библиотек мы прописываем в опциях линковки.
Однако динамические библиотеки можно неявно подгружать прямо из кода! Для этого на разных системах существует разное системное апи, но для юниксов это dlopen+dlsym.
dlopen по заданному пути файла библиотеки возвращает void указатель на хэндлер этой либы. С помощью хэндлера, функции dlsym и текстового названия определенной функции можно получить указатель на эту функцию и вызвать ее.
Тут пример будет довольно длинный, поэтому начнем с начала.
У вас есть какой-то интерфейс и вы хотите передать реализацию этого интерфейса другой команде, которая имеет чуть больше скилла в данной доменной области:
Эта команда берет и реализует этот интерфейс:
Также вы договорились, что каждая реализация интерфейса предоставляет 2 функции: создания и уничтожения наследников.
Функции create_plugin и destroy_plugin обязаны иметь сишную линковку, чтобы достать указатели на них по их имени из библиотеки с помощью dlsym:
С помощью dlopen и пути к библиотеке-реализации интерфейса получает хэндлер либы. Дальше получаем указатели на функции создания и уничтожения плагина с помощью dlsym, хэндлера и текстовому имени функции.
Разве по имени функции можно получить указатель на нее? Похоже на какую-то рефлексию с первого взгляда.
Тут дело в именах функций и отображении их в символы бинарного файла при компиляции. В С нет никакого манглинга имен, поэтому в готовом бинарном файле можно найти символ, соответствующий названию функции, и связанный с ним адрес этой фукнции. Именно поэтому create_plugin и destroy_plugin помечены extern "C", чтобы их имена обрабатывались по правилам С.
По сути, это все еще про указатели на функции, просто интересно, что на момент компиляции программы у вас может не быть реализации этих функции.
Choose the right name. Stay cool.
#опытным
В тему указателей на функции вкину еще один способ реализации полиморфизма в С++ - разделяемые или динамические библиотеки.
Обычно разделяемые библиотеки загружаются на самом старте программы(какие-нибудь 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.
Книга по C++, которую ты обязан прочитать в 2025 году
Я почему-то уверен, что она у тебя уже есть. Либо в списке литературы для самообразования, либо где-то среди скачанных pdf, а может, даже в печатном виде. Возможно, ты даже начинал её читать, но… работа, таски, собеседования… деньги вроде и так платят… и ты так и не осилил Мейерса "Эффективный и современный С++".
А ведь именно эта книга — ключ, который поможет тебе пробить потолок уровня middle и начать движение к senior.
Да, можно скачать список из 400 популярных вопросов, выучить их и бодро ответить на вопрос: «Расскажи про std::unique_ptr». А потом тебе прилетает дополнительный вопрос: «А как изменится размер std::unique_ptr при использовании пользовательского удалителя?» Иии… Я не буду додумывать твой уровень знаний. Попробуй сам ответить на этот вопрос и поставь себе оценку
И это только один из примеров тонких нюансов, которые описаны в этой книге и которые реально спрашивают на собеседованиях.
Ещё одна сильная сторона книги: почти каждая фича современного C++ иллюстрируется примерами её использования в реальном проде. А это большая редкость для технической литературы.
Хватит откладывать эту книгу на «потом». Поставь цель на этот год и, наконец-то, найди время, чтобы прочитать 300 страниц "Эффективного и современного С++".
Я почему-то уверен, что она у тебя уже есть. Либо в списке литературы для самообразования, либо где-то среди скачанных pdf, а может, даже в печатном виде. Возможно, ты даже начинал её читать, но… работа, таски, собеседования… деньги вроде и так платят… и ты так и не осилил Мейерса "Эффективный и современный С++".
А ведь именно эта книга — ключ, который поможет тебе пробить потолок уровня middle и начать движение к senior.
Да, можно скачать список из 400 популярных вопросов, выучить их и бодро ответить на вопрос: «Расскажи про std::unique_ptr». А потом тебе прилетает дополнительный вопрос: «А как изменится размер std::unique_ptr при использовании пользовательского удалителя?» Иии… Я не буду додумывать твой уровень знаний. Попробуй сам ответить на этот вопрос и поставь себе оценку
И это только один из примеров тонких нюансов, которые описаны в этой книге и которые реально спрашивают на собеседованиях.
Ещё одна сильная сторона книги: почти каждая фича современного C++ иллюстрируется примерами её использования в реальном проде. А это большая редкость для технической литературы.
Хватит откладывать эту книгу на «потом». Поставь цель на этот год и, наконец-то, найди время, чтобы прочитать 300 страниц "Эффективного и современного С++".
starts_with, ends_with
#новичкам
До (и включая) C++17, если вы хотите проверить начало или конец в строке на соответствие референсу, вы должны использовать самописные решения, буст или другие сторонние библиотеки. К счастью, это меняется с C++20.
В нем появляются стандартные методы
Как видите, они имеют три перегрузки: для string_view, одного символа и строкового литерала.
Кейсов применения этих методов предостаточно: валидация расширения файла, валидация url, html кода, префикса пути до файла, хэдэров http реквестов. В любом более менее большом проекте найдется местечко для этих методов.
Примерчик:
Здесь мы простейшим отображением ranges отфильтровываем строки вектора, которые являются тегами, и оставляем только текстовую часть.
В общем, полезные штуки, которые помогают заменить громоздкие кастомные проверки через find на использование выразительных функций.
Be expressive. Stay cool.
#новичкам
До (и включая) C++17, если вы хотите проверить начало или конец в строке на соответствие референсу, вы должны использовать самописные решения, буст или другие сторонние библиотеки. К счастью, это меняется с C++20.
В нем появляются стандартные методы
std::string/std::string_view .starts_with() и .ends_with():constexpr bool starts_with(string_view sv) const noexcept;
constexpr bool starts_with(CharT c ) const noexcept;
constexpr bool starts_with(const CharT* s ) const;
constexpr bool ends_with(string_view sv )const noexcept;
constexpr bool ends_with(CharT c ) const noexcept;
constexpr bool ends_with(const CharT* s ) const;
Как видите, они имеют три перегрузки: для string_view, одного символа и строкового литерала.
const std::string url { "https://isocpp.org" };
// string literals
if (url.starts_with("https") && url.ends_with(".org"))
std::cout << "you're using the correct site!\n";
// a single char:
if (url.starts_with('h') && url.ends_with('g'))
std::cout << "letters matched!\n";Кейсов применения этих методов предостаточно: валидация расширения файла, валидация url, html кода, префикса пути до файла, хэдэров http реквестов. В любом более менее большом проекте найдется местечко для этих методов.
Примерчик:
const std::vector<std::string> tokens {
"<header>",
"<h1>",
"Hello World",
"</h1>",
"<p>",
"This is my super cool new web site.",
"</p>",
"<p>",
"Have a look and try!",
"</p>",
"</header>"
};
auto text = tokens |
std::views::filter([](const std::string& s) {
if (s.starts_with("<") || s.ends_with(">"))
return false;
return true;
});
for (const auto& str : text)
std::cout << str << std::endl;
// OUTPUT:
// Hello World
// This is my super cool new web site.
// Have a look and try!Здесь мы простейшим отображением ranges отфильтровываем строки вектора, которые являются тегами, и оставляем только текстовую часть.
В общем, полезные штуки, которые помогают заменить громоздкие кастомные проверки через find на использование выразительных функций.
Be expressive. Stay cool.
С какой книги начать изучение многопоточки в C++
Традиционно изучение многопоточки рекомендуют начать с книги Энтони Уильямса "C++. Практика многопоточного программирования". Но, на мой взгляд, эта рекомендация сродни совету новичку читать Страуструпа или K&R. Да, в профессионализме этих авторов никто не сомневается, но все эти книги предназначены для инженеров с опытом, а не для разработчиков, которые делают свои первые шаги.
Поэтому рекомендую вам на время отложить книгу Уильямса, пока вы не познакомитесь с книгой Райнера Гримма "Параллельное программирование на современном С++". Уточню сразу, что главу по устройству памяти при первом знакомстве с многопоточкой лучше пропустить, как это и советует сделать автор, восприятие остального материала от этого не пострадает
Лично мне книга понравилась, с одной стороны, кратким описанием многопоточных конструкций С++, а, с другой стороны, множеством наглядных примеров их использования. Книгу достаточно легко читать, и я периодически ловил себя на мысли, что опять прочитал несколько десятков страниц и даже этого не заметил. Одним словом, рекомендую попробовать, возможно Гримм подойдет вам больше, чем Уильямс
Традиционно изучение многопоточки рекомендуют начать с книги Энтони Уильямса "C++. Практика многопоточного программирования". Но, на мой взгляд, эта рекомендация сродни совету новичку читать Страуструпа или K&R. Да, в профессионализме этих авторов никто не сомневается, но все эти книги предназначены для инженеров с опытом, а не для разработчиков, которые делают свои первые шаги.
Поэтому рекомендую вам на время отложить книгу Уильямса, пока вы не познакомитесь с книгой Райнера Гримма "Параллельное программирование на современном С++". Уточню сразу, что главу по устройству памяти при первом знакомстве с многопоточкой лучше пропустить, как это и советует сделать автор, восприятие остального материала от этого не пострадает
Лично мне книга понравилась, с одной стороны, кратким описанием многопоточных конструкций С++, а, с другой стороны, множеством наглядных примеров их использования. Книгу достаточно легко читать, и я периодически ловил себя на мысли, что опять прочитал несколько десятков страниц и даже этого не заметил. Одним словом, рекомендую попробовать, возможно Гримм подойдет вам больше, чем Уильямс
Конфигурация и переменные окружения
#опытным
Любой серьезный сервис нуждается в конфигурации. Файлы конфигурации (JSON, YAML, INI) — популярный способ хранения настроек приложений. Так параметры можно хранить в репозитории, версионировать, да и просто удобно, когда все можно менять в одном месте и никак не менять команду запуска.
Однако не одними конфигами едины. Не всегда они подходят для решения определенных задач.
Возьмем например ключи шифрования. Не всегда они генерируются новые, для интеграции двух партнеров могут использоваться ключи, которые обновляются раз в год или раз в полгода. Безопасно ли ключ шифрования выставлять в конфиге?
Не совсем. Что если какой-нибудь умник после тестирования приложения случайно закоммитит ключ в репозиторий? Это серьезная опасность: репозиторий вашей команды скорее всего может читать любой сотрудник, у которого есть доступ к вашей системе совместной разработки. А если у вас еще сторонние лица имеют доступ к репе... Не завидую вам. Безопасники будут радостно потирать ладоши, когда будут вам пистоны вставлять за эту ошибку. Потом еще ключ перевыпускать скомпрометированный, долго и мучительно заменять его... Сам наступал на эти грабли, приятного мало.
Да и хранить ключ в открытом виде в файле на сервере такое себе. А если кто-нибудь подглядит?
То же самое можно сказать про креды базы данных, in-memory кэша, брокеров сообщений и прочего. Пароли могут быть скомпрометированы.
А как с докерами и кубернетисами вашими работать? Иметь 100500 образов с разными настройками кредов и множить их постоянно? Выглядит, как не очень расширяемое решение.
Конечно же никто не хранит в конфигах чувствительные данные и специфичные для конкретного инстанса переменные. Вместо этого используют переменные окружения.
Переменные окружения можно установить видимыми только для конкретного запущенного docker контейнера:
В k8s можно брать переменые окружения из отдельно развернутого и защищенного Vault. В этом случае вообще отсутсвует явное указание секрета:
Переменные окружения не попадают в репозиторий -> нет компрометации секретов.
Можно без изменения конфига на одном и том же сервере тестировать приложение в разных контурах:
В общем, переменные окружения в приложении - полезная вещь, не стоит ими принебрегать.
К чему это я и причем здесь С++?
Ну нам же нужно выяснить, как в стандартных плюсах можно получать значения переменных окружения. Об этом поговорим в следующем посте.
Protect your secrets. Stay cool.
#опытным
Любой серьезный сервис нуждается в конфигурации. Файлы конфигурации (JSON, YAML, INI) — популярный способ хранения настроек приложений. Так параметры можно хранить в репозитории, версионировать, да и просто удобно, когда все можно менять в одном месте и никак не менять команду запуска.
Однако не одними конфигами едины. Не всегда они подходят для решения определенных задач.
Возьмем например ключи шифрования. Не всегда они генерируются новые, для интеграции двух партнеров могут использоваться ключи, которые обновляются раз в год или раз в полгода. Безопасно ли ключ шифрования выставлять в конфиге?
Не совсем. Что если какой-нибудь умник после тестирования приложения случайно закоммитит ключ в репозиторий? Это серьезная опасность: репозиторий вашей команды скорее всего может читать любой сотрудник, у которого есть доступ к вашей системе совместной разработки. А если у вас еще сторонние лица имеют доступ к репе... Не завидую вам. Безопасники будут радостно потирать ладоши, когда будут вам пистоны вставлять за эту ошибку. Потом еще ключ перевыпускать скомпрометированный, долго и мучительно заменять его... Сам наступал на эти грабли, приятного мало.
{
"data_key": "qwerty123" // Утечка при публикации кода!
}Да и хранить ключ в открытом виде в файле на сервере такое себе. А если кто-нибудь подглядит?
То же самое можно сказать про креды базы данных, in-memory кэша, брокеров сообщений и прочего. Пароли могут быть скомпрометированы.
# config.yml (попадает в Git)
db:
host: db.example.com
username: admin
password: "P@ssw0rd123!" # Утечка при публикации кода!
А как с докерами и кубернетисами вашими работать? Иметь 100500 образов с разными настройками кредов и множить их постоянно? Выглядит, как не очень расширяемое решение.
Конечно же никто не хранит в конфигах чувствительные данные и специфичные для конкретного инстанса переменные. Вместо этого используют переменные окружения.
Переменные окружения можно установить видимыми только для конкретного запущенного docker контейнера:
docker run -e MY_VAR=value my_image
В k8s можно брать переменые окружения из отдельно развернутого и защищенного Vault. В этом случае вообще отсутсвует явное указание секрета:
env:
- name: MY_VAR
- value: vault:my_group/my_service#my_var
Переменные окружения не попадают в репозиторий -> нет компрометации секретов.
Можно без изменения конфига на одном и том же сервере тестировать приложение в разных контурах:
# Local
export DB_HOST=localhost
# Dev
export DB_HOST=dev-db.example.com
В общем, переменные окружения в приложении - полезная вещь, не стоит ими принебрегать.
К чему это я и причем здесь С++?
Ну нам же нужно выяснить, как в стандартных плюсах можно получать значения переменных окружения. Об этом поговорим в следующем посте.
Protect your secrets. Stay cool.
Изменяемые лямбда-функции
Ключевое слово mutable используется для сохранения состояния в лямбда-функциях. Обычно оператор вызова функции замыкания является константным. Другими словами — лямбда не может модифицировать переменные, захваченные по значению.
Но ключевое слово mutable может быть применено ко всей лямбда-функции, что сделает все её переменные изменяемыми.
Следует заметить, что в отличии от mutable-переменных в объявлении класса, мутабельные лямбда-функции должны использоваться относительно редко и очень аккуратно. Сохранение состояния между вызовами лямбда-функции может быть опасным и контринтуитивным.
Ключевое слово mutable используется для сохранения состояния в лямбда-функциях. Обычно оператор вызова функции замыкания является константным. Другими словами — лямбда не может модифицировать переменные, захваченные по значению.
Но ключевое слово mutable может быть применено ко всей лямбда-функции, что сделает все её переменные изменяемыми.
Следует заметить, что в отличии от mutable-переменных в объявлении класса, мутабельные лямбда-функции должны использоваться относительно редко и очень аккуратно. Сохранение состояния между вызовами лямбда-функции может быть опасным и контринтуитивным.
Перегружаем шаблоны классов
#опытным
В прошлом посте я говорил, что нельзя перегружать шаблоны классов.
В принципе, это логично. Если у вас класс принимает другие шаблонные параметры, то скорее всего это должен быть другой класс.
Но как и практически любое ограничение в С++, его можно хакнуть.
Особенности вариадик шаблонов - их можно специализировать для любого набора и комбинации шаблонных параметров.
Мы просто вводим вариабельный класс-пустышку и специализируем его с любым количеством типов.
Вот такие фокусы.
Однако у этого способа есть ограничения. Элементы пака параметров должны быть так скажем одного вида. То есть вы не можете специализировать этот шаблон с типовым и нетиповым параметром:
Но все равно это хороший инструмент, которым можно пользоваться.
#опытным
В прошлом посте я говорил, что нельзя перегружать шаблоны классов.
В принципе, это логично. Если у вас класс принимает другие шаблонные параметры, то скорее всего это должен быть другой класс.
Но как и практически любое ограничение в С++, его можно хакнуть.
Особенности вариадик шаблонов - их можно специализировать для любого набора и комбинации шаблонных параметров.
template <typename... T>
struct Foo;
template <typename T1>
struct Foo<T1> {};
template <typename T1, typename T2>
struct Foo<T1,T2> {};
Мы просто вводим вариабельный класс-пустышку и специализируем его с любым количеством типов.
Вот такие фокусы.
Однако у этого способа есть ограничения. Элементы пака параметров должны быть так скажем одного вида. То есть вы не можете специализировать этот шаблон с типовым и нетиповым параметром:
template <typename T, int N>
struct Foo<T, N> {}; // forbidden
Но все равно это хороший инструмент, которым можно пользоваться.
ssize_t
#новичкам
Есть такой интересный тип ssize_t. Интересный он, потому что в отличии от стандартных типов имеет несимметричный относительно нуля диапазон значений [-1, SSIZE_MAX]. То есть это знаковый тип, но с нюансом, что отрицательное значение может быть только одно: -1.
Зачем такой тип нужен?
В posix api есть много функций, которые возвращают количество байт. Но так как это С и там нет исключений, а об ошибках как-то надо говорить, то значение -1 является индикатором ошибки:
Если вы работаете с сырыми дескрипторами, то явно пользуетесь функциями read и write, которые возвращают количество считанных или записанных байт соответственно. Если что-то пошло не так, то вместо количества байт возвращается -1:
Но почему этого типа нет в С++ стандарте? С его помощью мы бы могли например решить задачу итерации по контейнеру в обратном порядке из предыдущего поста.
Ответ простой, если подумать. Этот тип нужен только для апи, которое возвращает -1, как ошибку. В С++ есть исключения, объекты и шаблоны. С помощью этих трех инструментов можно как душе вздумается сообщать об ошибках. И это будет лучше и экспрессивнее, чем просто -1.
Use the right tool. Stay cool.
#новичкам
Есть такой интересный тип ssize_t. Интересный он, потому что в отличии от стандартных типов имеет несимметричный относительно нуля диапазон значений [-1, SSIZE_MAX]. То есть это знаковый тип, но с нюансом, что отрицательное значение может быть только одно: -1.
Зачем такой тип нужен?
В posix api есть много функций, которые возвращают количество байт. Но так как это С и там нет исключений, а об ошибках как-то надо говорить, то значение -1 является индикатором ошибки:
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void buf, size_t count);
Если вы работаете с сырыми дескрипторами, то явно пользуетесь функциями read и write, которые возвращают количество считанных или записанных байт соответственно. Если что-то пошло не так, то вместо количества байт возвращается -1:
char buffer[1024];
ssize_t bytes_read = read(fd, buffer, sizeof(buffer));
if (bytes_read == -1) { / Обработка ошибки */ }
Но почему этого типа нет в С++ стандарте? С его помощью мы бы могли например решить задачу итерации по контейнеру в обратном порядке из предыдущего поста.
Ответ простой, если подумать. Этот тип нужен только для апи, которое возвращает -1, как ошибку. В С++ есть исключения, объекты и шаблоны. С помощью этих трех инструментов можно как душе вздумается сообщать об ошибках. И это будет лучше и экспрессивнее, чем просто -1.
Use the right tool. Stay cool.
Например, регистры — это самый быстрый уровень памяти внутри CPU, кэш (L1/L2/L3) снижает latency за счёт локальности данных, RAM хранит рабочее состояние приложений, а SSD/HDD используются для персистентного хранения с существенно более высоким временем доступа.
На картинке — базовая иерархия памяти, взаимодействие CPU — cache — RAM — disk, а также упрощённая модель работы HDD и архитектура SSD.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
Недостатки std::make_shared. Непубличные конструкторы
#новичкам
std::make_shared - это сторонний код по отношению к классу, объект которого он пытается создать. Поэтому на принципиальную возможность создания объекта влияет спецификатор видимости конструктора.
Если конструктор публичный - проблем нет, просто вызываем std::make_shared.
Но things get trickier, если конструктор непубличный. Его тогда не может вызывать никакой чужой код.
Для определенности примем, что конструктор приватный. Это может делаться по разным причинам. Например у нас есть необходимость в создании копий std::shared_ptr, в котором находится исходный объект this. Тогда класс надо унаследовать от std::enable_shared_from_this и возвращать из фабрики std::shared_ptr. Если создавать объект любым другим путем, то будет ub. Поэтому, как заботливые нянки, помогает пользователям не изменять количество отверстий в ногах:
Использовать make_shared здесь не выйдет. Хоть мы и используем эту функцию внутри метода класса, это не позволяет ей получить доступ к приватным членам.
В таком случае мы просто вынуждены использовать явный конструктор shared_ptr и явный вызов new.
А как вы знаете, в этом случае будут 2 аллокации: для самого объекта и для контрольного блока. Если объект создается часто, то это может быть проблемой, если вы упарываетесь по перфу.
Hide your secrets. Stay cool.
#новичкам
std::make_shared - это сторонний код по отношению к классу, объект которого он пытается создать. Поэтому на принципиальную возможность создания объекта влияет спецификатор видимости конструктора.
Если конструктор публичный - проблем нет, просто вызываем std::make_shared.
Но things get trickier, если конструктор непубличный. Его тогда не может вызывать никакой чужой код.
Для определенности примем, что конструктор приватный. Это может делаться по разным причинам. Например у нас есть необходимость в создании копий std::shared_ptr, в котором находится исходный объект this. Тогда класс надо унаследовать от std::enable_shared_from_this и возвращать из фабрики std::shared_ptr. Если создавать объект любым другим путем, то будет ub. Поэтому, как заботливые нянки, помогает пользователям не изменять количество отверстий в ногах:
struct Class: public std::enable_shared_from_this<Class> {
static std::shared_ptr<Class> Create() {
// return std::make_shared<Class>(); // It will fail.
return std::shared_ptr<Class>(new Class);
}
private:
Class() {}
};Использовать make_shared здесь не выйдет. Хоть мы и используем эту функцию внутри метода класса, это не позволяет ей получить доступ к приватным членам.
В таком случае мы просто вынуждены использовать явный конструктор shared_ptr и явный вызов new.
А как вы знаете, в этом случае будут 2 аллокации: для самого объекта и для контрольного блока. Если объект создается часто, то это может быть проблемой, если вы упарываетесь по перфу.
Hide your secrets. Stay cool.
std::invoke
#опытным
Если вы хотите универсально работать с любыми коллбэками в вашем коде, то вам просто необходимо знать эту функцию и уметь с ней работать. Это единственный способ в С++ вызвать любую сущность, походящую на функцию.
Давайте посмотрим на пример:
Все просто: передаем шаблонный коллбэк и его аргументы и используем perfect forwarding для вызова коллбэка.
Есть ли проблема в этом коде? Задумайтесь на секунду.
И проблема есть!
Что если мы попробуем передать в process_and_call указатель на нестатический метод класса? Метод класса - это такая же функция, просто она принимает неявный параметр this. В С++ есть специальный синтаксис для вызова методов классов:
Давайте посмотрим на корректную реализацию process_and_call с использованием std::invoke:
Прекрасная функция, которая может сильно упростить работу с callback'ами.
Be universal. Stay cool.
#опытным
Если вы хотите универсально работать с любыми коллбэками в вашем коде, то вам просто необходимо знать эту функцию и уметь с ней работать. Это единственный способ в С++ вызвать любую сущность, походящую на функцию.
Давайте посмотрим на пример:
template <typename Callback, typename... Args>
void process_and_call(Callback&& callback, Args&&... args) {
// some processing
std::forward<Callback>(callback)(std::forward<Args>(args)...);
}
Все просто: передаем шаблонный коллбэк и его аргументы и используем perfect forwarding для вызова коллбэка.
Есть ли проблема в этом коде? Задумайтесь на секунду.
И проблема есть!
Что если мы попробуем передать в process_and_call указатель на нестатический метод класса? Метод класса - это такая же функция, просто она принимает неявный параметр this. В С++ есть специальный синтаксис для вызова методов классов:
class Data {
public:
void memberFunction(int value) {
std::cout << "Data::memberFunction called with value: " << value << "\n";
}
};
Data data;
// Создаем указатель на метод класса
auto methodPtr = &Data::memberFunction;
// Вызываем метод через указатель на объекте
(data.*methodPtr)(42);
Согласитесь, что этот синтаксис отличается от std::forward<Callback>(callback)(std::forward<Args>(args)...);.
Поэтому такая реализация process_and_call несовершенна. Как можно это исправить?
Использовать std::invoke. Это функция буквально создана, чтобы вызывать все, что только можно вызвать. Она конечно же написана на вариабельных шаблонах, чтобы вы могли передать туда все, что душе угодно:
template< class F, class... Args >
std::invoke_result_t<F, Args...> invoke( F&& f, Args&&... args );
Давайте посмотрим на корректную реализацию process_and_call с использованием std::invoke:
template <typename Callback, typename... Args>
void process_and_call(Callback&& callback, Args&&... args) {
// some processing
std::invoke(std::forward<Callback>(callback), std::forward<Args>(args)...);
}
struct S {
void foo(int i) {}
};
process_and_call(&S::foo, S{}, 42);Прекрасная функция, которая может сильно упростить работу с callback'ами.
Be universal. Stay cool.
👀124
Все вызываемые сущности в С++. Ч1
#новичкам
В С++ полно вещей, которые можно вызывать, как функции. Но, как мы убедились в прошлый раз, не только функции в плюсах можно "вызывать". Хотя указатели на поля класса - это единственное такое исключение, давайте перечислим все вызываемые сущности в С++. Поехали!
👉🏿 Функции. Старые-добрые и всем знакомые функции. Очевидный кандидат:
👉🏿 Указатели на функцию
Если вы хотите куда-то передать функцию, как коллбэк, то скорее всего стриггерите неявное приведение имени функции к указателю на функцию. Вот так например:
Внутрь invoke free_function передастся как именно как указатель.
Указатель на функцию можно и явно определить и вызвать с его помощью соответствующей функции:
Когда мы просто передаем имя функции в call_callback, это имя приводится к типу указателя на функцию:
👉🏿 Ссылки на функции
Да, и такие тоже есть:
Если передать в функцию в качестве параметра по ссылке, то тогда тоже получите ссылку на функцию:
👉🏿 Статические методы классов
Также мы можем явно определить указатель или ссылку на статический метод класса, так как это обычная функция:
👉🏿 Функциональные объекты
В обывательском понимании функторы - это объекты классов с определенным оператором(). Однако не все так просто. На самом деле с точки стандарта функциональный объект - это одно из 3-х: указатель на функцию, объект класса с определенным оператором() и объект класса с определенным оператором приведения к указателю на функцию. Их всех объединяет одинаковый синтаксис вызова, поэтому их засунули в одну категорию. Указатели на функции мы разобрали, рассмотрим 2 оставшихся.
👉🏿👉🏿 Объект класса с определенным вызова operator(). Это такие стандартные олдскульные(до С++11) функторы, которые использовались, например, для передачи в стандартные алгоритмы. Название, в общем, полностью описывает реализацию. Единственное, что хочется отметить - необходимость таких функторов заключена в их способности хранить стейт, иначе можно было использовать обычные функции.
Уже много получилось, все в один пост не влезет. Поэтому в следующих раз будет продолжение.
Have all the tools. Stay cool.
#новичкам
В С++ полно вещей, которые можно вызывать, как функции. Но, как мы убедились в прошлый раз, не только функции в плюсах можно "вызывать". Хотя указатели на поля класса - это единственное такое исключение, давайте перечислим все вызываемые сущности в С++. Поехали!
👉🏿 Функции. Старые-добрые и всем знакомые функции. Очевидный кандидат:
void free_function(int a) {
std::cout << "Free function: " << a << std::endl;
}
free_function(42);👉🏿 Указатели на функцию
Если вы хотите куда-то передать функцию, как коллбэк, то скорее всего стриггерите неявное приведение имени функции к указателю на функцию. Вот так например:
void free_function(int a) {
std::cout << "Free function: " << a << std::endl;
}
std::invoke(free_function, 42);Внутрь invoke free_function передастся как именно как указатель.
Указатель на функцию можно и явно определить и вызвать с его помощью соответствующей функции:
auto fun_ptr = &free_function;
fun_ptr(42);
(*fun_ptr)(42);
Когда мы просто передаем имя функции в call_callback, это имя приводится к типу указателя на функцию:
void free_function(int a) {
std::println("Free function: {}", a);
}
call_callback(free_function, 42);👉🏿 Ссылки на функции
Да, и такие тоже есть:
auto& fun_ref = free_function;
fun_ref(42);
(*fun_ref)(42); // интересно, что такой синтаксис разрешен
Если передать в функцию в качестве параметра по ссылке, то тогда тоже получите ссылку на функцию:
void check_name(auto& obj) {
// by passing free_function to check_name, obj become function reference
}
check_name(free_function);👉🏿 Статические методы классов
struct Handler {
static void static_method(int a) {
std::cout << "Static method: " << a << std::endl;
}
};
Handler::static_method(42);Также мы можем явно определить указатель или ссылку на статический метод класса, так как это обычная функция:
auto static_method_ptr = &Handler::static_method;
std::invoke(static_method_ptr, 42);
👉🏿 Функциональные объекты
В обывательском понимании функторы - это объекты классов с определенным оператором(). Однако не все так просто. На самом деле с точки стандарта функциональный объект - это одно из 3-х: указатель на функцию, объект класса с определенным оператором() и объект класса с определенным оператором приведения к указателю на функцию. Их всех объединяет одинаковый синтаксис вызова, поэтому их засунули в одну категорию. Указатели на функции мы разобрали, рассмотрим 2 оставшихся.
👉🏿👉🏿 Объект класса с определенным вызова operator(). Это такие стандартные олдскульные(до С++11) функторы, которые использовались, например, для передачи в стандартные алгоритмы. Название, в общем, полностью описывает реализацию. Единственное, что хочется отметить - необходимость таких функторов заключена в их способности хранить стейт, иначе можно было использовать обычные функции.
class SequenceGenerator {
int current;
int step;
public:
SequenceGenerator(int start = 0, int step_size = 1)
: current(start), step(step_size) {}
// Оператор вызова без аргументов - возвращает следующее число
int operator()() {
int val = current;
current += step;
return val;
}
};
SequenceGenerator gen;
std::vector<int> numbers(5);
std::generate(numbers.begin(), numbers.end(), gen); // заполняем вектор с помощью функтораУже много получилось, все в один пост не влезет. Поэтому в следующих раз будет продолжение.
Have all the tools. Stay cool.
👀92
Например, REST задаёт стандарт взаимодействия по HTTP, GraphQL — гибкую выборку данных, а gRPC — эффективную бинарную коммуникацию.
На схеме — основные области: основы (контракты, HTTP, stateless), паттерны (REST, RPC, WebSockets), безопасность (OAuth2, JWT, TLS), а также дизайн и эволюция API.
Сохрани, чтобы не потерять!
Please open Telegram to view this post
VIEW IN TELEGRAM
👀155
std::thread
std::thread является частью стандартной библиотеки C++ и предоставляет возможность создания и управления потоками выполнения. Он позволяет запускать функции в отдельных потоках, обеспечивая параллельное выполнение кода.
Обратите внимание, что после создания потока std::thread, вы должны вызвать join() или detach() для корректной обработки завершения потока.
В приведенном примере мы использовали join(), чтобы основной поток дождался завершения потока t1. Если вы вызываете join() после завершения потока, программа может выдать исключение или вызвать неопределенное поведение.
std::thread является частью стандартной библиотеки C++ и предоставляет возможность создания и управления потоками выполнения. Он позволяет запускать функции в отдельных потоках, обеспечивая параллельное выполнение кода.
Обратите внимание, что после создания потока std::thread, вы должны вызвать join() или detach() для корректной обработки завершения потока.
В приведенном примере мы использовали join(), чтобы основной поток дождался завершения потока t1. Если вы вызываете join() после завершения потока, программа может выдать исключение или вызвать неопределенное поведение.
👀102
Сколько раз будет выполняться этот цикл?
Еще один вопрос с подвохом с IT-собеседований. Если бы вы сказали 300, а i был объявлен как int, вы были бы правы. Но поскольку i объявлен как unsigned char, правильный ответ – зацикливание (бесконечный цикл).
Объясняем. Выражение 2 * half_limit будет повышаться до int (на основе правил преобразования C++) и заимеет значение 300. Но так как i – это unsigned char, он пересматривается по 8-битному значению, которое после достижения 255 будет переполняться, поэтому вернется к 0, и цикл будет продолжаться вечно.
Еще один вопрос с подвохом с IT-собеседований. Если бы вы сказали 300, а i был объявлен как int, вы были бы правы. Но поскольку i объявлен как unsigned char, правильный ответ – зацикливание (бесконечный цикл).
Объясняем. Выражение 2 * half_limit будет повышаться до int (на основе правил преобразования C++) и заимеет значение 300. Но так как i – это unsigned char, он пересматривается по 8-битному значению, которое после достижения 255 будет переполняться, поэтому вернется к 0, и цикл будет продолжаться вечно.
👀97
Узнаем время работы программы на С++
Для того, чтобы найти время работы программы, нужно воспользоваться функцией clock(). Прототип функции clock() находится в заголовочном файле <ctime>, который нужно подключить.
Функция clock() возвращает значение времени в миллисекундах (1с = 1000млс). Причём отсчёт времени начинается с момента запуска программы.
Для того, чтобы найти время работы программы, нужно воспользоваться функцией clock(). Прототип функции clock() находится в заголовочном файле <ctime>, который нужно подключить.
Функция clock() возвращает значение времени в миллисекундах (1с = 1000млс). Причём отсчёт времени начинается с момента запуска программы.
👀93
Отличия volatile от std::atomic
#опытным
Кратко пробежимся по особенностям volatile переменных и атомиков, чтобы было side-by-side сравнение.
volatile переменные:
- Компилятору запрещается выкидывать операции над volatile переменными. Грубо говоря, компилятору запрещается "запоминать" значение таких переменных и он обязан их каждый раз читать из памяти.
- Это происходит, потому что операции над volatile переменными становятся видимыми сайд-эффектами. Такие операции в программе влияют на другие потоки и внешние системы. Компилятор в принципе может крутить ваш код на всех продолговатых инструментах, которых он хочет. Главное, чтобы видимое внешнему миру исполнение осталось прежним. Поэтому просто выкинуть из кода использование volatile переменной он не может.
- Физически это значит, что volatile переменные запрещается кэшировать в регистрах и их всегда нужно честно читать из памяти.
- Запрещается реордеринг операций volatile переменных с другими операциями с видимыми спец-эффектами, расположенных выше и ниже по коду.
- Любой другой реордеринг разрешен.
- Операции над такими переменными не являются гарантированно атомарными в том плане, что есть возможность увидеть их промежуточное состояние. В целом, ничто не мешает пометить volatile объект std::unordered_map и конечно же операции над мапой не будут атомарными. Они могут быть атомарными на определенных архитектурах для тривиальных типов с правильным выравниванием памяти, но это никто не гарантирует.
- Запись и чтение volatile переменных не связаны между собой отношением synchronized-with, поэтому на их основе нельзя выстроить межпотоковое отношение happens-before. А это значит, что по стандарту С++ доступ к volatile переменной из разных потоков - это гонка данных и ub.
std::atomic:
- Операции над атомиками - это также видимые спецэффекты, только ситуация немного другая. Запись в атомике и других переменных, синхронизируемые атомиком, становятся видимыми сайд эффектами только для потоков, которые прочитают последнюю запись в атомик.
- По сути, если компилятор докажет, что поток будет всегда читать одно и то же значение атомика, то он может его закэшировать. Если ваш код только читает из memory mapped io, то компилятор теоритически может выкинуть чтение и заменить заранее вычисленным значением. Поэтому атомик нельзя использовать, как замену volatile.
- Вы можете контролировать, какой барьер памяти хотите поставить атомарной операцией, и соответственно можете контролировать реордеринг. Самый сильный порядок предполагает, полный барьер памяти - никакие инструкции до атомика не могут быть переупорядочены ниже по коду и наоборот. Самый слабый порядок не предполагает никаких барьеров.
- Операции над атомарными переменными гарантировано являются атомарными в том смысле, что невозможно увидеть их промежуточное состояние. Это могут быть реально lock-free операции или в кишках операций могут использоваться мьютексы, но все это дает эффект атомарности.
- Запись и чтение атомиков связаны между собой отношением synchronized-with, поэтому на их основе можно построить межпотоковое отношение happens-before. Это значит, что по стандарту операции непосредственно над атомарными переменными не могут приводить к гонке данных.
- При использовании правильных порядков и барьеров памяти вы можете добиться того, что с помощью атомарных переменных вы сможете соединять операции над неатомиками отношением happens-before. Это значит, что атомики можно использовать для корректной синхронизации неатомарных переменных и предотвращения гонки данных над ними.
Про атомики можно говорить еще долго, но эти разговоры уже будут сильно оторваны от volitile. В этом посте хотелось бы сравнить их, чтобы можно быть проследить отличия по одним и тем же характеристикам.
Compare things. Stay cool.
#опытным
Кратко пробежимся по особенностям volatile переменных и атомиков, чтобы было side-by-side сравнение.
volatile переменные:
- Компилятору запрещается выкидывать операции над volatile переменными. Грубо говоря, компилятору запрещается "запоминать" значение таких переменных и он обязан их каждый раз читать из памяти.
- Это происходит, потому что операции над volatile переменными становятся видимыми сайд-эффектами. Такие операции в программе влияют на другие потоки и внешние системы. Компилятор в принципе может крутить ваш код на всех продолговатых инструментах, которых он хочет. Главное, чтобы видимое внешнему миру исполнение осталось прежним. Поэтому просто выкинуть из кода использование volatile переменной он не может.
- Физически это значит, что volatile переменные запрещается кэшировать в регистрах и их всегда нужно честно читать из памяти.
- Запрещается реордеринг операций volatile переменных с другими операциями с видимыми спец-эффектами, расположенных выше и ниже по коду.
- Любой другой реордеринг разрешен.
- Операции над такими переменными не являются гарантированно атомарными в том плане, что есть возможность увидеть их промежуточное состояние. В целом, ничто не мешает пометить volatile объект std::unordered_map и конечно же операции над мапой не будут атомарными. Они могут быть атомарными на определенных архитектурах для тривиальных типов с правильным выравниванием памяти, но это никто не гарантирует.
- Запись и чтение volatile переменных не связаны между собой отношением synchronized-with, поэтому на их основе нельзя выстроить межпотоковое отношение happens-before. А это значит, что по стандарту С++ доступ к volatile переменной из разных потоков - это гонка данных и ub.
std::atomic:
- Операции над атомиками - это также видимые спецэффекты, только ситуация немного другая. Запись в атомике и других переменных, синхронизируемые атомиком, становятся видимыми сайд эффектами только для потоков, которые прочитают последнюю запись в атомик.
- По сути, если компилятор докажет, что поток будет всегда читать одно и то же значение атомика, то он может его закэшировать. Если ваш код только читает из memory mapped io, то компилятор теоритически может выкинуть чтение и заменить заранее вычисленным значением. Поэтому атомик нельзя использовать, как замену volatile.
- Вы можете контролировать, какой барьер памяти хотите поставить атомарной операцией, и соответственно можете контролировать реордеринг. Самый сильный порядок предполагает, полный барьер памяти - никакие инструкции до атомика не могут быть переупорядочены ниже по коду и наоборот. Самый слабый порядок не предполагает никаких барьеров.
- Операции над атомарными переменными гарантировано являются атомарными в том смысле, что невозможно увидеть их промежуточное состояние. Это могут быть реально lock-free операции или в кишках операций могут использоваться мьютексы, но все это дает эффект атомарности.
- Запись и чтение атомиков связаны между собой отношением synchronized-with, поэтому на их основе можно построить межпотоковое отношение happens-before. Это значит, что по стандарту операции непосредственно над атомарными переменными не могут приводить к гонке данных.
- При использовании правильных порядков и барьеров памяти вы можете добиться того, что с помощью атомарных переменных вы сможете соединять операции над неатомиками отношением happens-before. Это значит, что атомики можно использовать для корректной синхронизации неатомарных переменных и предотвращения гонки данных над ними.
Про атомики можно говорить еще долго, но эти разговоры уже будут сильно оторваны от volitile. В этом посте хотелось бы сравнить их, чтобы можно быть проследить отличия по одним и тем же характеристикам.
Compare things. Stay cool.
👀172