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

Грокаем C++

Відкрити в Telegram

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

Показати більше
9 362
Підписники
+424 години
+27 днів
+3730 днів
Архів дописів
Мини-квизы Сегодня будет вторая и последняя пачка мини-квизов на тему перегрузки методов 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 нет, а следующая наиболее подходящая перегрузка удалена. Удаленные функции участвуют в разрешении перегрузки, поэтому компилятор решит, что мы хотим вызвать удаленную форму, и запретит нам это делать.

Каков результат попытки компиляции и запуска кода выше?
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 && = delete; //4
};

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

Ответ: Вызовутся методы 2 и 4 по порядку. rvalue reference может приводиться к константной левой ссылке, но также может приводиться к const rvalue ref. Второе преобразование достигается меньшими усилиями, поэтому вызовется 4 метод.

Каков результат попытки компиляции и запуска кода выше?
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();
  std::move(lvalue).foo();
}

Ответ: Вызовутся методы 2 и 3 по порядку. За неимением неконстантной перегрузки для левых ссылок, остается только константная перегрузка для первого вызова.Во втором случае rvalue reference может приводиться к константной левой ссылке, но в этот раз есть более подходящие кандидаты на перегрузку. И самым подходящим будет 3 метод.

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

Мини-квизы Сейчас пойдет пачка мини-квизов на проверку того, как хорошо вы понимаете выбор ref-qualified перегрузок. Для меньшей запутанности я буду оставлять все перегрузки в тексте кода, но закомменчу ненужные в каждом случае. Также подключены все необходимые инклюды и компиляция происходит под 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();
}

sysconf 2025 — конференция по системному программированию от создателей C++ Russia и DevOops 📅 22 марта в Москве + онлайн Ва
sysconf 2025 — конференция по системному программированию от создателей C++ Russia и DevOops 📅 22 марта в Москве + онлайн Вас ждут 22 спикера, 19 докладов и Lightning Talks. Разберемся на реальных кейсах, как устроены многопоточные рантаймы, компиляторы и низкоуровневые оптимизации. Подробнее — в расписании. 🎟 Билеты уже на сайте. Если оплачиваете самостоятельно — промокод GROKAEMCPP дает скидку 15%. Реклама. ООО «Джуг Ру Груп». ИНН 7801341446

Всем привет! Давайте проведем небольшой опрос, насколько опытные плюсовики у нас тут присутствуют. Так мы сможем более адекватно подстраивать контент под аудиторию. Как вы оцениваете свой уровень владения С++?
Anonymous voting

​​Совмещаем ссылочные и cv квалификаторы методов Далеко не всегда очевидно, какая именно функция является лучшим кандидатом для перегрузки в том или ином случае. Да, когда это только & или &&, то все довольно просто. Но что получается, когда мы добавим константность методам? Компилятор будет выбирать подходящую перегрузку по определенному алгоритму.
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
};
Дело в том, что все правые ссылки могут каститься к const lvalue reference, а левые к правым - ни при каких обстоятельствах. Неконстантные типы могут каститься к константным. И никак наоборот. Исходя из этих правил компилятор и разрешает перегрузки. Пометил методы из примера порядковыми номерами, чтобы потом было легче делать отсылки
SomeClass lvalue;
const SomeClass const_lvalue;

lvalue.foo();
const_lvalue.foo();
В случае lvalue.foo() вызываем перегрузку для неконстантной левой ссылки. Левые ссылки не могут приводиться к правым. Поэтому методы под номерами 3 и 4 не подходят. Неконстантные типы могут приводиться к константным. Поэтому нам подходят и 1, и 2 методы. Однако для вызова 2 придется сделать шажок - добавить константности, а для вызова первого - ничего. Поэтому выбирается первая перегрузка. В случае const_lvalue.foo() вызываем перегрузку для константной левой ссылки. 3 и 4 также откидываем по тем же причинам. Однако в этот раз нам подходит лишь 2 перегрузка, так как константный тип не может быть приведен к неконстантному. Итого вывод получится такой:
Call on lvalue reference
Call on const lvalue reference
Для rvalue ссылки нехитрыми рассуждениями можно прийти к правильному ответу о вызываемой перегрузке
SomeClass.foo();
// OUTPUT
// Call on rvalue reference
Тут довольно все просто. Но самая жесть начинается, когда у нас нет какой-то перегрузки/перегрузок из полного набора. На следующей неделе забомбардирую вас мини-квизами на эту тему. Посмотрим, как хорошо вы шарите за overload resolution. Choose the right way. Stay cool. #cppcore

Открытый вебинар «Архитектурные решения в Backend-разработке» 📚На вебинаре вы узнаете: 1. Как выбрать архитектурный стиль в
Открытый вебинар «Архитектурные решения в Backend-разработке» 📚На вебинаре вы узнаете: 1. Как выбрать архитектурный стиль в зависимости от требований к производительности, масштабируемости и отказоустойчивости. 2. Микросервисы vs монолит: плюсы и минусы, примеры смены подходов.  3. Событийно-ориентированная архитектура: когда и как ее использовать, основные принципы и инструменты. 4. CQRS и источник событий: как управлять данными в сложных условиях. 5. Как проектировать отказоустойчивые серверные системы, включая прерывание автоматического выключателя, повторную попытку, переборку и другие схемы. 6. Ключевые ошибки при проектировании конструкции и как их избежать. 👨‍💻Кому будет полезно? + Разработчикам Backend + Разработчикам FullStack + Системным аналитикам ⏰ 18 марта в 20:00 (мск). 🆓Бесплатно. Вебинар в рамках курса «Software Architect» 👉Записывайтесь:https://otus.pw/qoYh4/?erid=2W5zFHYn66t Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576 #реклама О рекламодателе

ref-qualified методы #опытным В С++ можно довольно интересными способами перегружать методы класса. Один из самых малоизвестных и малоиспользуемых - помечать методы квалификатором ссылочности. Чтобы было понятнее. Примерно все знают, что бывают константные и неконстантные методы.
struct SomeClass {
  void foo() {std::cout << "Non-const member function" << std::endl;}
  void foo() const {std::cout << "Const member function" << std::endl;}
};

SomeClass nonconst_obj;
const SomeClass const_obj;
nonconst_obj.foo();
const_obj.foo();

// OUTPUT
// Non-const member function
// Const member function
Константные объекты могут вызывать только константные методы. Поэтому мы можем перегрузить метод класса, чтобы он мог работать с константными объектами. В примере видно что у константного объекта вызывается константная перегрузка. По аналогии с cv-квалификаторами методов начиная с С++11 существуют ref-квалификаторы. Мы можем перегрузить метод так, чтобы он мог раздельно обрабатывать левые и правые ссылки.
struct SomeClass {
  void foo() & {std::cout << "Call on lvalue reference" << std::endl;}
  void foo() && {std::cout << "Call on rvalue reference" << std::endl;}
};

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

// OUTPUT
// Call on lvalue reference
// Call on rvalue reference
Обратим внимание на сигнатуру методов. Метки ссылочных квалификаторов ожидаемо принимают форму одного и двух амперсандов, по аналогии с типами данных левых и правых сслылок соотвественно. Располагаются они после скобок с аргументами метода. Работают они примерно также, как вы и ожидаете. lvalue-ref перегрузка вызывается на именованном объекте, rvalue-ref перегрузка - на временном. Зачем это придумано? Здесь на самом деле большие параллели с cv-квалификацией методов. Допустим, у вас класс - это какая-то коллекция. И вы хотите давать пользователям доступ к элементам этой коллекции через оператор[]. Для неконстантных объектов удобно возвращать ссылку. А вот для константных возвращение ссылки - потенциальное нарушение неизменяемости объекта. Поэтому в таких случаях константный оператор может возвращать элемент по значению. Также и с ссылочностью. В каких-то случаях оптимально или просто необходимо использовать для правых ссылок иную логику метода. Подробнее об этом чуде-юде будем разбираться в следующих постах. Stay flexible. Stay cool. #cpp11 #design

std::array #новичкам На самом деле, это очень-очень тонкая обертка над сишными массивами. Вот несколько упрощенная реализация, которая тем не менее полностью передает смысл и необходимые особенности.
template<typename T, size_t N>
struct array
{
  T& operator[](size_t index) {
    return _data[index];
  }
  T& front() {
    return _data[0];
  }
  T& back() {
    return _data[N];
  }
  T* data() {
    return _data;
  }
  constexpr size_t size() const {
    return N;
  }
  constexpr bool empty() const {
    return N == 0;
  }
// еще const версии перечисленных методов и некоторые другие методы и алиасы типов
private:
  T _data[N];
};
За счет использования шаблоного типа нижележащего массива std::array может работать с любыми встроенными и кастомными типами. А за счет нетипового шаблонного аргумента N, std::array знает количество элементов, которое в нем находится, еще на этапе компиляции!. И не нужно ничего вычислять! Достаточно вызвать метод size(), который буквально constexpr.
std::array arr{1, 2, 3};
static_assert(arr.size() == 3); // здесь не упадем
Обычно удобство абстракций идет вместе с платой за это удобство. Но это не тот случай. За счет того, что все методы std::array буквально занимают одну строчку, компилятору очень удобно инлайнить их код в caller'ов. Это приводит к тому, что низкоуровневый ассемблерный код при работе с C-style массивами и std::array практически всегда идентичен. std::array не мимикрирует ни под какой другой тип, так как это кастомный класс. Внутри себя он также инкапсулирует все необходимые операторы сравнения. В операциях с ним нет никакой путаницы, потому что они явно определены конкретно для этого класса. Его можно спокойно принимать в функцию по ссылке и по значению, а также указывать в качестве возвращаемого значения. И все это с привычной семантикой.
template<typename T, size_t N>
std::array<T, N> double_elements(std::array<T, N>& array) {
  std::array<T, N> result = array;
  for (auto& elem: result)
    elem = elem * 2;
  return result;
}
Если мы создаем массив в локальной области функции(99% случаев), то элементы std::array располагаются непрерывно на стеке. И размер std::array равен размеру C-style массива с одинаковым количеством элементов и их типом.
int c_arr[N]; 
std::arrray<int, N>  cpp_arr;
sizeof(cpp_arr) == cpp_arr.size() * sizeof(int) == 
sizeof(c_arr) == N * sizeof(int) == std::size(c_arr) * sizeof(int);
Итак. Выходит, что std::array идентичен сишному массиву по внутреннему устройству и произодительности, да еще и решает все проблемы неумелого использования последнего. Идеальный высокоуровневый инструмент! Так что std::array должен быть первым выбором в случае необходимости создания массива с длиной, известной на этапе компиляции. У PVS-Studio есть прекрасная статья на этот счет. Fix your flaws. Stay cool. #STL #cppcore

☝️ Это проект нашего подписчика, так что давайте его поддержим)

Всем привет!👋 Мы занимаемся разработкой приложения, которое соединяет людей. Мы хотим понять, что действительно нужно людям, какие функции могут сделать приложение полезным, удобным и приятным. Опрос займет всего 5–7 минут. 🕒 Все ответы анонимны, а ваши идеи и пожелания помогут нам создать приложение, которое станет вашим лучшим помощником в поиске общения и интересных знакомств. Заранее благодарим за помощь! 🎁В конце вас будет ждать приятный подарок😊 Пройти опрос ⬅️

​​Проблемы С-style массивов #опытным В наследство от языка С С++ достались статические массивы. Так называемые С-style массивы. Это проверенные средства языка, успешно решающие свои задачи. Но у них есть серьезные недостатки, которые в основном связаны с низкоуровневостью этого инструмента. Давайте кратко повторим, что такое C-style массив. Это непрерывная последовательность элементов одного типа и память под них выделяется на стеке и автоматически освобождается при выходе из функции. Определяется сишный массив вот так:
// создаем массив на 5 элементов, 
// которые по умолчанию инициализируются нулями
int arr1[5]; 

// создаем массив и предоставляем набор элементов, с помощью 
// которых компилятор вычисляет длину массива и инициализирует элементы
int arr2[] = {1, 2, 3, 4, 5}; 
Размер памяти, занимаемый массивом равняется количеству его элементов помноженному на размер типа данных:
constexpr size_t array_size = 5;
int arr[array_size];
sizeof(arr) == array_size * sizeof(int); // true
Соответственно, для получения количества элементов массива, нужно поделить sizeof от массива на размер типа данных, которые он хранит.
auto array_size = sizeof(arr) / sizeof(Type);
В чем же его недостатки? ❗️ Массивы нельзя сравнивать напрямую, а только поэлементно. Напрямую сравниваются указатели на первый элемент.
int arr1[] = {0, 1, 2, 3};
int arr2[] = {0, 1, 2, 3};

// ложь так как сраниваются указатели, 
// а они разные для разных объектов
arr1 == arr2; 
❗️ Мимикрирование под массивы разрешает странную семантику с условиями и арифметическими операциями.
// создаем пустую строку в виде массива
char arr[] = ""; 

// условие будет всегда true, хотя мы создали пустую строку
if (arr); 

// разрешается, но зачем? что значит прибавить к массиву число?
arr + 1; 
❗️В С разрешены [массивы переменной длины](https://t.me/grokaemcpp/56) на уровне стандарта. И синтаксис у них ровно такой же, как и у статических массивов, только при его создании размер указывается не константой, а переменной. В С++ это не стандартная фича, а расширения компилятора. То есть нельзя писать кроссплатформенный код с использованием массивов переменной длины. Но за счет идентичного синтаксиса очень легко спутать один вид массива с другим и похерить переменосимость. ❗️ От синтаксиса сочетания функций и массивов хочется вырвать себе глаза, закрыть компьютер и уйти жить в лес:
int foo(int arr[4]); 
// На самом деле такая сигнатура полностью эквивалентна int foo(int * arr), 
// что позволяет принимать в функцию массив любой длины и указатели.
// В С++ нет синтаксиса приема массива по значению.

void foo(int (&arr)[4]); // зато есть синтаксис приема массива по ссылке

// Нормального синтаксиса для возврата массива из функции также не завезли. 
// Вот воркэраунды.
int get_array()[10];
auto get_array() -> int[10];
❗️Массив не инкапсулирует в себе свой размер. Его нужно всегда вычислять, как мы говорили в начале. ❗️Из-за сложности синтаксиса, вы скорее всего захотите обрабатывать массивы с помощью функций с похожей сигнатурой:
void foo(int * p, size_t size);
Это потенциально может привести к доступу за границы выделенной области, так как функция foo ничего не знает про то, какой реальный размер имеет область памяти, на которую указывает p. Она должна доверять программисту и переданному значению size. А программисту верить - себя не уважать. В общем, сишные массивы - это не объекты и не обладают преимуществами ООП и универсальной семантики для объектов в С++. Поэтому стандартная библиотека предоставляет нам инструмент, который решает все проблемы C-style массивов. Это контейнер std::array. О нем мы поговорим в следующий раз. Upgrade your tools. Stay cool. #cppcore #goodoldc