Как стать автором
Обновить
7
0
Максим Шемендюк @old2ev

Системное программирование

Отправить сообщение

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

Например, помимо указанного мной в комментарии выше варианта использования трейтса на проверку CRTP-наследника is_crtp_base_of_v, можно проверить, является ли наследник одного базового CRTP-класса наследником какого-либо другого базового CRTP-класса и в зависимости от этого включить/отключить какой-нибудь метод (через requires) или же поменять логику существующего (через if constexpr). Иными словами "построить мост" в логике взаимодействия между двумя базовыми CRTP-классами одного CRTP-наследника.

Так же можно создавать базовые классы-метки, без полей и методов, то есть они не будут производить никакого мутирующего воздействия на наследников, но базовые CRTP-классы смогут отловить сам факт, того, что их наследник наследуется от класса-метки через std::is_base_of_v и за счёт этого могут так же менять логику работы. Таким образом можно писать "настраиваемое наследование".

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

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

Для ограничения шаблонов взаимодействия CRTP-наследников обычно использую в requires концепт is_crtp_base_of_v из объявления ниже:

#include <type_traits> 
#include <concepts>

 template <class T> 
 struct remove_cvref : std::remove_cv<std::remove_reference_t<T>> {}; 
  
 template <class T> 
 using remove_cvref_t = typename remove_cvref<T>::type; 
  
 template <class T, class U> 
 struct is_base_of : std::is_base_of<remove_cvref_t<T>, remove_cvref_t<U>> {}; 
  
 template <class T, class U> 
 concept is_base_of_v = is_base_of<T, U>::value; 
  
 template <template<typename T> class CRTP_Base, class CRTP_Derived> 
 struct is_crtp_base_of : std::is_base_of<remove_cvref_t<CRTP_Base<remove_cvref_t<CRTP_Derived>>>, remove_cvref_t<CRTP_Derived>> {}; 
  
 template <template<typename T> class CRTP_Base, class CRTP_Derived> 
 concept is_crtp_base_of_v = is_crtp_base_of<CRTP_Base, CRTP_Derived>::value;

Допустим, нам нужно чтобы CRTP-наследники могли сравниваться между собой:

// В теле CRTP-интерфейса Base
// Где: Derived - наследник передаваемый через шаблон

int getValue() const {
  return static_cast<Derived*>(this)->getValueImpl();
}

template<typename T>
requires is_crtp_base_of_v<Base, T>
bool operator==(T&& other) const { return getValue() == other.getValue(); }

// Предполагается что метод Derived::getValueImpl приватный/защищённый,
// чтобы лишние "потраха" не торчали наружу, и при этом CRTP-интерфейс Base дружественный
// для Derived, в ином случае (если метод public) в шаблоне оператора можно напрямую
// цеплять getValueImpl (ну или другой метод) без лишних обёрток, но если не будет указанного выше
// метода Base<Derived>::getValue то нет и гарантий, что наследник интерфейса обязан содержать
// метод getValueImpl, в следствии чего об ошибке мы узнаем только
// при инстанциировании оператора сравнения, так что лучше использовать
// подобную обёртку как контракт того что функция getValueImpl есть в наследнике, и если обёртка нигде не нужна,
// то просто бъявить её как private с модификатором inline в CRTP-интерфейсе.

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

По личному опыту: На ранних этапах изучения C++ делал шутан с видом сверху на Qt5 + QML. На C++ Qt создавал элементы через QQuickPaintedItem, а через QML размещал на "сцене". Проект торчал в топе таск менеджера по RAM из-за огромных утечек и люто лагал, поскольку главный цикл гонял все обработки событий + коллизию объектов в одном потоке. Понял я это только спустя время уже когда клепал другой пет-проект))

Совет: почитай про пул потоков (из теории можно почитать про системы массового обслуживания(СМО)), советую его использовать для обработки эвентов в игре ассинхронно. Так же чекай проект Valgrind'ом на утечки памяти. Подучи киллерфичу Qt - сигналы/слоты (не помню, но помоему они по дефолту юзают тред пул). Если хочешь сделать игру сетевой в Qt модуль QNetwork вполне неплохой.

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

Обязательно выучите базовые вещи в программировании: асимптотический анализ и структуры данных - данные темы сильно помогут в оптимизации кода Вашей программы. Плюсом будет хорошее понимание организация памяти процесса в операционных системах, на первых порах точно нужно знать: стэк, куча; В дальнейшем будет полезно (почитайте на досуге): виртуальная память, типы страниц(.text, .data, .bss, .noinit), маппинг страниц памяти / маппинг файлов в виртуальную память. Так же лучше выучить работу с процессами и методы межпроцессного взаимодействия (IPC). Выучите системы сборки: GNU Make, CMake. Что такое статические и динамические библиотеки и как их линковать в Ваш проект. Обязательно выучите средства отладки: GDB и Valgrind - они сильно облегчат жизнь

По многопоточности можно пробежаться в порядке усложнения темы. Доки по стандартной библиотеки C++ для многопоточности - https://en.cppreference.com/w/cpp/thread

  • Потоки: раздел Threads

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

  • Проблема состояния гонки (race condition) и методы её решения:

    • Критические секции: раздел Mutual exclusion

    • Условные переменные: раздел Condition variables

    • Атомики: раздел Atomic operations

  • Ассинхронные операции: раздел Futures

  • Так же будет полезно знать про корутины (удобно использовать в многопоточных программах): https://en.cppreference.com/w/cpp/language/coroutines

Обратите внимание на коллекцию библиотек C++ Boost - там есть много удобных вещей. Так же не плохо знать API операционных систем: WinAPI и POSIX API; хоть это и уровень Си, но применим в C++ (лучше изучать после Boost, чтобы не строить велосипедов).

Совет по набиванию руки: придумайте себе пет-проект(желатнльно посложней), не бойтесь строить велосипеды в учебных целях, чаще работайте с коммандной строкой.

Благодарствую за подсказку. Надо бы опробовать.

Предположим у нас есть Posix-совместимый код на C или C++ (такого кода очень много, сомневаться не стоит). И у нас появляется задача - сделать код полностью кроссплатформеным. Большая часть дела и так сделана, данный код пашет абсолютно где угодно - Linux, FreeBSD, Mac OS, Android и IOS ибо Posix-совместимо (по большей части). Но Windows по правилам не играет - Windows хочет быть долбанутым пациентом дурки. И из-за этого у нас есть два стула:

  • оборачивать весь код макросами для совместимости и писать под Windows используя наиболее близкое к Posix окружение(Cygwin/MinGW и т.п.);

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

Казалось бы "первый стул удобней", но то тут то там возникают весьма интересные проблемы из разряда отсутствия поддержки именно UTF-8 в функциях работы с файлами начиная с обычных Сишного fopen, закначивая всем std::filesystem. Posix окружения нет, а его очень сильно не хватает поэтому и существет целый набор эти костылей к Windows которые раз за разом шлефуются. По итогу, если ты используешь что-то что плохо работает в MinGW или Cygwin то добро пожаловать в MSVC - пиши в два раза больше кода.

Мне лично, да и большенству больше чем уверен плевать, что там за тёрки у Microsoft с их CRL и прочую их закрытую муть, если эта муть работает нормально. Программистам нужна переносимость на уровне исходного кода которая была в C и C++ изначально и без лишнего пердолинга с макросами для совместимости. Врятли кому-то хочется тратить лишние человекочасы на перенос кода на одну "не такую как все" платформу.

Исчезнет возможность подхватывать this класса где создаётся лямбда через лямбда-захват:

class SomeClass {
	int a = 255;
public:
  auto getLambda();
};

auto SomeClass::getLambda() {
	return [this](int a) {return a * this->a;}
}

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

ByteArray a(15), b(10), c(12);
b = a; // Скопировали a в b
// Вызванный деструктор b очистит выделенную память в указателе b.byte_array
c = std::move(a); // Переместили a в c
// Вызванный деструктор c очистит выделенную память в указателе c.byte_array

Да, можно было бы заменить определение оператора присваивания на:

ByteArray& ByteArray::operator=(ByteArray other) {
	if(byte_array) delete byte_array;
  _length = other._length;
  byte_array = other.byte_array;
  other.byte_array = nullptr;
  return *this;
}

Но если разницы особой нет, особого смысла это не имеет. Иногда случается, что в классе множество разных поинтеров, память из которых нужно вычищать. Да, можно вынести код очистки в отдельный приватный метод например в void clear() и за тем уже вызывать их в деструкторах и операторах присвоения. Можно конечно ещё использовать std::unique_ptr.

Что касательно использования placement-new - то он нужен лишь за тем чтобы вызвать конструктор у куска не инициализированной памяти, тоже вполне стандартная функция не взывающая никаких аллокаций/реаллокаций памяти кроме тех что прописаны в конструктор непосредственно.

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

uint8_t* a = (uint8_t*)malloc(sizeof(uint32_t) + sizeof(long double));
new(a) uint32_t(255);
new(a+sizeof(uint32_t)) long double(15.256);

Из-за наличия инициализации в условных конструкциях:

if(uint64_t reuired_length = f_length + s_length; reuired_length < ciphertext.length())
    ciphertext.resize(f_length + s_length);
  else if(reuired_length > ciphertext.length())
    throw std::runtime_error("Predicted ciphertext size lower then actual!");

Которая появилась в C++ 17-й версии, есть требования к версии C++ при компиляции кода, из-за этого и появилась версия C++ в названии, поскольку в некоторых версиях MinGW попрежнему явно приходится задавать версию при компиляции, если требуется поддержка версии C++ выше 14-й, я решил на это указать.

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

// Присвоить к текущему экземпляру класса AES_t
// экземпляр AES_t сконструированный поумолчанию
void AES_t::clear() { *this = AES_t(); }
// Использование логики определённой в RAII для копирования объекта
// при присвоении, по сути вызвать деструктор и конструктор через placement-new от
// скопированного/перемещённого из вне функции экземпляра данного
// класса
inline ByteArray& operator=(ByteArray other) {
    this->~ByteArray();     
    return *new(this) ByteArray(std::move(other));
}

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

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

Вот пример аналога рекурсивной лямбда-функции без std::function:

struct {
  void operator()() {
    return (*this)();
  }
} recusive_lambda;
recursive_lambda();

Да, это анонимная структура, но после компиляции останется лишь вызов перегрузки оператора, результат будет идентичен объявлению и вызову обычной лямбда-функции, но теперь у нас есть указатель this... но вот только теперь мы жертвуем лямбда-захватом, что в принципе сложно эмитировать при некоторых обстаятельствах. Кстати этот вариант лямбды можно засунуть в std::function , а следовательно и передовать в аргументы других функций, главное чтобы сигнатура перегрузки оператора совпадала с сигнатурой объявленной в std::function

Вместо starship можно просто установить fish shell с oh-my-fish и любой темой (никаких конфигов, всё просто и быстро):

  • Debian based дистрибутивы(с пакетным менеджером apt):

    $ sudo apt-get install fish
    $ sudo chsh -s /bin/fish

    Если нет в репозитории, повторить с предварительным выполнением:

    $ sudo apt-add-repository ppa:fish-shell/release-2
    $ sudo apt-get update
  • Arch based дистрибутивы(с пакетным менеджером pacman):

    $ sudo pacman -S fish
  • Red Hat based дистрибутивы(с пакетным менеджером dnf):

    $ dnf install fish # or yum
  • с пакетным менеджером yum:

    $ yum install fish
  • Установка как интерпретатор по умолчанию:

    $ chsh -s `which fish`

    В графических терминалах ставится в основном через настройки профиля в поле "Команда" тебуется прописать /bin/fish или usr/bin/fish

  • качаем исходнинки oh-my-fish и ставим его

$ git clone https://github.com/oh-my-fish/oh-my-fish
$ cd oh-my-fish
$ bin/install --offline 
  • Ставим тему, например chain (так как она работает в любых терминалах в том числе и termux на android'е)

$ omf install chain
  • Profit! Юзаем удобный терминал с автодополнением команд

Ссылки:

Fish: https://fishshell.com/

oh-my-fish: https://github.com/oh-my-fish/oh-my-fish

Список тем для oh-my-fish: https://github.com/oh-my-fish/oh-my-fish/blob/master/docs/Themes.md

Или просто установить fish:


  • Debian based дистрибутивы(с пакетным менеджером apt):


    $ sudo apt-get install fish
    $ sudo chsh -s /bin/fish

    Если нет в репозитории, повторить с предварительным выполнением:


    $ sudo apt-add-repository ppa:fish-shell/release-2
    $ sudo apt-get update

  • Arch based дистрибутивы(с пакетным менеджером pacman):


    $ sudo pacman -S fish

  • Fedora based дистрибутивы(с пакетным менеджером dnf):


    $ dnf install fish

  • Установка как интерпретатор по умолчанию:


    $ chsh -s `which fish`

    В графических терминалах ставится в основном через настройки профиля в поле "Команда" тебуется прописать /bin/fish или usr/bin/fish



Без особых красот как zsh, но есть подсветка синтаксиса и автодополнение команд и всё это по default'у.


Ссылка на официальный сайт с документацией и всем, всем, всем...

Небольшая поправочка:


while(!(size = client.loadData()));

Требуется запомнить размер буфера в size, иначе можно было и вовсе написать:


while(!client.loadData());

Информация

В рейтинге
Не участвует
Откуда
Москва, Москва и Московская обл., Россия
Дата рождения
Зарегистрирован
Активность

Специализация

Software Developer, Backend Developer
От 250 000 ₽
C++
Qt
Git
Node.js
JavaScript
CSS
HTML
Linux
Posix
GNU Make