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

Грокаем C++

前往频道在 Telegram

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

显示更多
9 386
订阅者
+924 小时
+147
+1430
帖子存档
Deducing this и CRTP #опытным У deducting this есть одна особенность. При обычном наследовании(без виртуальных функций) методы родительского класса знают про точный тип объектов наследников, которые вызывают метод:
struct Machine {
  template <typename Self>
  void print(this Self&& self) {
    self.print_name();
  }
};

struct Car : public Machine {
  std::string name;
  void print_name() {
    std::cout << "Car\n";
  }
};

Car{}.print(); // Выведется "Car"
Вам ничего это не напоминает? CRTP конечно. Этот паттерн и используется в принципе, чтобы родители имели доступ к точному типу объекта наследника:
template <typename Derived>
struct add_postfix_increment {
    Derived operator++(int) {
        auto& self = static_cast<Derived&>(*this);

        Derived tmp(self);
        ++self;
        return tmp;
    }
};

struct some_type : add_postfix_increment<some_type> {
    // Prefix increment, which the postfix one is implemented in terms of
    some_type& operator++();
};
За счет шаблонного параметра Derived, который должен быть точным типом наследника, мы можем безопасно кастануть this к указателю на наследника и вызывать у него любые методы. Но с появлением deducing this мы можем избежать рождения этого странного отпрыска наследования и шаблонов:
struct add_postfix_increment {
    template <typename Self>
    auto operator++(this Self&& self, int) {
        auto tmp = self;
        ++self;
        return tmp;
    }
};

struct some_type : add_postfix_increment {
    // Prefix increment, which the postfix one is implemented in terms of
    some_type& operator++();
};
Ну вот. У нас только один шаблонный метод. Но для пользователя он ничем не отличается от обычного нешаблонного метода. Все красиво, эстетично и не ломает голову людям, мало работающим с шаблонами. Make things more elegant. Stay cool. #template #cpp23

Передача объекта в методы по значению #опытным Небольшие типы данных, особенно до 8 байт длиной, быстрее передавать в методы или возвращать из методов по значению. С помощью deducing this мы можем вызывать методы не для ссылки(под капотом которой указатель), а для значения объекта. Семантика будет ровно такая, как вы ожидаете. Объект скопируется внутрь метода и все операции будут происходить над копией. Давайте посмотрим на пример:
struct just_a_little_guy {
    int how_small;
    int uwu();
};

int main() {
    just_a_little_guy tiny_tim{42};
    return tiny_tim.uwu();
}
Здесь используется старая нотация с неявным this. Посмотрим, какой код может нам выдать компилятор:
sub     rsp, 40                           
lea     rcx, QWORD PTR tiny_tim$[rsp]
mov     DWORD PTR tiny_tim$[rsp], 42     
call    int just_a_little_guy::uwu(void)  
add     rsp, 40                            
ret     0
Пройдемся по строчкам и посмотрим, что тут происходит: - первая строчка аллоцирует 40 байт на стеке. 4 байта для объекта tiny_tim, 32 байта теневого пространства для метода uwu и 4 байта паддинга. - инструкция lea загружает адрес tiny_tim в регистр rcx, в котором метод uwu ожидает свой неявный параметр. - mov помещает число 42 в поле объекта tiny_tim. - вызываем функцию-метод uwu - наконец деаллоцируем памяти и выходим из main А теперь применим deduction this с параметром по значению и посмотрим на ассемблер:
struct just_a_little_guy {
    int how_small;
    int uwu(this just_a_little_guy);
};
Ассемблер:
mov     ecx, 42                           
jmp     static int just_a_little_guy::uwu(this just_a_little_guy)
Мы переместили 42 в нужный регистр и сразу же прыгнули в функцию uwu, а не вызвали ее. Поскольку мы не передаем объект в метод по ссылке, нам ничего не нужно аллоцировать на стеке. А значит и деаллоцировать ничего не нужно. Раз нам не нужно за собой подчищать, то можно просто прыгнуть в функцию и не возвращаться оттуда. Конечно, это искусственный пример, оптимизация есть и мы можем в целом ожидать, то объекты маленьких типов можно быстрее обрабатывать с помощью deducing this. Optimize yourself. Stay cool. #cpp23 #optimization #compiler

Возможности для молодых людей в «Алабуге», Республике Татарстан В особой экономической зоне «Алабуга» активно развивается лид
Возможности для молодых людей в «Алабуге», Республике Татарстан В особой экономической зоне «Алабуга» активно развивается лидерская программа «100 Лидеров». В ней могут поучаствовать молодые специалисты от 19 до 26 лет. У участников есть возможность познакомиться с топ-менеджерами компании, поиграть в бизнес-игры, пройти собеседования с реальным шансом трудоустройства в компанию «Алабуга». Питание и проживание за счет компании. Работа в «Алабуге» - это зарплата от 110 до 240 тысяч рублей и участие в реализации проектов мирового уровня. Следующий поток - с 5 по 9 апреля! Заявку можно подать на сайте

Deducing this #опытным Все методы принимают неявный параметр - указатель this на текущий объект. Также мы можем вызывать методы для объектов с разной константностью/ссылочностью. И главное - компилятор знает в момент компиляции вызова метода настоящий тип объекта со всеми квалификаторами. Единственное, что отделяется нас от возможности введения шаблонности - это указательный тип this, который не инкапсулирует в себе информацию о квалификаторах объекта. И в С++23 именно этот момент и изменили. Теперь мы можем явно указывать тип объекта, на который указывает this. И это по сути полностью заменяет cv и ref квалификацию методов. Выглядит это так:
struct cat {
    std::string name;

    void print_name(this cat& self) {
        std::cout << name;       //invalid
        std::cout << this->name; //also invalid
        std::cout << self.name;  //all good
    }
    void print_name(this const cat& self) {
        std::cout << self.name;
    }
    void print_name(this cat&& self) {
        std::cout << self.name;
    }
    void print_name(this const cat&& self) {
        std::cout << self.name;
    }
};
Особенности: 👉🏿 Мы явно указываем параметр this. 👉🏿 Явно указываем тип объекта и его квалификаторы. 👉🏿 Считайте, что это статические методы, внутрь которых передали объект того же класса. Синтаксис доступа в полям соотвествующий: нельзя упоминать this, нельзя неявно обращаться к членам класса, только через имя параметра. 👉🏿 Поэтому нельзя такие методы объявлять статическими, ибо невозможно будет различить вызов статического и нестатического метода с одинаковым именем. Теперь у нас есть все инструменты и мы можем сделать шаблонный this. Давайте посмотрим на обновленный метод value класса optional:
template <typename T>
struct optional {
  // One version of value which works for everything
  template <class Self>
  constexpr auto&& value(this Self&& self) {
    if (self.has_value()) {
        return std::forward<Self>(self).m_value;
    }
    throw bad_optional_access();
  }
};
Вот это бэнгер! Мы деквадруплицировали код! Здесь мы используем шаблонный параметр Self с универсальной ссылкой. В этом случае параметр self будет в точности повторять тип объекта, на котором вызван метод. И для правильной передачи значения наружу мы используем идеальную передачу и std::forward + auto&& возвращаемое значение, которое тоже будет соответствовать cv+ref типу объекта. Настоящая магия, причем вне хогвартса! Имена Self и self использовать необязательно, это отсылки к питону и первом параметру методов классов self. Вот вам пропоузал по этой замечательной фиче. А мы в нескольких следующих постах будем разбирать кейсы, где она может быть применима. Simplify your life. Stay cool. #cpp23 #template

Проблемы ref-qualified методов #опытным Мы разобрали, что перегрузки методов по ссылочным типам объектов могут быть полезными в разных контекстах. Они могут использовать как в совокупности для достижения универсальности в обработке объектов, или точечно для тонкой настройки-подкрутки функциональности Но один из примеров в том посте выбивается из общей массы. Еще раз посмотрим на него:
template <typename T>
class optional {
  // version of value for non-const lvalues
  constexpr T& value() & {
    if (has_value()) {
      return this->m_value;
    }
    throw bad_optional_access();
  }

  // version of value for const lvalues
  constexpr T const& value() const& {
    if (has_value()) {
      return this->m_value;
    }
    throw bad_optional_access();
  }

  // version of value for non-const rvalues... are you bored yet?
  constexpr T&& value() && {
    if (has_value()) {
      return std::move(this->m_value);
    }
    throw bad_optional_access();
  }

  // you sure are by this point
  constexpr T const&& value() const&& {
    if (has_value()) {
      return std::move(this->m_value);
    }
    throw bad_optional_access();
  }
  // ...
};
Это примерно то, как метод value класса std::variant был введен в стандарт С++17. Мягко говоря, есть ощущение, что код дублируется. А если не считать мува, то вообще квадруплицируется. Это вот стандартная штука, когда функции отличаются немного и их нельзя объединить в одну. В таких случаях обычно помогают шаблоны. А учитывая, что у нас для левых ссылок нет мува, а для правых - есть, очень сильно напрашиваются универсальные ссылки и шаблонный std::forward. Но тут шаблон вообще никак не вписывается. Методы же не принимают даже никаких аргументов. Какой шаблонный параметр сюда вообще вписывается? Ну вообще говоря, методы принимают неявный аргумент this.... To be continued. Intrigue people. Stay cool. #cppcore

⚡️Асинхронность без сложных потоков? В C++20 это возможно. Корутины позволяют выполнять задачи параллельно без создания лишни
⚡️Асинхронность без сложных потоков? В C++20 это возможно. Корутины позволяют выполнять задачи параллельно без создания лишних потоков, экономя ресурсы и упрощая код. На открытом вебинаре 27 марта в 20:00 мск разберём, как co_await и co_yield работают в современных C++-проектах, где применяются в реальных задачах и почему это важно для высоконагруженных систем. Разберём практические примеры из сетевого программирования и обработки данных. Освойте новую парадигму асинхронности, избавьтесь от проблем с потоками и сделайте свой код проще и быстрее. Вы научитесь внедрять корутины в свои проекты, разберётесь в новшествах C++20/23 и сможете использовать их в продакшене. 👉Регистрируйтесь и получите скидку на большое обучение «C++ Developer. Professional»: https://otus.pw/qwii/?erid=2W5zFGdMrLG  Реклама. ООО "ОТУС ОНЛАЙН-ОБРАЗОВАНИЕ". ИНН 9705100963.

Как думаете, нужен ли С++ стандартный сборщик мусора?
Как думаете, нужен ли С++ стандартный сборщик мусора?

auto аргументы функций #опытным Проследим историю с возможностью объявлять аргументы функций, как auto. До С++14 у нас были только шаблонные параметры в функциях и лямбда выражения, без возможности передавать в них значения разных типов Начиная с С++14, мы можем объявлять параметры лямбда выражения auto и передавать туда значения разных типов:
auto print = [](auto& x){std::cout << x << std::endl;};
print(42);
print(3.14);
Это круто повысило вариативность лямбд, предоставив им некоторые плюшки шаблонов. У обычных функции, тем не менее, так и остались обычные шаблонные параметры. Но! Начиная с С++20, параметры обычных функций можно также объявлять auto:
void sum(auto a, auto b)
{
    auto result = a + b;
    std::cout << a << " + " << b << " = " << result << std::endl;
}

sum(1, 3);
sum(3.14, 42);
sum(std::string("123"), std::string("456));
// OUTPUT:
// 1 + 3 = 4
// 3.14 + 42 = 45.14
// 123 + 456 = 123456
Если для лямбд это было необходимым решением из-за того, что их не хотели делать шаблонными(хотя в С++20 их уже можно делать такими), то auto параметры обычных функций призваны немного упростить шаблонную логику там, где не нужно использовать непосредственно тип шаблонного параметра. Так сказать, шаблоны на чилле и расслабоне. Осталось только добавить, что параметры auto работают по принципу выведения типов для шаблонов, а не по принципу выведения типов auto переменных. История небольшая, но становится понятно, что С++ все больше уходит в неявную типизацию. С одной стороны это хорошо, проще писать код и не задумываться над типами. С другой стороны, чтобы этим пользоваться на высоком уровне, нужно знать всякие маленькие нюансики, которых становится все больше и больше. Кому нравится, тот обрадуется и будет пользоваться. Кому не нравится, может писать в стиле С++03 и все будет у него прекрасно. Hide unused details. Stay cool. #cpp11 #cpp14 #cpp20 #template

🔥 Меняете мир вокруг и IT-сферу? Приходите на Яндекс Dev Day&Night — конференцию для мобильных и бэкенд-разработчиков, продактов и аналитиков! ⏰ Когда? 19 апреля 📍 Где? Москва 💡 Что вас ждёт? – 5 треков докладов от экспертов – Полезные знакомства и общение – Тусовка до 2 ночи с коктейлями, диджеями и дискуссиями не под запись! В прошлом году нас было 670 участников, а в этом будет ещё больше. Такое точно нельзя пропускать! 👉 Регистрируйтесь прямо сейчас и зовите друзей. Реклама. ООО «Яндекс.Такси» ИНН: 7704340310 Erid: 2VfnxvpbsBN

Перегружаем деструктор #новичкам Мы знаем, что методы класса можно перегружать, как обычные фукнции. Мы также поняли, что можно перегружать методы так, чтобы они отдельно работали для rvalue и lvalue ссылок. Можно даже перегружать конструкторы класса, чтобы они создавали объект из разных данных. Но можно ли перегружать деструктор класса? Резонный вопрос, деструктор - это такой же метод и такая же функция, почему бы его и не перегрузить. По поводу дополнительных параметров деструктора. Деструкторы стековых переменных вызываются неявно при выходе из скоупа. В языке просто нет инструментов, чтобы сообщить компилятору, как надо удалить объект. Способ только один. Удаление объектов, аллоцированных на стеке, ничем не должно идейно отличаться от удаления автоматических переменных. Поэтому и операторы delete и delete[] не принимают никаких аргументов. Единственный вариант остается - это передавать дополнительные параметры при явном вызове деструктора. Однако кейсы применимости явного вызова деструктора и так сильно ограничены. Добавлять в стандарт перегрузку деструкторов, чтобы на этом строилась какая-то логика - излишне. И если вам уж захотелось построить какую-то логику на удалении, то можно ее вынести в статический метод destroy. Ну а вообще. Задача деструктора - освободить ресурсы класса. Для конкретного класса набор его ресурсов определен на этапе компиляции. И есть всего один способ корректно освободить ресурс: вызвать delete, закрыть сокет или вызвать деструктор. И этот способ определен самим ресурсом. Нет никакой опциональной логики при освобождении ресурсов в деструкторе. Вне зависимости от типа объекта и его ссылочности, данные внутри него выглядят одинаково. А значит и деструктор должен делать свою работу единообразно. Не то, чтобы сильно полезный пост. У новичков иногда возникают такие вопросы. Но в принципе иногда нужно задумываться над такими, казалось бы, привычными вещами, чтобы глубже понимать инструменты, с которыми мы работаем. Have a deeper understanding. Stay cool. #memory #cppcore

Кейсы применения ref-qualified методов #опытным В нескольких предыдущих постах мы говорили про ref-qualified методы и как компилятор выбирает правильную перегрузку. Эта фича многим незнакома и сходу не очень понятно, где ее можно использовать. Давайте сегодня чуть подробнее поговорим о том, где они могут быть реально полезны, чтобы вы вдохновились и использовали такую перегрузку методов чаще. ✅ Разработка библиотек. Довольно очевидно, что разработчикам всяких библиотек нужно учитывать примерно все сценарии использования их классов. Пользователи(безумные) могут скастить объект к константной правой ссылке и методы класса должны работать корректно. Тут очень важно, чтобы тип возвращаемого значения методов соответствовал типу объекта. Пример:
template <typename T>
class optional {

  constexpr T& value() & {
    if (has_value()) {
      return this->m_value;
    }
    throw bad_optional_access();
  }

  constexpr T const& value() const& {
    if (has_value()) {
      return this->m_value;
    }
    throw bad_optional_access();
  }

  constexpr T&& value() && {
    if (has_value()) {
      return std::move(this->m_value);
    }
    throw bad_optional_access();
  }

  constexpr T const&& value() const&& {
    if (has_value()) {
      return std::move(this->m_value);
    }
    throw bad_optional_access();
  }
  // ...
};
Если объект временный, то возвращаем правую ссылку на мувнутый ресурс. Если объект lvalue, то возвращаем обычную ссылку. ✅ Форсить ограничения на методы. Если у вас методы возвращают левые ссылки(константные и неконстантные), то неплохо бы их пометить &, чтобы эти методы могли вызываться только у именованных объектов. Ведь если получить ссылку на внутренний ресурс временного объекта, то временный объект уничтожится, а вы останетесь с разбитым корытом висячей ссылкой. Спасибо @d7d1cd за кейс)
struct Vector {
  int & operator[](size_t index) & { // notice & after arguments
    return vec[index];
  }
  std::vector<int> vec;
};

Vector v;
v.vec = {1, 2, 3, 4};
v[1]; // ok
Vector{{1, 2, 3, 4}}[1]; // compile error
Также прикрепляю ссылочку на быстрый ответ из блога стандарта С++ посвященный этому кейсу. ✅ Оптимизации. Иногда для определенных ссылочных типов мы можем оптимизировать какой-то метод. Например, в С++23 ввели rvalue reference перегрузку для метода substr класса std::basic_string. Мы знаем, что метод substr формирует новую строку, копируя туда рэндж из оригинальной строки. С++23 теперь сделал так, чтобы при вызове метода substr у правых ссылок объект подстроки тырил данные у оригинальной строки и фактически формировался из ее внутреннего буфера. Более подробно можно почитать в пропоузале. Также, если вы возвращаете из метода легковесный объект, то в перегрузке для rvalue ссылок вы можете возвращать объект по значению. Так вы избавляетесь от избыточной ссылочной семантики и индирекции и , возможно, улучшаете перформанс. Ведь маленькие типы быстрее передавать и возвращать именно по значению:
struct Vector {
  int operator[](size_t index) && { // notice & after arguments
    return vec[index];
  }
  std::vector<int> vec;
};
В общем, в каждом конкретном случае оптимизировать можно по-разному. Так что ref-qualified методы - это прекрасный инструмент тонкой настройки в руках профессионалов. Be useful. Stay cool. #cppcore #optimization #cpp23

😮‍💨Устали вручную разруливать зависимости в C++ проектах? Время автоматизировать процесс! 🕒💻 Пакетные менеджеры Conan и v
😮‍💨Устали вручную разруливать зависимости в C++ проектах? Время автоматизировать процесс! 🕒💻 Пакетные менеджеры Conan и vcpkg позволяют легко управлять библиотеками, устанавливать зависимости и ускорять сборку. Разберем, как это работает, на открытом уроке. Упростите себе жизнь: научитесь использовать пакетные менеджеры, чтобы писать код, а не разбираться с проблемами сборки. Спикер Денис Злобинстарший инженер-программист в Astra Linux, опытный наставник разработчиков. ➡️Встречаемся 20 марта в 20:00 мск, разберем всё на практике! Участники получат скидку на большое обучение по разработке на С++. Регистрация: https://otus.pw/X2us/ Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576, www.otus.ru

Каков результат попытки компиляции и запуска кода выше?
Anonymous voting

Третий пошел
struct SomeClass {
  // void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
  void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
  void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
  // void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};

int main() {
  SomeClass lvalue;
  lvalue.foo();
  const_cast<const SomeClass&&>(lvalue).foo();
}

Каков результат попытки компиляции и запуска кода выше?
Anonymous voting

Второй пошел
struct SomeClass {
  // void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1

  void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2

  void foo() && = delete; //3

  void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};

int main() {
  SomeClass lvalue;
  lvalue.foo();
  SomeClass{}.foo();
}

Каков результат попытки компиляции и запуска кода выше?
Anonymous voting

Мини-квизы Сегодня будет вторая и последняя пачка мини-квизов на тему перегрузки методов cv-ref квалификаторами. Мы учитываем пожелания подписчиков и теперь в квизах будет показываться правильный ответ сразу. Также чтобы не драконить вас дополнительными постами с объяснениями, я залил их в статью в телеграфе. Так что после квизов там вы сможете посмотреть, почему выбирается та или иная перегрузка. Также по прежнему в код за кадром подключаются все необходимые хэдэры, а программа собирается на 17-м стандарте. А в ответах квиза перенос строки обозначается через "\n". Вроде с дикслеймером все. Первый пошел:
struct SomeClass {
  void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
  void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
  // void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
  // void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};

int main() {
  SomeClass lvalue;
  lvalue.foo();
  SomeClass{}.foo();
}

Ответы на мини-квизы
struct SomeClass {
  // void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
  void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
  void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
  void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};

int main() {
  SomeClass lvalue;
  lvalue.foo();
  SomeClass{}.foo();
}
Здесь вызовутся методы 2 и 3 по порядку. За неимением неконстантной перегрузки для левых ссылок, остается только константная перегрузка для первого вызова.Во втором случае rvalue reference может приводиться к константной левой ссылке, но в этот раз есть более подходящие кандидаты на перегрузку. И самым подходящим будет 3 метод.
struct SomeClass {
  // void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
  void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
  // void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
  void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};

int main() {
  SomeClass lvalue;
  lvalue.foo();
  std::move(lvalue).foo();
}
Вызовутся методы 2 и 4 по порядку. rvalue reference может приводиться к константной левой ссылке, но также может приводиться к const rvalue ref. Второе преобразование достигается меньшими усилиями, поэтому вызовется 4 метод.
struct SomeClass {
  // void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
  void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
  // void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
  void foo() const && = delete; //4
};

int main() {
  SomeClass lvalue;
  lvalue.foo();
  SomeClass{}.foo();
}
Здесь будет ошибка компиляции на втором вызове. Для него подходили бы 3, 4 и 2 перегрузки в порядке приоритета. Но 3 нет, а следующая наиболее подходящая перегрузка удалена. Удаленные функции участвуют в разрешении перегрузки, поэтому компилятор решит, что мы хотим вызвать удаленную форму, и запретит нам это делать.

Ответ: здесь будет ошибка компиляции на втором вызове. Для него подходили бы 3, 4 и 2 перегрузки в порядке приоритета. Но 3 нет, а следующая наиболее подходящая перегрузка удалена. Удаленные функции участвуют в разрешении перегрузки, поэтому компилятор решит, что мы хотим вызвать удаленную форму, и запретит нам это делать.