Грокаем C++ – Telegram
Грокаем C++
9.37K subscribers
45 photos
1 video
3 files
578 links
Два сеньора C++ - Владимир и Денис - отныне ваши гиды в этом дремучем мире плюсов.

По всем вопросам (+ реклама) @ninjatelegramm

Менеджер: @Spiral_Yuri
Реклама: https://telega.in/c/grokaemcpp
Мы на TGstat: https://tgstat.ru/channel/@grokaemcpp/stat
Download Telegram
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
🔥29👍124🤯4
Совмещаем ссылочные и 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
17👍11🔥9❤‍🔥21
Всем привет!
Давайте проведем небольшой опрос, насколько опытные плюсовики у нас тут присутствуют. Так мы сможем более адекватно подстраивать контент под аудиторию.
Как вы оцениваете свой уровень владения С++?
Anonymous Poll
32%
С++ меня избивает. Beginner
15%
Синяки все еще есть, но я уже работаю. Junior
22%
Все вроде понимаю, но нихрена не понятно. Middle
17%
Виден свет в конце тоннеля. Middle+
11%
Написал себе нунчаки на С++. Senior
4%
Чемпион мира С++, народный артист России и Чечено-Ингушетии, человек признанный, авторитетный, мэтр
🤣57❤‍🔥863😢1
Мини-квизы

Сейчас пойдет пачка мини-квизов на проверку того, как хорошо вы понимаете выбор 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();
}
🔥83👍31
Какой результат попытки компиляции и запуска кода выше?
Anonymous Poll
19%
Ошибка компиляции
71%
Call on const lvalue reference\nCall on rvalue reference
10%
Call on const rvalue reference\nCall on const lvalue reference
🔥31👍1
Второй пошел
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();
}
🔥72
Каков результат попытки компиляции и запуска кода выше?
Anonymous Poll
16%
ошибка компиляции
65%
Call on const lvalue reference\nCall on const rvalue reference
19%
Call on const lvalue reference\nCall on const lvalue reference
🔥1
Третий пошел
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();
}
🔥91👍1
Каков результат попытки компиляции и запуска кода выше?
Anonymous Poll
66%
ошибка компиляции
34%
Call on const lvalue reference\nCall on const lvalue reference
👍2🔥1
Ответы на мини-квизы

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 нет, а следующая наиболее подходящая перегрузка удалена. Удаленные функции участвуют в разрешении перегрузки, поэтому компилятор решит, что мы хотим вызвать удаленную форму, и запретит нам это делать.
🔥24👍1021
Мини-квизы

Сегодня будет вторая и последняя пачка мини-квизов на тему перегрузки методов 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();
}
👍6🔥43👏21
Каков результат попытки компиляции и запуска кода выше?
Anonymous Quiz
26%
ошибка компиляции
21%
Call on const lvalue reference\nCall on const lvalue reference
53%
Call on lvalue reference\nCall on const lvalue reference
🔥62👍21🤓1
Второй пошел

struct SomeClass {
// void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1

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

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

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

int main() {
SomeClass lvalue;
lvalue.foo();
SomeClass{}.foo();
}
🔥4👍321
Каков результат попытки компиляции и запуска кода выше?
Anonymous Quiz
54%
ошибка компиляции
46%
Call on const lvalue reference\nCall on const rvalue reference
🔥4👍321
Третий пошел

struct SomeClass {
// void foo() & {std::cout << "Call on lvalue reference" << std::endl;} //1
void foo() const & {std::cout << "Call on const lvalue reference" << std::endl;} //2
void foo() && {std::cout << "Call on rvalue reference" << std::endl;} //3
// void foo() const && {std::cout << "Call on const rvalue reference" << std::endl;} //4
};

int main() {
SomeClass lvalue;
lvalue.foo();
const_cast<const SomeClass&&>(lvalue).foo();
}
👍4🔥321
Каков результат попытки компиляции и запуска кода выше?
Anonymous Quiz
35%
ошибка компиляции
28%
Call on const lvalue reference\nCall on rvalue reference
37%
Call on const lvalue reference\nCall on const lvalue reference
🔥8👍42❤‍🔥1
Кейсы применения ref-qualified методов
#опытным

В нескольких предыдущих постах мы говорили про ref-qualified методы и как компилятор выбирает правильную перегрузку. Эта фича многим незнакома и сходу не очень понятно, где ее можно использовать. Давайте сегодня чуть подробнее поговорим о том, где они могут быть реально полезны, чтобы вы вдохновились и использовали такую перегрузку методов чаще.

Разработка библиотек. Довольно очевидно, что разработчикам всяких библиотек нужно учитывать примерно все сценарии использования их классов. Пользователи(безумные) могут скастить объект к константной правой ссылке и методы класса должны работать корректно. Тут очень важно, чтобы тип возвращаемого значения методов соответствовал типу объекта. Пример:

template <typename T>
class optional {

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

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

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

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


Если объект временный, то возвращаем правую ссылку на мувнутый ресурс. Если объект lvalue, то возвращаем обычную ссылку.

Форсить ограничения на методы. Если у вас методы возвращают левые ссылки(константные и неконстантные), то неплохо бы их пометить &, чтобы эти методы могли вызываться только у именованных объектов. Ведь если получить ссылку на внутренний ресурс временного объекта, то временный объект уничтожится, а вы останетесь с разбитым корытом висячей ссылкой. Спасибо @d7d1cd за кейс)

struct Vector {
int & operator[](size_t index) & { // notice & after arguments
return vec[index];
}
std::vector<int> vec;
};

Vector v;
v.vec = {1, 2, 3, 4};
v[1]; // ok
Vector{{1, 2, 3, 4}}[1]; // compile error


Также прикрепляю ссылочку на быстрый ответ из блога стандарта С++ посвященный этому кейсу.


Оптимизации. Иногда для определенных ссылочных типов мы можем оптимизировать какой-то метод. Например, в С++23 ввели rvalue reference перегрузку для метода substr класса std::basic_string. Мы знаем, что метод substr формирует новую строку, копируя туда рэндж из оригинальной строки. С++23 теперь сделал так, чтобы при вызове метода substr у правых ссылок объект подстроки тырил данные у оригинальной строки и фактически формировался из ее внутреннего буфера. Более подробно можно почитать в пропоузале.

Также, если вы возвращаете из метода легковесный объект, то в перегрузке для rvalue ссылок вы можете возвращать объект по значению. Так вы избавляетесь от избыточной ссылочной семантики и индирекции и , возможно, улучшаете перформанс. Ведь маленькие типы быстрее передавать и возвращать именно по значению:

struct Vector {
int operator[](size_t index) && { // notice & after arguments
return vec[index];
}
std::vector<int> vec;
};


В общем, в каждом конкретном случае оптимизировать можно по-разному.

Так что ref-qualified методы - это прекрасный инструмент тонкой настройки в руках профессионалов.

Be useful. Stay cool.

#cppcore #optimization #cpp23
20👍9🔥82❤‍🔥2
Перегружаем деструктор
#новичкам

Мы знаем, что методы класса можно перегружать, как обычные фукнции. Мы также поняли, что можно перегружать методы так, чтобы они отдельно работали для rvalue и lvalue ссылок. Можно даже перегружать конструкторы класса, чтобы они создавали объект из разных данных.

Но можно ли перегружать деструктор класса?

Резонный вопрос, деструктор - это такой же метод и такая же функция, почему бы его и не перегрузить.

По поводу дополнительных параметров деструктора.

Деструкторы стековых переменных вызываются неявно при выходе из скоупа. В языке просто нет инструментов, чтобы сообщить компилятору, как надо удалить объект. Способ только один. Удаление объектов, аллоцированных на стеке, ничем не должно идейно отличаться от удаления автоматических переменных. Поэтому и операторы delete и delete[] не принимают никаких аргументов.

Единственный вариант остается - это передавать дополнительные параметры при явном вызове деструктора. Однако кейсы применимости явного вызова деструктора и так сильно ограничены. Добавлять в стандарт перегрузку деструкторов, чтобы на этом строилась какая-то логика - излишне. И если вам уж захотелось построить какую-то логику на удалении, то можно ее вынести в статический метод destroy.

Ну а вообще. Задача деструктора - освободить ресурсы класса. Для конкретного класса набор его ресурсов определен на этапе компиляции. И есть всего один способ корректно освободить ресурс: вызвать delete, закрыть сокет или вызвать деструктор. И этот способ определен самим ресурсом.

Нет никакой опциональной логики при освобождении ресурсов в деструкторе. Вне зависимости от типа объекта и его ссылочности, данные внутри него выглядят одинаково. А значит и деструктор должен делать свою работу единообразно.

Не то, чтобы сильно полезный пост. У новичков иногда возникают такие вопросы. Но в принципе иногда нужно задумываться над такими, казалось бы, привычными вещами, чтобы глубже понимать инструменты, с которыми мы работаем.

Have a deeper understanding. Stay cool.

#memory #cppcore
27👍15🔥7😁41
auto аргументы функций
#опытным

Проследим историю с возможностью объявлять аргументы функций, как auto.

До С++14 у нас были только шаблонные параметры в функциях и лямбда выражения, без возможности передавать в них значения разных типов

Начиная с С++14, мы можем объявлять параметры лямбда выражения auto и передавать туда значения разных типов:

auto print = [](auto& x){std::cout << x << std::endl;};
print(42);
print(3.14);


Это круто повысило вариативность лямбд, предоставив им некоторые плюшки шаблонов.

У обычных функции, тем не менее, так и остались обычные шаблонные параметры.

Но! Начиная с С++20, параметры обычных функций можно также объявлять auto:

void sum(auto a, auto b)
{
    auto result = a + b;
    std::cout << a << " + " << b << " = " << result << std::endl;
}

sum(1, 3);
sum(3.14, 42);
sum(std::string("123"), std::string("456));
// OUTPUT:
// 1 + 3 = 4
// 3.14 + 42 = 45.14
// 123 + 456 = 123456


Если для лямбд это было необходимым решением из-за того, что их не хотели делать шаблонными(хотя в С++20 их уже можно делать такими), то auto параметры обычных функций призваны немного упростить шаблонную логику там, где не нужно использовать непосредственно тип шаблонного параметра. Так сказать, шаблоны на чилле и расслабоне.

Осталось только добавить, что параметры auto работают по принципу выведения типов для шаблонов, а не по принципу выведения типов auto переменных.

История небольшая, но становится понятно, что С++ все больше уходит в неявную типизацию. С одной стороны это хорошо, проще писать код и не задумываться над типами. С другой стороны, чтобы этим пользоваться на высоком уровне, нужно знать всякие маленькие нюансики, которых становится все больше и больше.

Кому нравится, тот обрадуется и будет пользоваться. Кому не нравится, может писать в стиле С++03 и все будет у него прекрасно.

Hide unused details. Stay cool.

#cpp11 #cpp14 #cpp20 #template
🔥31👍163👎2❤‍🔥11
Как думаете, нужен ли С++ стандартный сборщик мусора?
👎103🤣37😁14👍102
Проблемы ref-qualified методов
#опытным

Мы разобрали, что перегрузки методов по ссылочным типам объектов могут быть полезными в разных контекстах. Они могут использовать как в совокупности для достижения универсальности в обработке объектов, или точечно для тонкой настройки-подкрутки функциональности

Но один из примеров в том посте выбивается из общей массы. Еще раз посмотрим на него:

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

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

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

// you sure are by this point
constexpr T const&& value() const&& {
if (has_value()) {
return std::move(this->m_value);
}
throw bad_optional_access();
}
// ...
};


Это примерно то, как метод value класса std::variant был введен в стандарт С++17.
Мягко говоря, есть ощущение, что код дублируется. А если не считать мува, то вообще квадруплицируется.

Это вот стандартная штука, когда функции отличаются немного и их нельзя объединить в одну.

В таких случаях обычно помогают шаблоны. А учитывая, что у нас для левых ссылок нет мува, а для правых - есть, очень сильно напрашиваются универсальные ссылки и шаблонный std::forward.

Но тут шаблон вообще никак не вписывается. Методы же не принимают даже никаких аргументов. Какой шаблонный параметр сюда вообще вписывается?

Ну вообще говоря, методы принимают неявный аргумент this....

To be continued.

Intrigue people. Stay cool.

#cppcore
🔥21👍741