Грокаем C++
Ir al canal en Telegram
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов. По всем вопросам (+ реклама) @ninjatelegramm Менеджер: @Spiral_Yuri Реклама: https://telega.in/c/grokaemcpp Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Mostrar más9 362
Suscriptores
+424 horas
+27 días
+3730 días
Archivo de publicaciones
9 362
Мини-квизы
Сегодня будет вторая и последняя пачка мини-квизов на тему перегрузки методов 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();
}9 362
Ответы на мини-квизы
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 нет, а следующая наиболее подходящая перегрузка удалена. Удаленные функции участвуют в разрешении перегрузки, поэтому компилятор решит, что мы хотим вызвать удаленную форму, и запретит нам это делать.9 362
Ответ: здесь будет ошибка компиляции на втором вызове. Для него подходили бы 3, 4 и 2 перегрузки в порядке приоритета. Но 3 нет, а следующая наиболее подходящая перегрузка удалена. Удаленные функции участвуют в разрешении перегрузки, поэтому компилятор решит, что мы хотим вызвать удаленную форму, и запретит нам это делать.
9 362
Третий пошел
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();
}9 362
Ответ:
Вызовутся методы 2 и 4 по порядку. rvalue reference может приводиться к константной левой ссылке, но также может приводиться к const rvalue ref. Второе преобразование достигается меньшими усилиями, поэтому вызовется 4 метод.
9 362
Второй пошел
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();
}9 362
Ответ: Вызовутся методы 2 и 3 по порядку. За неимением неконстантной перегрузки для левых ссылок, остается только константная перегрузка для первого вызова.Во втором случае rvalue reference может приводиться к константной левой ссылке, но в этот раз есть более подходящие кандидаты на перегрузку. И самым подходящим будет 3 метод.
9 362
Мини-квизы
Сейчас пойдет пачка мини-квизов на проверку того, как хорошо вы понимаете выбор 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();
}9 362
sysconf 2025 — конференция по системному программированию от создателей C++ Russia и DevOops
📅 22 марта в Москве + онлайн
Вас ждут 22 спикера, 19 докладов и Lightning Talks. Разберемся на реальных кейсах, как устроены многопоточные рантаймы, компиляторы и низкоуровневые оптимизации. Подробнее — в расписании.
🎟 Билеты уже на сайте. Если оплачиваете самостоятельно — промокод
GROKAEMCPP дает скидку 15%.
Реклама. ООО «Джуг Ру Груп». ИНН 78013414469 362
Всем привет!
Давайте проведем небольшой опрос, насколько опытные плюсовики у нас тут присутствуют. Так мы сможем более адекватно подстраивать контент под аудиторию.
Как вы оцениваете свой уровень владения С++?
9 362
Совмещаем ссылочные и 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
9 362
Открытый вебинар «Архитектурные решения в Backend-разработке»
📚На вебинаре вы узнаете:
1. Как выбрать архитектурный стиль в зависимости от требований к производительности, масштабируемости и отказоустойчивости.
2. Микросервисы vs монолит: плюсы и минусы, примеры смены подходов.
3. Событийно-ориентированная архитектура: когда и как ее использовать, основные принципы и инструменты.
4. CQRS и источник событий: как управлять данными в сложных условиях.
5. Как проектировать отказоустойчивые серверные системы, включая прерывание автоматического выключателя, повторную попытку, переборку и другие схемы.
6. Ключевые ошибки при проектировании конструкции и как их избежать.
👨💻Кому будет полезно?
+ Разработчикам Backend
+ Разработчикам FullStack
+ Системным аналитикам
⏰ 18 марта в 20:00 (мск).
🆓Бесплатно. Вебинар в рамках курса «Software Architect»
👉Записывайтесь:https://otus.pw/qoYh4/?erid=2W5zFHYn66t
Реклама. ООО «Отус онлайн-образование», ОГРН 1177746618576
#реклама
О рекламодателе
9 362
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 #design9 362
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 #cppcore9 362
Всем привет!👋 Мы занимаемся разработкой приложения, которое соединяет людей.
Мы хотим понять, что действительно нужно людям, какие функции могут сделать приложение полезным, удобным и приятным.
Опрос займет всего 5–7 минут. 🕒
Все ответы анонимны, а ваши идеи и пожелания помогут нам создать приложение, которое станет вашим лучшим помощником в поиске общения и интересных знакомств.
Заранее благодарим за помощь!
🎁В конце вас будет ждать приятный подарок😊
Пройти опрос ⬅️
9 362
Проблемы С-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