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
帖子存档
Каков результат попытки компиляции и запуска кода выше?
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

​​Когда мы вынуждены явно использовать new #опытным Сырые указатели - фуфуфу, бееее. Это не вкусно, мы такое не едим. new expression возвращает сырой указатель на объект. Соотвественно, мы должны максимально избегать явного использования new. У нас все-таки умные указатели и функции std::make_* довольно давно завезли. Однако все-таки есть кейсы, когда мы просто вынуждены использовать new явно: 👉🏿 std::make_unique не может в кастомные делитеры. Если хотите создать уникальный указатель со своим удалителем - придется использовать new.
auto ptr = std::unique_ptr<int, void()(int)>(new int(42), [](int* p) {
    delete p;
    std::cout << "Custom deleter called!\n";
});
👉🏿 Приватный конструктор у класса. Странно вообще пытаться создать объект такого класса, но не торопитесь. Приватный конструктор может быть нужен, чтобы оставить только один легальный способ создания объекта - фабричную функцию Create. Она возвращает уникальный указатель на объект и обычно является статическим членом класса. Функция Create имеет доступ к приватным методам, поэтому может вызвать конструктор. Но вот std::make_unique ничего не знает о приватных методах класса и не сможет создать объект. Придется использовать new.
struct Class {
  static std::unique_ptr<Class> Create() {
    // return std::make_shared<Class>(); // It will fail.
    return std::unique_ptr<Class>(new Class);
  }
private:
  Class() {}
}
👉🏿 Жизнь без 20-го стандарта. До 20-го стандарта вы не могли создать объект POD класса без указания фигурных скобок. Но именно так и делает std::make_unique. То есть вот так нельзя делать в С++17:
struct MyStruct {
  int a, b, c;
};
auto ptr = std::make_unique<MyStruct>(1, 2, 3); // Will fail C++17
auto ptr = std::unique_ptr<MyStruct>(new MyStruct{1, 2, 3}); // Norm
Но можно в С++20. Так что тем, кто необновился, придется использовать new. В целом, все. Если что забыл - накидайте в комменты. Но помимо этого, администрация этого канала не рекомендует в домашних и рабочих условиях явно вызывать new. Это может привести к потери конечности(отстрелу ноги). Stay safe. Stay cool. #cppcore #memory #cpp20 #cpp17

Ты умеешь кодить, создавать телеграм-ботов, знаешь много теории, есть потенциал, но не знаешь где применить свои навыки и нач
Ты умеешь кодить, создавать телеграм-ботов, знаешь много теории, есть потенциал, но не знаешь где применить свои навыки и начать работать? Тогда тебе стоит подать заявку в FUNDAMENT, Это новый проект готовый приступать работать. Мы ищем программистов и сотрудников что бы запускаться! Заходи в @ruFUNDAMENT и отправляй заявку (рассматриваем все)

​​emplace_back vs push_back #новичкам Раз уж такая масленица пошла, расскажу про весь сыр-бор с методами вектора(да и не только вектора). В последовательные контейнеры можно запихнуть данные в конец двумя способами: метод push_back и метод emplace_back.
template< class... Args >  
reference emplace_back( Args&&... args ); // returns ref to created element

void push_back( const T& value );
void push_back( T&& value );
По сигнатуре видно, что они предназначены немного для разного. Начнем со сложного. emplace_back принимает пакет параметров. Эти параметры предполагаются как аргументы конструктора хранимого типа T. Реализован он примерно так:
template <typename... Args>
    reference emplace_back(Args&&... args) {
        if (size == capacity) grow();
        return *new (start + size++) T(std::forward<Args>(args)...);
    }
Если надо, то расширяемся и делаем placement new на участке памяти для нового объекта, попутно используя perfect forwarding для передачи аргументов в конструктор. Вот тут кстати те самые круглые скобки используются, которые не давали pod типам нормально конструироваться. push_back принимает ссылку на уже готовый объект. То есть объект должен быть создан до входа в метод. И на основе этого значения уже конструируется объект в контейнере. В простейшем случае push_back вызывает внутри себя emplace_back:
void push_back(T&& value) {
  emplace_back(std::move(value));
}
Чтобы вызвать пуш бэк нужно вызвать 2 конструктора: от аргументов и copy|move. Для emplace_back же нужен только один конструктор - от аргументов. То есть emplace_back банально эффективнее, чем push_back. Для случаев, когда мы почему-то не можем создать объект внутри emplace_back(POD типы и < С++20) мы его создаем снаружи и копируем/муваем внутрь. Тогда эффективности двух методов одинаковая. Получается, что emplace_back в любом случае не менее эффективнее, чем push_back. Именно поэтому нужно всегда предпочитать использовать emplace_back. Be just better. Stay cool. #STL #memory