ar
Feedback
Грокаем C++

Грокаем C++

الذهاب إلى القناة على Telegram

Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов. По всем вопросам (+ реклама) @ninjatelegramm Менеджер: @Spiral_Yuri Реклама: https://telega.in/c/grokaemcpp Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat

إظهار المزيد
9 388
المشتركون
+224 ساعات
+147 أيام
+1330 أيام
أرشيف المشاركات
​​Swap idiom Рассуждения в комментах под предыдущим постом навели меня на мысли рассказать о swap idiom. Дело в том, что, когда у вас есть рабочие деструктор, конструктор копирования и перемещения, вы можете соединять методы, которые должны принимать константную lvalue ссылку и rvalue ссылку, в один метод, который принимает параметр по значению. То есть можно вместо 2-х методов сеттеров можно написать 1:
template <class T>
struct TemplateClass {
  void SetValue(T value) {
    value_ = std::move(value);
  }
private:
  T value_;
};
Этой же концепцией вдохновлено появление swap идиомы. На самом деле я немного вру, но с появлением мув-семантики идиома приобрела эти черты. Суть в чем. Есть у вас класс, который мэнэджит какие-то ресурсы. Например самописный класс массива:
class SimpleArray
{
public:
    SimpleArray(std::size_t size = 0)
        : mSize(size),
          mArray(mSize ? new intmSize : nullptr) {}

    SimpleArray(const SimpleArray& other)
        : mSize(other.mSize),
          mArray(mSize ? new int[mSize] : nullptr) {
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }
  SimpleArray(simple_array&& other) noexcept
        : mSize(other.mSize),
          mArray(other.mArray) {}

    ~SimpleArray()
    {
        delete [] mArray;
    }

private:
    std::size_t mSize;
    int* mArray;
};
Все хорошо, но для выполнения правила 5 нам нужно определить еще и 2 оператора присваивания: перемещающий и копирующий. Обычно в них в начале очищают существующий объект и потом записываются новые данные. Покажу на примере конструктора копирования:
SimpleArray& operator=(const SimpleArray& other) {
    if (this != &other) {
        delete [] mArray;
        mArray = nullptr;
        mSize = 0;

        mSize = other.mSize;
        mArray = mSize ? new int[mSize] : nullptr;
        std::copy(other.mArray, other.mArray + mSize, mArray);
    }
    return *this;
}
В такой реализации есть 3 проблемы: ❗️ Нам просто необходима проверка на самоприсвоение, чтобы в объекте остались те же данные. Но это настолько редкий кейс, что каждый раз при присвоении тратить время на проверку не очень хочется. А хочется операторы без этой проверки. ❗️ У нас есть только базовая гарантия исключений. Если из new бросится исключение, то состояние изменяемого объекта хоть и останется согласованным, но оно все равно изменится. А операция не завершится до конца. Хотелось бы строгой гарантии безопасности исключений. ❗️ Мы повторяем код. Помимо проверки самоприсваивания и очищения ресурсов тупо повторяется код копирующего конструктора. Хочется этого не делать. Чтобы решить эти проблемы, мы можем сделать интересную штуку - принимать параметр оператора присваивания на обычное значение. Тогда на входе оператора у нас уже будет готовый скопированный(или перемещенный объект) и нам нужно будет лишь поменять содержимое этих двух объектов местами. И нам не нужно беспокоиться о том, что останется в параметре функции - он все равно удалится после выхода из нее. Теперь оператор будет выглядеть так:
SimpleArray& operator=(SimpleArray other) noexcept {
  swap(*this, other);
  return *this;
}
Как же красиво! Нам осталось только реализовать функцию swap. Она может быть и методом класса, но почему бы еще не иметь просто функцию, которая свапает контент. Поэтому покажу реализацию дружественной функции.
friend void swap(SimpleArray& first, SimpleArray& second) noexcept {
  using std::swap;
  swap(first.mSize, second.mSize);
  swap(first.mArray, second.mArray);
}
Выглядит кратко, читаемо, да еще и исключений нет(об этом даже явно в коде можно сказать)! Ляпота. ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ Stay laconic. Stay cool. #patter #cppcore #cpp11

🔥 Программистика находка для каждого питониста Проведём за ручку от самых простых проектов до сложных. Расскажем самые трудн
🔥 Программистика находка для каждого питониста Проведём за ручку от самых простых проектов до сложных. Расскажем самые трудные концепции простым языком. Научим не боятся сложных задач и наслаждаться своей работой 👍Подписывайся и развивайся

​​Шаблонный сеттер #опытным Увидел на ревью интересный кейс. Мы о нем уже говорили, что не сильно акцентировали внимание. Сегодня больше времени уделим одному интересному явлению. Если у вас есть какой-то шаблонный класс, который хранит тип Т, и в этом классе есть сеттер на этот тип, то по давней привычке(еще с 98 стандарта) его можно написать вот так:
template <class T>
struct TemplateClass {
  void SetValue(const T& value) {
    value_ = value;
  }
private:
  T value_;
};
Привычка - дело хорошее и экономит ресурс мозга на выполнение действий. Не так много когнитивного внимания нужно тратить на деятельность. Но иногда привычки ограничивают нас. Мы-то уже в modern C++ эре. И в данном случае как раз такой кейс. Что будет, если мы захотим передать в этот метод временный объект? Например так:
struct ShowConstruct {
  ShowConstruct() = default;
  ShowConstruct(int value) : field{value} {
    std::cout << "Param construct " << field << std::endl;}
  ShowConstruct& operator=(const ShowConstruct& other) {
    field = other.field;
    std::cout << "Copy assign " << field << std::endl;
    return *this;}
  ShowConstruct& operator=(ShowConstruct&& other) {
    field = other.field;
    std::cout << "Move assign " << field << std::endl;
    return *this;}
  int field = 0;
};

TemplateClass<ShowConstruct> obj;
obj.SetValue(ShowConstruct{5});
На экран выведется:
Param construct 5
Copy assign 5
Это значит, что даже если мы передаем в такой сеттер временный объект, у которого можно забрать его ресурсы и сэкономить на копировании, мы все равно не получаем этих бенефитов. Потому что в сеттере value уже относится к категории lvalue. А при присваивании объекта от lvalue будет вызываться копирующий оператор присваивания. А нам бы хотелось, чтобы вызывался перемещающий оператор. Как этого достичь? Использовать универсальную ссылку. Скажет прошаренный читатель. Для шаблонного кода мы можем пометить параметр метода двумя амперсандами и дальше внутри передавать его во все места через std::forward. Таким образом, если нам на вход пришел именованный объект, то std::forward скастует его к lvalue ссылке, а если временный, то к rvalue ссылке. И это поможет нам в нужных случая вызывать правильный оператор присваивания. И std::forward и universal reference доступны с 11-го стандарта вместе с введением мув-семантики.
template <class T>
struct TemplateClass {
  void SetValue(T&& value) {
    value_ = std::forward<T>(value);
  }
private:
  T value_;
};

TemplateClass<ShowConstruct> obj;
obj.SetValue(ShowConstruct{5});
Теперь мы получаем нужный вывод:
Param construct 5
Move assign 5
Однако этот прошаренный читатель оказался не таким уж и прошаренным! Такая штука не сработает для шаблонных параметров класса!
in class template argument deduction, template parameter 
of a class template is never a forwarding reference
Универсальная ссылка(она же forwarding reference) появляется только, когда тип параметра функции Т&& и Т - шаблонной параметр самой функции. В нашем случае нет никакого вывода - тип Т известен из класса. Поэтому и никакой универсальной ссылки не появляется. Мы просто определили метод, который принимает rvalue ссылку. При попытке передать туда lvalue будет ошибка:
TemplateClass<ShowConstruct> obj;
ShowConstruct lvalue{7};
obj.SetValue(lvalue);

//ERROR: rvalue reference to type 'ShowConstruct' 
// cannot bind to lvalue of type 'ShowConstruct'
Какой выход? Просто рядышком с сеттером для константной lvalue ссылки написать сеттер для rvalue ссылки.
template <class T>
struct TemplateClass {
  void SetValue(const T& value) {
    value_ = value;
  }
  void SetValue(T&& value) {
    value_ = std::move(value);
  }
private:
  T value_;
};

TemplateClass<ShowConstruct> obj;
obj.SetValue(ShowConstruct{5});
ShowConstruct lvalue{7};
obj.SetValue(lvalue);
Тогда все нормально скомпилируется и в нужных места будут вызваны нужные операторы. Stay universal. Stay cool. #cpp11

​​Правила константности #новичкам Константность - важное свойство сущности в коде. Оно не только позволяет обезопасить объекты от изменения, но еще и говорит программисту о гарантиях, которые дает та или иная функция. Допустим, принимая параметр по константной ссылке, функция говорит программисту: "расслабься, ничего я не сделаю с твоим объектом". Это повышает читаемость кода. В С++ много чего можно сделать константным. Объекты, указатели, ссылки, параметры функции, методы класса и тд. И зачастую новичкам сложно разобраться в правилах присваивания константности. Сегодня разберемся в этом. Не будем долго задерживаться над константными методами. Константные объекты могут вызвать только константные методы. Все. Синтаксис такой:
void Class::Method(Type1 param1, Type2 param2) const {}
Теперь и константные, и неконстантные объекты могут вызывать метод Method. Дальше все так или иначе сводится к правилам в объявлении переменных. Что в качестве поля класса, параметра функции, что объявлении обычной переменной - разницы нет. Правила одни. Поехали. Константный объект можно объявить двумя способами:
const T obj;
// Либо
T const obj;
Эти записи абсолютно эквивалентны! Это очень важно запомнить, потому что при разговоре о ссылках и указателях это играет большую роль. Собственно также есть 2 нотации объявления массивов констант:
const T arr[5];
// либо
T const arr[5];
И 2 нотации определения ссылок:
const T& ref;
// либо
T const & ref;
Помните, что при создании ссылки в скоупе функции вы обязаны ее инициализировать. При объявлении поля класса этого делать не обязательно, потому что вы не создаете объект прямо сейчас. Но вы обязаны инициализировать ссылку до входа в конструктор либо через список инициализации конструктора, либо через default member initializer, так как базового поля класса иницализируются до входа в конструктор. При объявлении параметра функции тоже не нужно сразу инициализировать ссылку, потому что функция принимает уже существующую и инициализированую ссылку на вход. Обычно при таком объявлении ссылку называют константной. Это не совсем верно. Ссылка при любых обстоятельствах сама по себе является константной. Как только вы забиндили ссылку на объект, она всегда будет смотреть на этот объект и изменять его. Более подробно про особенности ссылок посмотреть тут. При новом присваивании ссылки вызовется оператор присваивания и изменится существующий объект.
struct Type {
  Type& operator=(const Type& other) { 
    std::cout << "copy assign" << std::endl; 
    return *this;
  }
};

Type a{};
Type& b = a;
b = Type{};
// OUTPUT:
// copy assign
Когда говорят "константная ссылка" имеют ввиду ссылку на константу. И при любом виде объявления const T& ref или T const & ref она также будет ссылкой на константу. Теперь указатели. Наверное самое сложное из всего перечисленного. Указатели, в отличии от ссылок, сами могут быть константными, да еще и указывать на константные объекты. А еще могут быть многоуровненые указатели. В общем сложно. Но есть правило: при объявлении указателя каждое появление ключевого слова const относится к тому уровню вложенности, который находится слева от этого слова. Вы просто читаете объявление справа налево и получаете правильное понимание объявления. Примеры:
// Читаем справа налево
int * const ptr; // ptr - это константный указатель на инт
int * * const ptr; // ptr - это константный указатель на указатель на инт
int * const * const ptr; // ptr - это константный указатель на константный указатель на инт

// Самый низкий уровень, который относится к самому объекту, 
// можно писать двумя способами, о которых мы говорили выше

int const * const * * const ptr; // ptr - это константный указатель на указатель 
                                 // на константный указатель на интовую константу
const int * const * * const ptr; // Тоже самое
ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ Rely on fixed thing in your life. Stay cool. #cppcore ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ

​​Опасности использования директив препроцессора Вчерашний способ выбора ветки кода имеет несколько недостатков: ⛔️ Препроцессор работает с буквами/текстом программы, но не понимает программных сущностей. Это значит, что типабезопасность уходит из окна, и открывается простор для разного рода трудноотловимых багов. ⛔️ При компиляции проверяется только та ветка, которая попадет в итоговый код. Если вы не протестировали свой код для разных значений внешних параметров, а такое бывает например когда пока что есть только одно значение, а другое будет только в будущем, а в будущем тесты естественно никто дописывать не будет. Тогда в этом будущем, когда значение параметра неожиданно изменится, в лучшем случае упадет сборка, а в худшем - это будет работать на проде и чудить одному Богу известные чудеса. ⛔️ Вы ограничены возможностями препроцессора. Это значит, что вы не можете использовать в условии compile-time вычисления (аля результат работы constexpr функции). ⛔️ Отсюда же вытекает отсутствие возможности проверки условий, основанных на шаблонных параметрах кода. Это все из-за того, что препроцессор работает до начала компиляции программы. Он в душе не знает, что вы вообще программу пишите. Ему в целом ничего не мешает обработать текст Войны и Мира. Именно из-за отсутствия понимания контекста программы, мы и не можем проверять условия, основанные на compile-time значениях или шаблонных параметрах. Если вы хотите проверить, указатель ли к вам пришел в функцию или нет, или собрать какую-то метрику с constexpr массива и на ее основе принять решение - у вас ничего не выйдет. ⛔️ Вы очень сильно ограничены возможностями препроцессора. Попробуйте например сравнить какой-нибудь макрос с фиксированной строкой. Спойлер: у вас скорее всего ничего не выйдет. Например, как в примере из поста выше мы не можем написать так:
int DotProduct(const std::vector<int>& vec1, const std::vector<int>& vec2)
{
  int result = 0;
  #if CPU_TYPE == "INTEL"
    // mmx|sse|avx code
  #elif CPU_TYPE == "ARM"
    // arm neon code
  #else 
    static_assert(0, "NO CPU_TYPE IS SPECIFIED");
  #endif
  return result;
}
Поэтому и приходилось определять тип циферками. Это конечно мем: сущность, которая работает с текстом программы, то есть со строками, не может работать со строками. ⛔️ С препроцессором в принципе опасно работать и еще труднее отлаживать магические баги. Могут возникнуть например вот такие трудноотловимые ошибки. Вам придется смотреть уже обработанную единицу трансляции, причем иногда даже не понимая, где может быть проблема. А со всеми включенными бинарниками и преобразованиями препроцессора это делать очень долго и больно. А потом оказывается, что какой-то умник заменил в макросах функцию DontWorryBeHappy на ILovePainGiveMeMore. В комментах @xiran22 скидывал пример библиотечки, написанной с помощью макросов. Вот она, можете посмотреть. Это не только пример сложности понимания кода и всех проблем выше. Тут просто плохая архитектура, затыки которой решаются макросами. Поделитесь в комментах своими интересными кейсами простреленных ступней из-за макросов. Avoid dangerous tools. Stay cool. #compiler #cppcore

🚀Успейте за ростом рынка IoT! 👉Узнайте больше об internet of things на бесплатном вебинаре онлайн-курса «IoT Developer» - «
🚀Успейте за ростом рынка IoT! 👉Узнайте больше об internet of things на бесплатном вебинаре онлайн-курса «IoT Developer» - «Учет энергоресурсов в платформе ThingsBoard»: регистрация На занятии рассмотрим решение учета энергоресурсов (вода, газ, э/э, тепло), познакомимся с реализацией демо проекта на платформе ThingsBoard: 1. Подключение устройств 2. Создание сущностей 3. Разработка цепочек правил 4. Разработка дашборда 🤝Понравится урок — продолжите обучение на курсе по спеццене и даже в рассрочку! erid: LjN8KZBYc

​​Директивы ifdef, ifndef, if #новичкам Иногда код, который мы пишем, должен зависеть от каких-то внешних параметров. Например, неплохо было бы довалять дебажный вывод при дебажной сборке. Или нам нужно написать кусочек платформоспецифичного кода и конкретная платформа передается нам наружными параметрами. Разные в общем бывают ситуации. Получается нам нужен какой-то механизм, который может проверять эти внешние параметры и в зависимости от их значений включать или выключать нужный кусок кода. Эту задачу можно решать по-разному. Сегодня мы обсудим доисторический способ, который, несмотря на свой почтенный возраст и опасность применения, активно используется в существующих проектах. Этот способ - использование директив препроцессора #ifdef, #ifndef, #if. Все три - условные конструкции. Первая смотрит, определен ли в коде какой-то макрос. Если да, то делаем одни действия, если нет - другие. Второй наоборот, входит в первую ветку условия, если макрос не определен, и входит во вторую, если определен. Директива #if проверяет какое-то условие, ничего необычного. Все три директивы могут иметь как полные формы(с веткой в случае если условие ложно), так и неполные(без "else"). Однако #ifdef и #ifndef не могут использовать директиву #elif по понятным причинам. И вот в чем их прикол. Препроцессор работает с текстом программы. И он просто удаляет из этого текста ненужную ветку так, что до компиляции она даже не доходит, а нужная ветка как раз и подвергается обработке компилятором. Например, у нас есть какой-то платформоспецифичный участок кода. Пусть это будет низкоуровневая оптимизация скалярного произведения на векторных инструкциях. Они разные для интеловских процессоров и для армов. Код может выглядеть примерно так:
int DotProduct(const std::vector<int>& vec1, const std::vector<int>& vec2)
{
  int result = 0;
  #if CPU_TYPE == 0
    // mmx|sse|avx code
  #elif CPU_TYPE == 1
    // arm neon code
  #else 
    static_assert(0, "NO CPU_TYPE IS SPECIFIED");
  #endif
  return result;
}
Если каждое значение CPU_TYPE включает нужную ветку кода и убирает из текста программы все остальные. Если мы хотим оптимизировать только под интеловские процессоры, то можем написать чуть проще:
int DotProduct(const std::vector<int>& vec1, const std::vector<int>& vec2)
{
  int result = 0;
  #ifdef OPTIMIZATION_ON
    // mmx|sse|avx code
  #else 
    for (int i = 0; i < vec1.size(); ++i)
      result += vec1[i] * vec2[i];
  #endif
  return result;
}
(Все примеры - учебные, все совпадения с реальным кодом - случайны, не повторяйте код в домашних условиях). Здесь мы проверяем директивой ifdef, определен ли макрос OPTIMIZATION_ON, сигнализирующий что нужно использовать векторные инструкции. Если да, то ключаем в текст программы оптимизированный код. Если нет - обычный. Можно еще кучу примеров и приложений этим директивам привести. Но я хотел подчеркнуть именно вот эту особенность, что мы можем добавлять или выбрасывать определенные участки кода в зависимости от внешних параметров. Широко известно, что такой способ не только устарел, но еще и опасен. Завтра посмотрим, чем конкретно. Choose the right path. Stay cool. #compiler

​​Виртуальный деструктор и std::shared_ptr #опытным Плюсы - поистине удивительный язык. Вот подписчик изучил у нас на канале пользу виртуального деструктора и пошел в комментарии. А там Василий прислал пример, который говорит о том, что в определенном случае виртульность деструктора не важна и без него все работает корректно. И подписчик действительно удивляется: "What the fuck is going on?!?!?!?". Разберем все по порядку. Пример вот такой:
struct Base {
  ~Base() {
    std::cout << "Base::~Base()" << std::endl;
  }
};

struct Derived : Base {
  ~Derived() {
    std::cout << "Derived::~Derived()" << std::endl;
  }
};

int main() {
  std::shared_ptr<Base> p1 = std::make_shared<Derived>();
}
Прикол в том, что при удалении p1 вызовется деструктор наследованного класса:
Derived::~Derived()
Base::~Base()
Почему так? Во время создания std::shared_ptr вы можете задать свой кастомный делитер. Но даже если вы его не предоставили, делитер все равно создается. Просто компилятор сам выведет по его мнению подходящий удалятель. И сохранит его в контрол блок умного указателя. Так вот логично, что, если мы создаем указатель от объекта тип Derived, то и делитер выбирается соотвествующий. И в контрол блоке правого шареда будет делитер, который удаляет Derived*. Далее при присваивании указатель на этот конкретный контрол блок копируется левому шареду. После этого контрольный блок p1 содержит тот самый изначальный делитер, который условно говоря сделает перед удалением указателя каст к классу наследника(delete static_cast<Derived*>(ptr)). Именно поэтому и вызывается деструктор наследника. Если мы попытаемся создать std::shared_ptr вот так:
std::shared_ptr<Base> shared(static_cast<Base*>(new Derived));
то никакой магии уже не будет и деструктор наследника не вызовется. Потому что делитер ничего не будет знать о наследнике, так как мы явным образом привели указатель наследника в указателю на базовый класс. Ну и с уникальным указателем такая штука тоже не сработает. Там делитер не хранится в объекте указателя, он зашит в сам тип. Поэтому для такой строчки:
std::unique_ptr<Base> p1 = std::make_unique<Derived>();
для p1 не вызовется деструктор наследника, потому что делитер типа std::unique_ptr<Base> удаляет только указатели на базовый класс. Чтобы объект удалялся корректно, нужен виртуальный деструктор базового класса. Без него никак. Хоть такой интересный момент в плюсах и существует - не нужно на него полагаться. Одними шаредами жизнь не заканчивается, а классы должны вести себя корректно. Поэтому виртуальный деструктор - наше все! Stay amazed. Stay cool. #cpp11 #cppcore

🌎Программист, говори с миром на одном языке! 👉Узнайте больше о возможностях С на бесплатном вебинаре онлайн-курса «Программ
🌎Программист, говори с миром на одном языке! 👉Узнайте больше о возможностях С на бесплатном вебинаре онлайн-курса «Программист С» — «Unicode в С»: регистрация На вебинаре мы: — подробнее познакомимся с интернациональным стандартом символов Unicode и низкоуровневым устройством его кодировок — развеем несколько популярных мифов в области кодировок и посмотрим на инструменты языка C для работы с юникодом. ✅Занятие будет полезно всем программистам с опытом. 👉После вебинара вы сможете продолжить обучение на курсе по спеццене, в том числе, в рассрочку erid: LjN8KBBMH

​​Результаты ревью Как и обещали, публикуем самый подробный ответ. Автором оказался Михаил, давайте похлопаем ему👏👏👏👏. Ну и коротенькое саммари из комментариев: 🔞 С пространством имен точно намудрили, так как есть типа нестандартный istream, который возвращает стандартный streampos. Если это работало, то где-то был using определен, но пацаны не решили, как они будут дальше писать - с std:: или без. Либо крестик снимите, либо трусы наденьте. 🔞 Если это стандартный стрим, то и размер у него должен быть соответствующего типа std::streamsize. 🔞 Можно использовать новые стандарты с их constexpr'ами и std::array'ями. C++11 уже с натяжкой можно назвать новым стандартом, поэтому в большинстве проектов он уже доступен. 🔞 Оч много вопросов по поводу типа uint32. Стандартный ли это тип или нет? Что там с его выравниванием? Зачем он так мимикрирует под стандартный? Видимо ребятам было лень приписывать "_t" и они объявили typedef... Ну и самая мякотка и основной консёрн - чтение. 🔞 Всего одна наивная проверка на конец стрима, но нет никаких проверок на ошибки(failbit, badbit с помощью соответствующих методов istream или просто is.good()). Потенциально это может привести к бесконечному циклу, так как ошибка произойдет, стрим мы читать не сможем, а условие окончания не достигнуто. 🔞 Цикл довольно тяжело читать, лучше поменять его на for. Тогда будет более явным образом подсвечены начальное состояние, условие окончания и переходная операция. Такая короткая функция, а вон сколько всего можно улучшить! Ревью - очень важная часть разработки ПО. Его обязательно нужно в принудительном порядке проводить во всех проектах, чтобы они не скатились в лютое govniwe. Если вы начинающий, даже такие короткие сессии ревью не вашего кода помогут вам не совершать глупых ошибок. Другим же это хорошая практика тренировки своих извилин в неизвестном кодовом энвайроменте. Analyser yourself. Stay cool.

​​Ревью Попробуем сегодня новую рубрику на канале - #ревью. Мы будем выкладывать коротенькие и не очень куски кода, а вы в комментариях попытаетесь найти все-превсе ошибки в нем. Ну и предложить улучшения конечно. Че хейтить код попросту? Пока не понятен формат, но давайте на этот раз скажем, что ответ того человека, кто найдет больше всех адекватных ошибок и предложит их решение, мы перепостим на канал. Посмотрим, что из этого выйдет. Думаю, будет интересно) Код на картинке под постом. Analyse your life. Stay cool.

​​Short circuit операторы для кастомных типов Свойство короткосхемности в плюсах, как мы все уже знаем, имеют 2 оператора: логическое И и логическое ИЛИ. Но здесь есть проблема, что они теряют это свойство, если их перегружают. Давайте немножко углубимся в философию и порассуждаем кое над чем. Что вообще такое логическое И и что оно делает?(я говорю только про И для краткости, те же рассуждения применяются и к ИЛИ) Эта логическая функция aka коньюнкция. Она отображает множество {0, 1}^N в {0, 1}. То есть она принимает N аргументов, каждый из которых может иметь только в двух значений 0 или 1, и результатом ее работы тоже является одно из двух значений: 0 и 1. Результатом будет 0, если хотя бы один из аргументов имеем значение 0. В обратном случае, результатом будет 1. Что это нам дает. А то, что операндами по строгому математическому определению могут быть только булевые значения. То есть, когда вы делаете логическое И с любыми объектами, на самом деле вы не хотите перегружать этот оператор для работы со своими объектами. Вы хотите(может и не осознанно) ровно такую же логику работы, как и у встроенного оператора: приводить операнды к true или false на ходу. Потому что Логическое И работает с бинарными сущностями. Это просто из определения исходит, что операнды должны быть бинарными. Поэтому на самом деле нужно не перегружать оператор, а научить компилятор преобразовывать объект в тип bool. Тогда вы сможете насладиться всеми чудесами вычислений по короткой схеме. Пример из прошлого поста можно переписать вот так:

struct CustomStruct
{
  int number = 0;
  operator bool() const
  {
    return number;
  }
};

static int check = 0;

CustomStruct func()
{
  check = 1;
  return CustomStruct{};
}

int main() {
  CustomStruct a{};
  a && func();
  std::cout << check << std::endl;
}
Теперь мы научили компилятор преобразовывать объекты нашего кастомного класса в булы и вместо перегруженного оператора используем встроенный. И вуаля, вывод этого кода будет "0". То есть функция func не выполнилась, потому что результат выражения стал ясен после вычисления первого операнда и смысла от вычисления второго нет. Вот так получается, что нет смысла перегружать операторы логического И и ИЛИ для кастомных объектов. На самом деле нужно перегрузить оператор приведения к булевому значению. И будет вам счастье. Define things properly. Stay cool. #cppcore

​​Short circuit для кастомных операторов #опытным Есть одно важное уточнение, которое не было упомянуто в посте про short-circuit операторы, но несколько комментаторов на это указывали. Прочитайте, кстати, пост, если впервые видите словосочетание short circuit. В плюсах короткосхемностью обладают операторы && и ||. Из коробки их операндами могут быть переменные логического, целочисленного и указательного типа. Однако они все так или иначе приводятся к типу булеан. Поэтому в принципе корректно говорить, что логические операторы работают только с логическими типами. Что в целом довольно логичная логика. Однако есть в этом Эдеме есть и змий искуситель, который портит всю малину. Эти операторы можно перегружать для кастомных типов. И тогда они теряют свои короткосхемные свойства. Взгляните на следующий код:

struct CustomStruct
{
  int number = 0;
  bool operator&&(const CustomStruct& other) 
  {
    return number && other.number;
  }
};

static int check = 0;

CustomStruct func()
{
  check = 1;
  return CustomStruct{};
}

int main() {
  CustomStruct a{};
  a && func();
  std::cout << check << std::endl;
}
Здесь мы создаем самую простую структурку и перегружаем для нее оператор логического И. Дальше, чтобы проверить ленивость вычисления оператора, пишем простую функцию, которая при исполнении изменяет статическую переменную. Так мы сможем наверняка убедиться, выполнилась ли функция или нет: если выполнилась, то переменная check будет выставлена в единицу, если нет, то останется нулем. И вывод будет реально "1". Что выглядит довольно печально. Ну и кстати, такое поведение довольно легко объяснить. Когда мы перегружаем операторы, то мы создаем новые функции. И я хочу акцентировать на этом внимание: это именно пользовательские функции, как бы они там не назывались. А аргументы пользовательских функций должны быть вычислены ДО захода в функцию. Поэтому любые операнды должны быть полностью вычислены до вычисления значения всего выражения. Это и приводит к отсутствию свойства short circuit. Хотя в том виде, в котором оператор перегружен в коде выше, внутри него используется short circuit операция и на самом деле второй операнд не будет учитываться, если у вызываемого объекта поле класса равно нулю. Но за счет того, что мы обязаны вычислить второй операнд, то просто технически не выполняются требования короткой схемы вычислений. Встроеные же операторы реализованы на более низком уровне и не являются в прямом смысле функциями. И в эту реализацию изначально заложен короткосхемный функционал. Однако есть способ разрешить ленивое вычисление логического И или ИЛИ для кастомных типов. Но об этом в следующий раз. Don't loose your properties. Stay cool #cppcore

🖥 С++ Академиясамый крупный обучающий ресурс в Telegram, посвященный С++. Здесь ребята 24/7 пишут код, тестируют разные библиотеки, копаются в их настройках и применяют лучшие инструменты в работе, а потом делятся с вами. Лучшие бесплатные курсы С++ 2024Освежить, как устроена многопоточность в C++Анализатор кода C/C++Whisper.cpp для перевода аудио в текстМАСТ-ХЭВ папка, для всех, кто любит и узучает C++ и многое дргое Подписывайтесь, потом сами себе спасибо скажете: @cpluspluc

​​Member initialization. Best practices #новичкам Пост по запросу подписчика. Вот его вопрос. И реально ведь непонятно, что делать. Столько разных вариантов и возможностей можно придумать для инициализации полей класса, что голова ходит кругом. Какой метод самый оптимальный? Сейчас и будем разбираться. Здесь я буду приводить какое-то общие и распространенные принципы. К каждому можно придраться и сказать "а у нас в проекте по-другому!". Исключения и другие подходы есть везде. Если хотите высказать свои варианты - комменты открыты. Начну с того, что нужно предпочитать инициализировать поля либо с помощью списка инициализации конструктора, либо с помощью default member initializer. Дело в том, что все поля на самом деле инициализируются до входа в конструктор! Если списком инициализации или default member initializer'ом не установлено, как поле должно инициализироваться, то в конструктор оно попадет инициализированным по умолчанию. Именно поэтому, например, не можете в конструкторе инициализировать объект класса, у которого нет конструктора по умолчанию. Будет ошибка компиляции и у вас потребуют дефолтный конструктор. Запомните: конструктор нужен для нетривиальных вещей. С простой иницализацией справятся ctor initialization list и инициализатор по умолчанию. Далее. Остается 2 способа, как инициализировать. Какой из них выбрать и в какой пропорции смешивать? CppCoreGuidelies говорят нам: "Prefer default member initializers to member initializers in constructors for constant initializers". То есть, если инициализатор константный, то используйте default member initializer. Причина: inplace инициализатор делает явным то, что именно эти дефолтовые значения будут использоваться во всех конструкторах. Пример:
class X { // BAD 
  int i; 
  string s; 
  int j; 
public: 
  X() :i{666}, s{"qqq"} { } // j is uninitialized 
  X(int ii) :i{ii} {} // s is "" and j is uninitialized 
 // ... 
};
Как в этом случае читатель кода поймет, была ли инициализация j специально пропущена(что скорее всего не очень гуд) или было ли для s намеренным выставление его значения в "qqq" в первом случае и в пустую строку во втором случае(почти стопроцентный баг)? Все эти ошибки могут появиться при добавлении новых полей в класс. По классике: добавили новое поле, использовали его в методах, но вот в одном месте упустили инициализацию. Кейс настолько жизненный, что мое почтение. Более адекватный вариант:
class X2 { 
  int i {666}; 
  string s {"qqq"}; 
  int j {0}; 
public: 
  X2() = default; // all members are initialized to their defaults 
  X2(int ii) :i{ii} {} // s and j initialized to their defaults 
// ... 
};
Красота. Все в одном месте, все четко и понятно. Тут используется одна фишка: у вас есть несколько конструкторов, которые могут выставлять значения полям, а могут и не выставлять. Вы в одном месте определяете дефолтные значения и в списках инициализации конструкторов переопределяете инициализирующее значение для нужного поля, так как список подавляет инициализатор по умолчанию. Также это более читаемый вариант, так как все дефолтные значения находятся в одном месте и не нужно бегать глазами по коду в их поисках. Используйте default member initializer и будет вам счастье! Stay happy. Stay cool. #cpp11 #cppcore #goodpractice

Сочетание member initialization list и default member initializer #опытным Вот здесь мы поговорили о том, почему важно соблюдать порядок следования полей класса в списке инициализации конструктора. Дело в том, что вне зависимости от того, как написан этот список, поля будут инициализироваться в порядке появления их объявления. Также в С++11 у нас появилась фича под названием default member initializer. Это та самая штуковина, которая позволяет вам инициализировать нестатические поля класса не в конструкторе, а прям inplace. Типа того:
struct Class {
  int field = 5;
};
Фича полезная, многие ей часто пользуются. Но вот возникает вопрос: как список инициализации конструктора взаимодействует с default member initializer? Если я инициализирую поля вне конструктора и компилятор видит эти значения явным образом, то возможно эти поля и получают значение первыми? Сейчас все узнаем. Посмотрим на такой пример:
struct Char {
  Char(char c) : field{c} {std::cout << "Char " << field << std::endl;}
  Char() = default;
  char field;
};

struct TestClass {
  TestClass() : a{'1'},
          c{'3'},
          e{'5'} {}
  Char a;
  Char b = '2';
  Char c;
  Char d = '4';
  Char e;
};
Есть простенький класс Char, который выводит на консоль момент создания объекта. И тестовый класс, на котором мы и проводим эксперимент. И в этом эксперименте мы и проверим, в каком порядке свои значения получают поля b и d, относительно a, c, e. На самом деле здесь правило ровно такое же. Нестатические поля класса инициализируются в порядке их появления в описании класса. Поэтому вывод будет таким:
Char 1
Char 2
Char 3
Char 4
Char 5
С этим разобрались. И тут назревает вопрос: а что будет, если я в начале проициализирую поле inplace, а потом еще раз в constructor initializer list? Какая из инициализаций победит другую? Или быть может они произойдут обе в какой-то очередности? Выглядеть это может так:
struct Char {
  Char(char c) : field{c} {std::cout << "Char " << field << std::endl;}
  Char() = default;
  char field;
};

struct TestClass {
  TestClass() : a{'1'},
          b{'2'},
          c{'3'},
          d{'4'},
          e{'5'} {}
  Char a;
  Char b = 'b';
  Char c;
  Char d = 'd';
  Char e;
};
Опять в подопытные мы взяли поля b и d и задали им значения с помощью default member initializer. А вдогонку еще и в списке инициализации присвоили им значение. В такой ситуации default member initializer не играет никакой роли, блаженно складывает лапки и отдает бразды правления списку инициализации. Вывод будет тем же, что и в прошлом примере:
Char 1
Char 2
Char 3
Char 4
Char 5
Но это только список инициализации так работает. Если для инициализации поля вы используете обычный конструктор, то оно первый раз проинициализируется с помощью default member initializer(которая обязательно происходит до входа в тело конструктора), а второй раз - в теле конструктора.
struct TestClass {
  TestClass() : a{'1'},
          c{'3'},
          d{'4'},
          e{'5'} {b = '2';}
  Char a;
  Char b = 'b';
  Char c;
  Char d = 'd';
  Char e;
};
// Output

Char 1
Char b
Char 3
Char 4
Char 5
Char 2
Пишите в комменты, если есть еще какие-то интересные кейсы взаимодействия этих сущностей. В будущем, разберем их на канале. Mix things properly. Stay cool. #cpp11 #cppcore

🚙Мониторинг движения транспорта в IoT служит для всестороннего мониторинга всех участников дорожного движения, включая отсле
🚙Мониторинг движения транспорта в IoT служит для всестороннего мониторинга всех участников дорожного движения, включая отслеживание скорости, траекторий, выхода пешеходов на дорогу и всего, что может в конечном итоге повлиять на поток и безопасность дорожного движения. 👉Узнайте больше на бесплатном вебинаре онлайн-курса «IoT Developer» - «Мониторинг транспорта в платформе ThingsBoard»: регистрация На занятии рассмотрим решение мониторинга транспорта, познакомимся с реализацией демо проекта на платформе ThingsBoard: 1. Подключение устройств 2. Создание сущностей 3. Разработка цепочек правил 4. Разработка дашборда 🤝Понравится урок — продолжите обучение на курсе по спеццене и даже в рассрочку! erid: LjN8KN7VP

Default member initializer #новичкам Представьте себе большой класс, определенный целиком в одном файле. Этак строк на 300-400. Обычно принято в таком порядке описывать класс: конструкторы, деструктор, методы и только потом поля. Вариации могут быть разными, но из моей практики одно остается неизменным: объявления конструктора и полей находятся в разных концах тела класса. И вот бывают случаи, когда при создании объекта какие-то поля получают свое значение не из внешних параметров, а какие-то заранее заданные. Дефолтовые. И вообще было бы очень приятненько видеть значения по умолчанию полей каждый раз, когда мы встречаем их объявления в теле класса. Если бегло читать код, то часто приходится смотреть на список полей. И было бы просто удобно не возвращаться к конструкторам каждый раз, чтобы вспомнить эти значения, а иметь их сразу рядом с объявлением полей. Такие удобства появились у нас в C++11 - default member initializer. Это именно то, что и хотелось иметь в описанных выше ситуациях. Пример
template<typename T>
struct Stack {
  // rule of 5
  void push(const T& elem) {...}
  void push(T&& elem) {...}
  T& front() {...}
  T& front() const {...}
  void pop() {}
  T GetMinElem() {...}
private:
  std::deque<T> container;
  T min_elem{Limit<T>::max_value};
}
Здесь мы создает простой шаблонный класс стека с одной особенностью: в каждый момент времени вы можете из этого стека получить самое минимальное значение из тех элементов, которые содержатся в этом стеке. Кстати, вам задачка на подумать, как такое можно сделать. Пример здесь сильно укороченный. Если реализовывать все по чесноку, то реализация такого шаблонного класса займет приличное количество места. Вариантов методов и конструкторов может быть миллион. И я не очень хочу в них возвращаться, чтобы узнать, какое изначальное состояние имеет поле min_elem. А здесь мы сразу видим: у пустого стека примем значение минимального элемента, как максимально возможное значение этого типа. Тогда при добавлении в стек первого элемента для обновления минимума мы можем пользоваться тем же условием, что и для добавления остальных элементов
if (new_elem <= min_elem)
  min_elem = new_elem;
Limit<T> - шаблонный класс, который хранит максимальное и минимальное значение для заданного шаблонного типа. Это может быть реализовано как угодно: через явные специализации, через if constexpr и так далее. Шаблонная магия в общем. Кто хочет, опять же, может в комментах попрактиковаться в реализации этого класса. Кто не знал - пользуйтесь, вещь полезная. Stay useful. Stay cool. #cpp11 #cppcore

Виртуальный деструктор #новичкам Возможно САМЫЙ популярный вопрос на собеседованиях на джунов и мидлов. Оно и справедливо в принципе: очень простой и понятный вопрос, но ответ на него требует хорошего уровня понимания ООП в принципе и как оно конкретно работает в плюсах. Динамический полиморфизм, наследование, порядок вызовов конструкторов и деструкторов - да, это база. Но именно ее и нужно проверить у начинающих и продолжающих разработчиков. Обычно они заходят немного издалека и просят вангануть, что выведется в консоль для примерно такого кода:
struct Resource {
  Resource() { std::cout << "Resourse has been acquired\n";}
  ~Resource() { std::cout << "Resource has been released\n";}
};

struct Base {
  Base() { std::cout << "Base Constructor Called\n";}
  ~Base() { std::cout << "Base Destructor called\n";}
};

struct Derived1: Base {
  Derived1() { 
    ptr = std::make_unique<Resource>(); 
    std::cout << "Derived constructor called\n";
  }
  ~Derived1() {std::cout << "Derived destructor called\n";}
private:
  std::unique_ptr<Resource> ptr;
};

int main() {
  Base *b = new Derived1();
  delete b;
}
Вроде ничего сложного, но вот надо все штуки вспомнить, как там объекты создаются, в каком порядке что вызывается. Для начинающих тут часто затупки начинаются. В коде вроде все хорошо написано и невнимательный кандидат может выдать вот это:
Base Constructor Called
Resourse has been acquired
Derived constructor called
Derived destructor called
Resource has been released
Base Destructor called
Вот тут-то его и подловили! На самом деле никакого деструктора наследника вызвано не будет и соответственно ресурсы не освободятся. Интервьюер дает наводку посмотреть на деструктор базового класса. И кандидат с красным лицом кричит: "Деструктор - невиртуальный! По указателю на базовый класс вызовется сразу деструктор базового класса, а деструктор дочернего не вызовется. Будет утечка памяти". Его так на курсах научили говорить. И дальше он выдает правильный вывод программы. И тут интервьюер говорит: "А что будет, если наследник не будет содержать никаких полей? Какие проблемы будут у этого кода?". И молодой разработчик в ступоре: он же знает, что невиртуальный деструктор приводит к утечкам. Но тут вроде как и утекать нечему. И говорит, что вроде как и проблем не будет. Естественно, это неправда. Если с виду ничего плохого не может произойти и даже при запуске программы ничего плохого не происходит - это не значит, что в программе нет проблем. Стандарт говорит:
if the static type of the object to be deleted 
is different from its dynamic type, the static type 
shall be a base class of the dynamic type of 
the object to be deleted and the static type 
shall have a virtual destructor or
 the behavior is undefined.
Отсутствие виртуального деструктора при удалении через базовый класс приводит к неопределенному поведению. И точка. Можете даже больше не упоминать утечки. Потому что может память утечь, а может и Пентагон задудосится от такой программы. Никто не знает. Для корректного поведения полиморфных объектов и вызова деструктора дочернего класса вам обязательно нужен виртуальный деструктор базового класса. Часто встречал эту проблему у младших разработчиков, да и сам я спотыкался на этом. Но теперь наши подписчики вооружены и опасны! Stay armed. Stay cool. #cppcore #interview

Разбираем как решать задачи на LeetCode ✅ Примеры решений ✅ Пояснения Выбери своё направление: 1. JavaScript 2. Python 3. Jav
Разбираем как решать задачи на LeetCode ✅ Примеры решений ✅ Пояснения Выбери своё направление: 1. JavaScript 2. Python 3. Java 4. C# 5. C/C++ 6. Golang 7. PHP 8. Kotlin 9. Swift