fa
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 روز
آرشیو پست ها
C++ Russia 2025 — конференция для опытных разработчиков на C++ 🧑‍💻 13 марта онлайн 📍 20–21 марта в Москве в отеле «МонАрх»
C++ Russia 2025 — конференция для опытных разработчиков на C++ 🧑‍💻 13 марта онлайн 📍 20–21 марта в Москве в отеле «МонАрх» Вас ждут 39 выступлений от 47 спикеров. Без поверхностных рассуждений — только техническая конкретика и примеры из реальных проектов. Доклады, на которые стоит обратить внимание: → Антон Полухин — [Не]очевидные оптимизации и паттерны из userver → Евгений Ерохин — Branch prediction, или Откуда процессор берет производительность → Константин Владимиров — Каша из топора: модули в C++, проблемы и решения → Алексей Веселовский — LLVM MemProf и методы профилирования памяти Все выступления — на сайте. Кроме докладов будут дискуссии, нетворкинг, активности и розыгрыши от партнеров. 🎟 Купить билеты Если оплачиваете самостоятельно — промокод GROKAEMCPP дает скидку 15% на билеты «Для частных лиц». Но выгоднее будет поучаствовать за счет компании. В этой статье есть аргументы, почему это будет выгодно не только вам, но и работодателю. Реклама. ООО «Джуг Ру Груп». ИНН 7801341446

​​Фиксим проблему с конструированием POD типов #опытным В прошлом посте говорили о том, что тяжело POD типам работать с функциями, которые внутри себя вызывают new. Клятые скобки! Это очевидные недоработки стандарта. Которые взяли и пофиксили в С++20! Теперь объекты POD типов можно создавать как через фигурные скобки, так и через круглые. Хотя небольшая разница есть. Но это уже супердетали:
struct A {  
  int a;  
  int&& r;  
};  
  
int f();  
int n = 10;  
  
A a1{1, f()};               // OK, lifetime is extended  
A a2(1, f());               // well-formed, but dangling reference  
A a3{1.0, 1};               // error: narrowing conversion  
A a4(1.0, 1);               // well-formed, but dangling reference  
A a5(1.0, std::move(n));    // OK
Теперь можно использовать все преимущества метода emplace_back и писать такой код:
std::vector<Point> vec;
vec.emplace_back(1, 2, 3);
Вроде мелочь, а раньше это выбивало из колеи. Правило almost always use emplace_back здесь не работало. Давайте добавлю еще чуть больше контекста(и своей боли) про emplace_back vs push_back. Что было до с++20. push_back спокойно переваривал следующий код:
std::vector<Point> vec;
vec.push_back({1, 2, 3});
Он принимает аргументом уже сконструированный объект нужного типа. И компилятору достаточно знаний о типе вектора и фирурных скобок, чтобы понять, как конструируется объект. Как видите здесь нет явного указания типа. И это работало до 20-х плюсов и было причиной делать исключения для правила с emplace_back'ом. Сравните:
std::vector<Point> vec;
vec.push_back({1, 2, 3});
vec.emplace_back(Point{1, 2, 3});
Больше букав! А если название структуры длинное? Вот вот. Тут либо нужно было отходить от правила везде использования emplace_back, либо постоянно вписывать конструктор в аргументы вызова emplace_back, либо определять совершенно тривиальные и от этого не несущие полезной логики конструкторы. Благо теперь наши пальцы сильнее защищены от стачивания, что не может не радовать! Кстати, с С++20 и сам термин POD стал помечаться устаревшим, теперь его заменили более тонкие типы TrivialType, ScalarType, и StandardLayoutType. Ну и функции std::make_* тоже работают как надо с простыми структурами. Enjoy small things. Stay cool. #cppcore #cpp20

Ограничения в конструировании POD типов #опытным Довольно часто приходится работать с plain old data типами. Они не имеют никаких специальных методов и конструкторов, это просто структуры с полями. Например:
struct Point {
  int x;
  int y;
  int z;
}
Создают объекты таких типов с помощью синтаксиса универсальной инициализации через фигурные скобки {}:
Point p{1, 2, 3};
Point p(1, 2, 3); // Wrong
При этом через круглые скобки создать объект такого класса нельзя. С повсеместным использованием универсальной инициализации с точки зрения код стайла создание таких объектов даже не выбивается из общего кода. Но все-таки у этого есть проблемы. Часто такие структурки хочется хранить в каком-нибудь векторе. Для добавления в вектор можно использовать push_back и emplace_back. Использование emplace_back выгоднее по перформансу, поэтому нужно обычно использовать именно этот метод. Но с POD типами он работает хреново.
std::vector<Point> vec;
vec.emplace_back(1, 2, 3); // Error!
Вот так вы написать не можете. emplace_back под капотом использует создание объекта с помощью new именно через круглые скобки. А как мы помним, так инициализировать POD типы нельзя. Поэтому приходится в этих случаях явно конструировать объект и вызывать мув-конструктор:
vec.emplace_back(Point{1, 2, 3});
что убивает преимущества emplace_back перед push_back. Либо можно использовать непросредственно push_back. Та же проблемы и в использовании функций std::make_*. Под капотом они тоже используют new с круглыми скобками и просто невозможно нормально использовать эти функции. Приходится явно вызывать new с фигурными скобками, что усугубляет проблему:
std::unique_ptr<Point>(new Point{1, 2, 3}); 
Конечно кейс с emplace_back намного чаще встречается в практике. POD типам не нужны фабличные методы и, обычно, контроль времени жизни. В общем, не жизнь, а страдания. Однако есть свет в конце тоннеля! Но об этом в следующий раз. See the light. Stay cool. #cppcore

RAID, бэкапы, репликация, WORM — если это часть вашей работы, добро пожаловать в BAUM. Здесь вы найдете полезные материалы, н
RAID, бэкапы, репликация, WORM — если это часть вашей работы, добро пожаловать в BAUM. Здесь вы найдете полезные материалы, новости индустрии, понятные объяснения о технологиях и процессах. Ну и, конечно же, шутки про работу в IT. Присоединяйтесь, если хотите разбираться в СХД так же хорошо, как в мемах.

​​Почему мы везде не используем nothrow new? #опытным В прошлый раз мы обсудили, что существует форма оператора new, которая не возбуждает исключений, а вместо этого при ошибке возвращает нулевой указатель. Однако я почему-то уверен, что большинство из вас впервые увидели эту форму. Почему же ее практически нигде не используют? 1️⃣ В современных плюсах вообще не часто можно увидеть прямой вызов new. Контейнеры и функции helper'ы std::make_* инкапсулируют в себе аллокации. Внутри них вызывается обычный бросающий new. Только в очень специфических кейсах явный вызов new оправдан. Поэтому пул примеров в принципе очень небольшой. 2️⃣ Представьте, что у вас закончилась память и nothrow new вернул вас nullptr. Можете ли вы локально обработать ошибку недостатка памяти? 99.9%, что нет. Поэтому вы будете вести эту ошибку по всему стеку вызовов до того места, где ее возможно обработать. То есть весь проект должен быть построен с учетом возможности возврата ошибки и постоянной проверкой этих ошибок. 3️⃣ И все же есть те люди, которых устраивает такая форма проекта с возвратом ошибки из функции и постоянной ее проверкой. Но если немного подумать, то выяснится, что очень часто ошибку недостатка памяти вы примерно никак не сможете обработать, кроме как напишите об этом в лог и завершите приложение. То есть, шо словите вы std::bad_alloc, шо просто напишите об этом в лог - разница не большая. Раз разница небольшая, мы можем обработать ошибку только на определенном слое приложения, а исключения предоставляют возможность централизованной их обработки, то давайте только там и поставим эту обработку. В одном единственном месте. Скорее всего это будет где-то в функции main или в главной функции потока. Сейчас может начаться холивар "исключения vs объекты ошибок". Но вряд ли можно отрицать, что работать с bad_alloc исключением банально проще, чем обрабатывать nullptr. Именно поэтому вы скорее всего вообще не увидите nothrow new. Handle problems easily. Stay cool. #cppcore

C++ — мощный инструмент, но с ним нужно правильно работать Хотите писать на C++, но настройка окружения кажется головной боль
C++ — мощный инструмент, но с ним нужно правильно работать Хотите писать на C++, но настройка окружения кажется головной болью? ◽️ Компилятор не видит файлы? ◽️Дебаг работает через раз? ◽️VSCode — просто красивый редактор? 💡 Приходите на открытый вебинар «Готовим рабочее место: C++ + VSCode»! 📅 5 марта в 20:00 (мск) Мы разберём: ◽️Как настроить VSCode для C++: компиляторы, расширения, дебаг ◽️Как запускать программы без ошибок и ручной магии ◽️Как сделать среду разработки удобной и эффективной 🎯 А ещё всем участникам подарим скидку на топовый курс «C++ Developer»! 🔗 Регистрация: https://otus.pw/6LYpx/ Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576

Еще один способ сделать new небросающим #опытным Дело в том, что new - не совсем ответственнен за поведение при недостатке памяти. По плюсовой традиции тут можно кастомизировать почти все. Встречайте: std::new_handler. Это такой typedef'чик:
typedef void (*new_handler)();
И алиас для функций, которые отвечают за обработку ситуации нехватки памяти. И вызываются они аллоцирующими функциями operator new и operator new[]. Чтобы установить такой хэдлер используется функция std::set_new_handler:
std::new_handler set_new_handler(std::new_handler new_p) noexcept;
Она делает new_p новой глобальной функцией нового обработчика и возвращает ранее установленный обработчик. Предполагаемое назначение хэндлера - одна из трех вещей: 1️⃣ Сделать больше памяти доступной. (За гранью фантастики) 2️⃣ Залогировать проблему и завершить программу (например, вызовом std::terminate) +. Если вам плевать на graceful shutdown, то ок. 3️⃣ Кинуть исключение типа std::bad_alloc или его отпрысков с какой-нибудь кастомной надписью и/или залогировать проблему. Раз std::new_handler - алиас на указатель функции, то что будет, если мы передадим nullptr в качестве хэндлера? На самом деле это и делается по умолчанию. При старте программы nullptr выставляется в качестве хэндлера. При невозможности выделить память operator new вызывает std::get_new_handler. Если указатель на функцию нулевой, то в этом случае new ведет себя дефолтно - просто кидает исключение std::bad_alloc. В ином случае вызывает хэндлер. С обработчиками есть один нюанс. Если обработчик успешно заканчивает работу(то есть из него не вызван terminate или не брошено исключение), то operator new повторяет ранее неудачную попытку выделения и снова вызывает обработчик, если выделение снова не удается. Чтобы закончить цикл, new-handler может вызвать std::set_new_handler(nullptr): если после неудачной попытки new обнаружит, что std::get_new_handler возвращает нулевое значение указателя и выбросит std::bad_alloc. Примерно так это работает:
void handler()
{
    std::cout << "Memory allocation failed, terminating\n";
    std::set_new_handler(nullptr);
}
 
int main()
{
    std::set_new_handler(handler);
    try
    {
        while (true)
        {
            new int [1000'000'000ul] ();
        }
    }
    catch (const std::bad_alloc& e)
    {
        std::cout << e.what() << '\n';
    }
}
Пользуйтесь фичей, чтобы оставлять на проде девопсерам сообщение: "А че так мало памяти в конфиге пода прописано, мм?" Спасибо @Nikseas_314 за идею для поста) Customize your tools. Stay cool. #memory #cppcore

​​Безопасный для исключений new #опытным Большинство приложений не могут физически жить без исключений. Даже если вы проектируете все свои классы так, чтобы они не возбуждали исключений, то от одного вида exception'ов вы вряд ли уйдете. Дело в том, что оператор new может бросить std::bad_alloc - исключение, которое говорит о том, что система не может выделить нам столько ресурсов, сколько было запрошено. Однако мы можем заставить new быть небросающим! Надо лишь в скобках передать ему политику std::nothow. Синтаксис очень похож на placement new. Это в принципе он и есть, просто у new есть перегрузка, которая принимает политику вместо указателя.
MyClass * p1 = new MyClass; // привычное использование
MyClass * p2 = new (std::nothrow) MyClass; // небросающая версия
В первом случае при недостатке памяти выброситься исключение. А во втором случае - вернется нулевой указатель. Прям как в std::malloc. Так что, если хотите избавиться от исключений - вот вам еще один инструмент. #cppcore

Готовы к карьерному рывку? Станьте C++ Software Engineer в YADRO всего за 3 дня 🚀 Прямо сейчас российская технологическая ко
Готовы к карьерному рывку? Станьте C++ Software Engineer в YADRO всего за 3 дня 🚀 Прямо сейчас российская технологическая компания YADRO проводит SPRINT OFFER для C++ Software Engineer. 🔵 Чтобы присоединиться к команде Telecom: •‎ Оставьте заявку на сайте до 9 марта. •‎ Пройдите скрининг с рекрутером. А после — техническое и менеджерское интервью. YADRO ждёт кандидатов сразу в два направления: Команда Telecom Platform разрабатывает полное платформенное решение для телекоммуникационных систем. На его основе строятся самые современные узлы сотовых сетей LTE- и GSM-стандартов — например, базовые станции и системы управления. Инженеры разработки базовой станции LTE/GSM и 5G Core создают высоконагруженные системы, обеспечивающие связь как критически важных, так и новых поколений. А также разрабатывают надёжное и масштабируемое ПО для мобильных сетей. Присоединяйтесь к инженерному сообществу с опытными специалистами, чтобы напрямую влиять на продукт и реализовывать амбициозные проекты! Оставляйте заявку до 9 марта и станьте частью команды YADRO. Все подробности — по ссылке.

Почему так смешно?😂😂😂
Почему так смешно?😂😂😂

​​new vs malloc Чем отличаются new и malloc? Один из популярных вопросов на собеседованиях, которые проверяет, насколько хорошо вы знакомы с тонкостями работы с памятью в С/С++. Поэтому давайте сегодня это обсудим. Не совсем корректно, наверное сравнивать фичи двух разных языков с разными доминантными парадигмами программирования. Но раз в стандарте есть std::malloc, а new тоже выделяет память, то можно попробовать. 👉🏿 new expression помимо аллокации памяти вызывает конструктор объекта. std::malloc только выделяет память. 👉🏿 std::malloc - совсем не типобезопасный. Он возвращает void * без какого-либо признака типа. Придется явно кастовать результат к нужному типу. new в свою очередь возвращает типизированный указатель. 👉🏿 При ошибке выделения памяти new бросает исключение std::bad_alloc, в то время как std::malloc возвращает NULL. Соответственно нужны разные способы обработки ошибочных ситуаций. 👉🏿 Поведение new может быть переопределено внутри кастомных классов, поведение std::malloc - неизменно. 👉🏿 Если вам не нужно конструирование объекта, то просто вызывайте operator new. Он делает то же самое, что и std::malloc(потенциально вызывает его внутри себя). 👉🏿 Для new не нужно вручную высчитывать количество нужных байт. То есть мы не лезем на низкий уровень. Мы заботимся только типе данных, количестве объектов и об аргументах конструктора. 👉🏿 new плохо работает с реаллокациями. Нужно выделить новый сторадж, скопировать туда данные и вызвать delete. В то время, как malloc имеет функцию-партнера realloc, которая может изменить размер существующего куска памяти более эффективно, чем последовательность new-memcpy-delete. Однако они имеют одну неочевидную схожесть. Нужно стараться по максимуму избегать их явного вызова. Давно придумали умные указатели и контейнеры, которые позволяют максимально освободить разработчика от обязанности ручного управления памятью. Мы все же современные плюсовики. Поэтому в большинстве случаев, вам не нужны будут прямые вызовы этих функций. В более редких случаях(например кастомные аллокаторы) можно явно использовать new. Ну и в совсем редких случаях(нужда в реаллокации памяти или работа с сишным кодом) можно использовать malloc. Control your memory. Stay cool. #cppcore #interview

Все грани new #опытным Не каждый знает, что в плюсах new - это как медаль, только лучше. У медали 2 стороны, а у new целых 3! Сейчас со всем разберемся. То, что наиболее часто используется, называется new expression. Это выражение делает 2 вещи последовательно: пытается в начале выделить подходящий объем памяти, а потом пытается сконструировать либо один объект, либо массив объектов в уже аллоцированной памяти. Возвращает либо указатель на объект, либо указатель на начало массива. Выглядит оно так:
 // creates dynamic object of type int with value equal to 42
int* p_triv = new int(42);
 // created an array of 42 dynamic objects of type int with values equal to zero
int* p_triv_arr = new int[42];

struct String {std::string str};

 // creates dynamic object of custom type String using aggregate initialization
String* p_obj = new String{"qwerty"};
 // created an array of 5 dynamic objects of custom type String with default initialization
String* p_obj = new String[5];
Эта штука делает 2 дела одновременно. А что, если мне не нужно выполнять сразу 2 этапа? Что, если мне нужна только аллокация памяти? За это отвечает operator new. Это оператор делает примерно то же самое, что и malloc. То есть выделяет кусок памяти заданного размера. Выражение new именно этот оператор и вызывает, когда ему нужно выделить память. Но его можно вызвать и как обычную функцию, а также перегружать для конкретного класса:
// class-specific allocation functions
struct X
{
    static void* operator new(std::size_t count)
    {
        std::cout << "custom new for size " << count << '\n';
        // explicit call to operator new
        return ::operator new(count);
    }
 
    static void* operator new[](std::size_t count)
    {
        std::cout << "custom new[] for size " << count << '\n';
        return ::operator new;
    }
};
 
int main()
{
    X* p1 = new X;
    delete p1;
    X* p2 = new X[10];
    delete[] p2;
}
Но что, если у меня уже есть выделенная память и я хочу на ней создать объект? Допустим, я не хочу использовать кучу и у меня есть массивчик на стеке, который я хочу переиспользовать для хранения разных объектов, потенциально разных типов. Тогда мне нужен инструмент, который позволяет только вызывать конструктор на готовой памяти. Для этого есть placement new. Это тот же самый new expression, только для этого есть свой синтаксис. Сразу после new в скобках вы передаете указатель на область памяти, достаточной для создания объекта.
alignas(T) unsigned char buf[sizeof(T)];
// after new in parentheses we specified location of future object
T* tptr = new(buf) T; 
// You must manually call the object's destructor
tptr->~T(); 
В этом случае кстати вызывается специальная перегрузка operator new:
void* operator new  (std::size_t count, void* ptr);
Однако она не делает ничего полезного и просто возвращает второй аргумент наружу. Вот так по разному в С++ можно использовать new. Главное - не запутаться! Have a lot of sides. Stay cool. #cppcore #memory

​​Неинициализированные переменные Все мы знаем, что это плохо, ужасно, это проделки Сатаны и заговор евреев. Но нет-нет да и используем их. Сегодня кратко приведу еще один довод в пользу того, чтобы выкинуть из своей картины мира неинициализированные переменные. Кейс из ревью. Вот у вас есть примерно такая функция и примерно так ее можно вызвать:
std::optional<SomeType> WaitForSomething(bool& success) {
  SomeType result;
  if (!SomeOperation()) {
    // error
    return std::nullopt;
  }
  if (condition) {
    //fill result
  }
  success = (condition == 1);
  return result;
}

bool success;
auto obj = WaitForSomething(success);
if (!obj) {
  // handle error
}
if (success) {
  // do something
}
Вроде нормальная функция, в случае ошибки не возвращает объекта, в случае успешного условия заполняет как-то объект, в случае неуспеха оставляет его дефолтным. И заполняется output параметр, сигнализирующий об наступлении условия. Здесь success заполняется всегда вне зависимости, наступит условие или нет. И вроде как очень безопасно туда передать неинициализированный bool. Какая же разница? success в любом случае инициализируется в функции. А вот приходит другой программист и изменяет эту функцию примерно так:
std::optional<SomeType> WaitForSomething(bool& success) {
  SomeType result;
  if (!SomeOperation()) {
    // error
    return std::nullopt;
  }
  while (condition1 && condition2) {
    //fill result
    if (condition3)
      success = true;
  }
  return result;
}
По логике реального кода было все четко. Кроме того, что теперь в ветке, где не выполняются одновременно condition1, condition2 и condition3 success остается тем же, что и передали в функцию. Так конечно не нужно делать. Это хоть чуть-чуть, но изменяет логику поведения функции. Но я хочу тут другой момент затронуть. Теперь при передаче неинициализированной success результат функции может быть дефолтовым, а success == true, так как в него изначально был записан true. В реальности привело к довольно долгому дебагу. Но если бы изначально мы создавали бы инициализированную в ложь переменную
bool success = false;
то этой боли можно было бы избежать. А изменение функциональности самой функции выявят тесты. Вывод: не используйте неинициализированные переменные! И включите уже соответствующую опцию компилятора, которая ругается на них ошибкой. Define your value. Stay cool. #cppcore #goodpractice

​​Пингвинячимся На плюсах писать и линукс не знать - вы в своем уме?. Современная backend С++ разработка жестко привязана к этой системе. Отсутствие навыков работы с linux - гарантия ограниченных карьерных возможностей и технической экспертизы. Но ладно, так и быть, вам я расскажу одно средство, которое поможет вам прокачаться в пингвине. Это канал LinuxCamp. Его ведет реальный системный разработчик, который поможет тебе освоить Linux и программирование на профессиональном уровне! А еще он плюсовик, как и все мы тут. У него вы найдете уникальные гайды по администрированию Linux и подробные статьи о внутреннем устройстве операционных систем. А также крутые техники сочетания С++ и linux в разработке Не первый и точно не последний раз рекомендую этот канал. Так что подписывайтесь на LinuxCamp и становитесь частью сообщества истинных пингвинячих гуру!

Как думаете, кто и когда отправит С++ на покой? Или он будет жить вечно?
Как думаете, кто и когда отправит С++ на покой? Или он будет жить вечно?

​​Почему приватные методы находятся в описании класса? Публичные, защищенные методы - понятно, они нужны либо для пользователей класса, либо для наследников. Поля - понятно, они влияют на размер объекта, а клиенты должны уметь правильно аллоцировать нужное количество памяти под объект. Ну а приватные-то методы зачем раскрывать? Зачем их помещать внутрь класса и делать видимыми для клиентского кода? Ну во-первых. Представим, что этого ограничения нет. Тогда все приватные методы объявлялись и определялись бы в файле реализации, который никто не видит. Но если я могу в файле реализации определить приватный метод, то кто угодно может это сделать. Это будет давать рандомным людям прямой доступ к закрытым полям класса. Если мы завершили определение класса, то у нас нет способов как-то пометить именно наши файлы, как "благословленные владельцем". Есть всего лишь юниты трансляции и они равнозначны. Получается, что единственный способ сказать, что вот этот набор методов официально одобрен создателем - это объявить его в описании класса. В С++ мы имеем прямой доступ к памяти, а значит, мы легко можем поменять байтики для приватных полей и все. Или даже создать тип, с таким же описанием, только с дополнительным методом. Кастануть недоступный тип к своему и вуаля, вы можете как хотите вертеть объектом во всех удобных вам позах. Но это уже хаки, мы такого не одобряем. Не используя манипуляции с памятью, мы не сможем добавлять рандомную функциональность в рандомный класс. А во-вторых, оказывается в С++ приватные методы участвуют в разрешении перегрузок(внезапно). В целом, так оно и должно быть. Никто не мешает вам определить публичный метод и перегрузить его приватным методом. Проблема(или фича) в том, что этап разрешения перегрузок стоит перед проверкой модификатора доступа. То есть даже если метод приватный и его никто не должен увидеть, он все равно участвует в разрешении перегрузок наряду со всеми остальными методами. Поэтому каждый клиент должен видеть полный набор приватных методов. Об этом мы уже говорили в контексте pimpl идиомы. В чем прикол того, что приватные методы участвуют в разрешении перегрузок? Давайте снова представим, что такого правила нет. И вот у нас есть две перегрузки, одна приватная для double, другая публичная для int. И перегрузка с double всегда отбрасывалась бы только лишь по причине того, что она приватная. Тогда мы легко можем вызвать публичную функцию с дробным числом 1.5 и нам ничего не будет. Оно просто неявно приведется к int и все на этом. А теперь посмотрим, что будет, если мы поменяем модификатор приватной перегрузки на публичный? Ничего не упадет, НО! наш вызов метода с аргументом 1.5 теперь пойдет в другую функцию! То есть изменится поведение объекта. Комитет хотел избежать таких ситуаций, поэтому ввели такое вот ограничение. Наверное, причина не одна. Но им этой одной было достаточно. Однако, такой протокол поведения влечет за собой различные сайд-эффекты.
struct DumbClass {
    void DumbFunction(int param) {
        // i do nothing because i’m dumb
    }
private:
    void DumbFunction(double param) = delete;
};

int main() {
    DumbClass{}.DumbFunction(1.0);
}
// error: use of deleted function ‘void DumbClass::DumbFunction(double)’
Я могу удалить приватную перегрузку публичного метода, например какую мы обсуждали выше. И вызвать публичный метод опять с дробным числом. Но компилятор на меня наругается, потому что я попытался вызвать удаленную перегрузку метода. Хотя она вообще-то объявлена, как приватная! А то, что я ее удалил - это детали реализации. Получается раскрытие деталей реализации. Не знаю, как оценивать все перечисленные причины. Напишите свое мнение в комментариях. Stay aware. Stay cool. #design #howitworks #cppcore #hardcore

Ваш код тормозит под нагрузкой? Не справляется с задачами параллельного вычисления? 💡 Приходите на открытый урок по многопот
Ваш код тормозит под нагрузкой? Не справляется с задачами параллельного вычисления? 💡 Приходите на открытый урок по многопоточному программированию C++! ▫️Вместе напишем многопоточную реализацию задачи подсчёта суммы чисел. ▫️Разберём примитивы синхронизации. ▫️Ускорим код с помощью стандартной библиотеки. 🎯 Вы начнёте осваивать многопоточное программирование и сделаете первый шаг к позициям middle+ в C++! А ещё получите скидку на большое обучение «C++ Developer». 📅 Успейте записаться на открытый вебинар 25 февраля в 20:00 (мск)! 🔗 Регистрируйтесь прямо сейчас и узнайте, как эффективно управлять потоками в C++: https://otus.pw/Gf9B0/ Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576

​​Как вызвать методы класса напрямую через vtable? #опытным Всем нам как С++ разработчикам интересно, как все устроено внутри всего. Вызвано это профессиональной деформацией или врожденным позывом, но у нас у всех это есть. Поэтому сейчас заглянем немного под капот... Полиморфизм - там штука, которая сделала ООП и ОО дизайн такими мощными и широкоиспользуемыми инструментами. За счет чего он реализован в С++? Наиболее распространенное решение - vtable. Таблица виртуальных методов. Не буду сильно погружаться, но в сущности, это массив, который хранит указатели на методы класса. И сейчас мы попробуем вызвать метод класса напрямую через этот массив используя низкоуровневые преобразования.
struct Base {
    virtual void func1() {
        std::cout << i+1<< std::endl;
    }
    virtual void func2() {
        std::cout << i+2 << std::endl;
    }
};

struct Derived: Base {
    void func1() override {
        std::cout << i+3 << std::endl;
    }
    void func2() override {
        std::cout << i+4 << std::endl;
    }
};

int main() {
    auto basePtr = std::unique_ptr<Base>(new Derived);
    uint64_t ** pVtable = (uint64_t **)basePtr.get();
    printf("Vtable: %p\n", pVtable);
    printf("First Entry of VTable: %p\n", (void*)pVtable[0][0]);
    printf("Second Entry of VTable: %p\n", (void*)pVtable[0][1]);

    using VoidFunc = void (*) (void*);
    VoidFunc firstMethod = (VoidFunc) pVtable[0][0];
    firstMethod(basePtr.get());
    VoidFunc secondMethod = (VoidFunc) pVtable[0][1];
    secondMethod(basePtr.get());
}
Для начала нам нужна иерархия объектов. У родителя и ребенка по 2 метода, каждый выводит определенный инкремент целочисленного поля класса. Теперь создаем умный указатель родительского класса, который содержит объект ребенка. Далее нужно найти указатель на vtable. Обычно он всегда лежит в первых 8 байтах объектов полиморфных классов. Так как у нас двухуровневая косвенность (указатель на объект, а в нем указатель на таблицу), мы объявляем двойной указатель на 8-мибайтный инт и он и будет нашим vtable_ptr. Почему uint64_t? Да на самом деле косвенность трехуровненая, потому что в ячейках vtable лежат указатели на методы, а указатель легко представить в виде 8-байтного числа. Самое сложное позади, мы нашли все, что нужно. Осталось только кастануть инты, лежащие в ячейках vtable к указателям на функцию правильного типа. И вуаля. Готово. Возможный вывод:
Vtable: 0x5a7841237eb0
First Entry of VTable: 0x5a781fd28ae2
Second Entry of VTable: 0x5a781fd28b22
3
4
Где это знание нужно? Ну вообще в принципе неплохо разбираться в том, как работает полиморфизм в С++. У всего есть свои особенности и в какой-то необычной ситуации эти знания вам могут помочь пофиксить багу. Знание внутреннего устройства инструмента позволить вам понимать его преимущества и недостатки. Возможно в проектах с высокими требованиями к производительности(слабое железо как в embedded или супер-ультра-гипер low latency как в hft), вам предстоит выбирать между динамическим полиморфизмом и статическим полиморфизмом aka шаблонами. Чтобы сделать выбор качественно и аргументировать его, нужно знание базы. А как говорится, пока сам не потрогаешь ручками - не поймешь. Только не нужно так писать в продовом коде! А то будет, как на картинке внизу. Stay hardwared. Stay cool.

​​No new line Оказывается, чтобы получить неопределенное поведение даже необязательно писать какой-то плохой код. Достаточно просто не добавить перенос строки в конце подключаемого файла! Небольшой пример:
Файлик foo.hpp:

// I love code
// I love C++<no newline>

Файлик bar.cpp:

#include "foo.hpp"
#include "baz.hpp"
А теперь вспоминаем, что препроцессор вставляет все содержимое хэдера на место инклюда И(!) не вставляет после него символ конца строки. То есть спокойно может получится следующее:
// I love code
// I love C++#include "baz.hpp"
То есть включение baz.hpp может быть полностью заэкранировано. Учитывая, сколько всего препроцессор может делать с кодом, комбинации вариантов развития событий могут быть абсолютно разными. Стандарт нам говорит:
... If a source file that is not empty does not end in a new-line character,
 or ends in a new-line character immediately preceded by a backslash
 character before any such splicing takes place, the behavior is undefined.
Так что ub без кода - вполне существующая вещь. Или уже нет? На самом деле приведенная цитата была из стандарта 2003 года. С++11 пофиксил эту проблему и обязал препроцессоры вставлять new line в конце подключаемых файлов:
A source file that is not empty and that does not end in a new-line character, 
or that ends in a new-line character immediately preceded by a backslash 
character before any such splicing takes place, shall be processed 
as if an additional new-line character were appended to the file.
Так что теперь проблемы нет. Решил написать об этом, просто потому что очень весело, что в плюсах можно было такими неочевидными способами отстрелить себе конечность. Ну и хорошо, что стандарт все-таки не только новую функциональность вводит, а фиксит вот такие вот недоразумения. Fix your flaws. Stay cool. #compiler