Результат попытки компиляции и запуска?
Anonymous Poll
15%
Ошибка компиляции
46%
Выведется 0
1%
Выведется 42
18%
Неопределенное поведение, остальное - домыслы
26%
Выведется рандомное число
Разница инициализаций
После вчерашнего поста у некоторых читателей мог возникнуть резонный вопрос. Почему глобальные переменные инициализируются автоматически, а локальные - нет? Не легче ли было установить какое-то одно правило для всех?
Единые правила - хорошая вещь. И как многие хорошие вещи, они чего-то стоят. А в С++ есть такой девиз: "мы не платим за то, что не используем". Мне не всегда нужно задавать значение переменной. Иногда меня это вообще не интересует. Я могу создать неинициализированную переменную и передать ее в функцию, где ей присвоится конкретное значение.
Чтобы ответить на вопрос из начала поста, давайте посмотрим, чем мы вообще платим за инициализацию в обоих случаях.
Рассмотрим локальные переменные.
В сущности, они являются просто набором байт на текущем фрейме стека. И программа интерпретирует эти байты, как наши локальные переменные.
Чтобы инициализировать локальную переменную, нужно положить нолик в каждый байтик, который ассоциирован с этой переменной. И так нужно делать каждый раз при каждом вызове функции. Итого стоимость инициализации: немножко кода на зануление памяти при каждом входе в скоуп переменной.
Теперь глобальные переменные
Они инициализируются всего один раз при старте программы. Соответственно, стоимость - немножко кода 1 раз при старте программы.
Причем обычно, когда мы говорим про какие-то затраты и перфоманс, мы говорим о времени, когда программа уже делает полезную работу. То есть инициализация глобальных переменных проходит в "бесплатное" с точки зрения производительности время.
Итого мы получаем, что предварительная установка значений глобальных переменных проходит для нас фактически бесплатно, а для локальных переменных мы тратимся на каждый вход в скоуп переменной.
Теперь представьте, что мы бы потребовали устанавливать валидное значение всегда. Это просто неэффективно. Да и не нужно.
Кстати, на самом деле zero-инициализация глобальных переменных может обходится нам действительно бесплатно. И никаких кавычек! Но об этом в следующем посте.
Be effective. Stay cool.
#cppcore #compiler
После вчерашнего поста у некоторых читателей мог возникнуть резонный вопрос. Почему глобальные переменные инициализируются автоматически, а локальные - нет? Не легче ли было установить какое-то одно правило для всех?
Единые правила - хорошая вещь. И как многие хорошие вещи, они чего-то стоят. А в С++ есть такой девиз: "мы не платим за то, что не используем". Мне не всегда нужно задавать значение переменной. Иногда меня это вообще не интересует. Я могу создать неинициализированную переменную и передать ее в функцию, где ей присвоится конкретное значение.
int i;
FillUpVariable(i);
Чтобы ответить на вопрос из начала поста, давайте посмотрим, чем мы вообще платим за инициализацию в обоих случаях.
Рассмотрим локальные переменные.
В сущности, они являются просто набором байт на текущем фрейме стека. И программа интерпретирует эти байты, как наши локальные переменные.
Чтобы инициализировать локальную переменную, нужно положить нолик в каждый байтик, который ассоциирован с этой переменной. И так нужно делать каждый раз при каждом вызове функции. Итого стоимость инициализации: немножко кода на зануление памяти при каждом входе в скоуп переменной.
Теперь глобальные переменные
Они инициализируются всего один раз при старте программы. Соответственно, стоимость - немножко кода 1 раз при старте программы.
Причем обычно, когда мы говорим про какие-то затраты и перфоманс, мы говорим о времени, когда программа уже делает полезную работу. То есть инициализация глобальных переменных проходит в "бесплатное" с точки зрения производительности время.
Итого мы получаем, что предварительная установка значений глобальных переменных проходит для нас фактически бесплатно, а для локальных переменных мы тратимся на каждый вход в скоуп переменной.
Теперь представьте, что мы бы потребовали устанавливать валидное значение всегда. Это просто неэффективно. Да и не нужно.
Кстати, на самом деле zero-инициализация глобальных переменных может обходится нам действительно бесплатно. И никаких кавычек! Но об этом в следующем посте.
Be effective. Stay cool.
#cppcore #compiler
Бесплатная zero-инициализация
Вчера я сказал, что иногда в самой программе может попросту отсутствовать код по занулению неинициализированных глобальных переменных. Сегодня разберем, за счет чего это может достигаться.
Во время старта программы ей необходимо выделить память под такие вещи, как стек, кучу, код самой программы и глобальные переменные. Память программе предоставляет операционная система. Ну и естественно, что в эту память раньше была записана какая-то информация. Вообще говоря, потенциально конфиденциальная. То есть раньше был какой-то процесс, который писал информацию в память, завершился, и теперь ее отдают другому процессу.
И что получается, наш новорожденный процесс может видеть какую-то конфиденциальную информацию? Это же большая уязвимость.
Может ли операционная система опираться на честность человека, написавшего код, или на компилятор, что кто-то из них останется приличным парнем и сам занулит всю выданную программе память? В большинстве случаев может. Но здесь очень важны исключения, которых быть не должно.
Поэтому ОС никому не доверяет и сама зануляет всю память, которую выдает новому процессу.
Компилятор/линкер при формировании бинарника собирает все неинициализированные переменные вместе в одну секцию с названием .bss.
Получается, при старте программы у ОС запрашивается память в том числе под секцию .bss, и эта память уже аллоцируется зануленной! И никакого кода не нужно, за нас все делает операционка.
Важное уточнение, что такое поведение наблюдается не у всех операционок. Да, все эти ваши винды, линуксы и прочие макоси зануляют память перед ее передачей другому процессу. Но для каких-нибудь микроконтроллеров это может быть неактуально и компилятор должен честно вставить код зануления для того, чтобы соблюсти требования стандарта.
В чате последние пару дней были бурные обсуждения того, что этого зануления может и не быть. Ну как бы, может и не быть. Только тогда компилятор будет противоречить стандарту. И пользоваться им можно на свой страх и риск.
Don't reveal secrets. Stay cool.
#OS #compiler #cppcore
Вчера я сказал, что иногда в самой программе может попросту отсутствовать код по занулению неинициализированных глобальных переменных. Сегодня разберем, за счет чего это может достигаться.
Во время старта программы ей необходимо выделить память под такие вещи, как стек, кучу, код самой программы и глобальные переменные. Память программе предоставляет операционная система. Ну и естественно, что в эту память раньше была записана какая-то информация. Вообще говоря, потенциально конфиденциальная. То есть раньше был какой-то процесс, который писал информацию в память, завершился, и теперь ее отдают другому процессу.
И что получается, наш новорожденный процесс может видеть какую-то конфиденциальную информацию? Это же большая уязвимость.
Может ли операционная система опираться на честность человека, написавшего код, или на компилятор, что кто-то из них останется приличным парнем и сам занулит всю выданную программе память? В большинстве случаев может. Но здесь очень важны исключения, которых быть не должно.
Поэтому ОС никому не доверяет и сама зануляет всю память, которую выдает новому процессу.
Компилятор/линкер при формировании бинарника собирает все неинициализированные переменные вместе в одну секцию с названием .bss.
Получается, при старте программы у ОС запрашивается память в том числе под секцию .bss, и эта память уже аллоцируется зануленной! И никакого кода не нужно, за нас все делает операционка.
Важное уточнение, что такое поведение наблюдается не у всех операционок. Да, все эти ваши винды, линуксы и прочие макоси зануляют память перед ее передачей другому процессу. Но для каких-нибудь микроконтроллеров это может быть неактуально и компилятор должен честно вставить код зануления для того, чтобы соблюсти требования стандарта.
В чате последние пару дней были бурные обсуждения того, что этого зануления может и не быть. Ну как бы, может и не быть. Только тогда компилятор будет противоречить стандарту. И пользоваться им можно на свой страх и риск.
Don't reveal secrets. Stay cool.
#OS #compiler #cppcore
Почему тогда локальные переменные не зануляются?
Вчера мы разобрали, что когда операционка выдает процессу память, она ее зануляет. Тогда получается, что сегмент глобальных данных автоматически заполнен нулями.
Но возникает вопрос: раз ОС такая молодец и зануляет всю память, то почему локальные переменные и куча заполнены мусором? Какие-то двойные стандарты.
Все на самом деле немножко сложнее.
Есть такое понятие, как "zero-fill on demand". Заполнение нулями по требованию.
Когда процесс запрашивает память под свои сегменты, стек и кучу, ОС на самом деле не дает ему реальные страницы памяти. А дает "виртуальные". То есть ничего не аллоцирует по факту. Такие страницы заполнены нулями.
Процесс может свободно читать эти страницы и будет действительно видеть там нули. Однако это не будет физической памятью. Как только процесс захочет что-то записать в нее, только тогда операционка разрождается, реально аллоцирует физическую страницу и копирует в нее содержимое той виртуальной страницы. То есть заполняет физическую нулями.
И так она делает один раз на каждую физическую страницу.
Вот как появляются нули в реальной памяти. Теперь почему они не остаются навсегда.
Дело в том, что процесс переиспользует свою память. Программа в течение всей своей жизни использует один и тот же стек и кучу.
Мы выделили маллоком массив байт, попользовали его и освободили. И эта память не вернулась операционке. Процесс может ее переиспользовать. Да, изначально, при попытке записи в эти байты, ОС выдавала зануленные страницы. Но после того, как мы ими попользовались, там уже лежат наши данные. И с точки зрения куска программы, которая в следующий раз получит эту память, там уже лежит "мусор". Но это просто данные из предыдущей аллокации.
Также и локальные переменные. Мы выполнили одну функцию, вернулись обратно, и выполняя следующую функцию, мы будем переиспользовать память стека под локальные переменные.
Именно поэтому кстати, мы можем очень легко получить доступ к данным, которые лежали на стеке ранее:
Возможный вывод такого кода:
Обратите внимание, что, вызывая функцию с переменной uninitialize в первый раз, мы получили мусор. Однако после вызова func1, где переменная инициализирована, в памяти стека на месте, где лежала initialize будет лежать число 10. Так как сигнатуры и содержимое функций в целом идентичны, то uninitialize во второй раз будет располагаться на том же самом месте, где и была переменная initialize. Соответственно, она будет содержать то же значение.
А учитывая, что до пользовательского кода выполняется некий "скрытый код", то даже в "начале" программы вы будете видеть на стеке мусор.
Reuse resources. Stay cool.
#OS #compiler
Вчера мы разобрали, что когда операционка выдает процессу память, она ее зануляет. Тогда получается, что сегмент глобальных данных автоматически заполнен нулями.
Но возникает вопрос: раз ОС такая молодец и зануляет всю память, то почему локальные переменные и куча заполнены мусором? Какие-то двойные стандарты.
Все на самом деле немножко сложнее.
Есть такое понятие, как "zero-fill on demand". Заполнение нулями по требованию.
Когда процесс запрашивает память под свои сегменты, стек и кучу, ОС на самом деле не дает ему реальные страницы памяти. А дает "виртуальные". То есть ничего не аллоцирует по факту. Такие страницы заполнены нулями.
Процесс может свободно читать эти страницы и будет действительно видеть там нули. Однако это не будет физической памятью. Как только процесс захочет что-то записать в нее, только тогда операционка разрождается, реально аллоцирует физическую страницу и копирует в нее содержимое той виртуальной страницы. То есть заполняет физическую нулями.
И так она делает один раз на каждую физическую страницу.
Вот как появляются нули в реальной памяти. Теперь почему они не остаются навсегда.
Дело в том, что процесс переиспользует свою память. Программа в течение всей своей жизни использует один и тот же стек и кучу.
Мы выделили маллоком массив байт, попользовали его и освободили. И эта память не вернулась операционке. Процесс может ее переиспользовать. Да, изначально, при попытке записи в эти байты, ОС выдавала зануленные страницы. Но после того, как мы ими попользовались, там уже лежат наши данные. И с точки зрения куска программы, которая в следующий раз получит эту память, там уже лежит "мусор". Но это просто данные из предыдущей аллокации.
Также и локальные переменные. Мы выполнили одну функцию, вернулись обратно, и выполняя следующую функцию, мы будем переиспользовать память стека под локальные переменные.
Именно поэтому кстати, мы можем очень легко получить доступ к данным, которые лежали на стеке ранее:
void fun1() {
int initialize = 10;
std::cout << initialize << std::endl;
}
void fun2() {
int uninitialize;
std::cout << uninitialize << std::endl;
}
int main() {
fun2();
fun1();
fun2();
}
Возможный вывод такого кода:
32760
10
10
Обратите внимание, что, вызывая функцию с переменной uninitialize в первый раз, мы получили мусор. Однако после вызова func1, где переменная инициализирована, в памяти стека на месте, где лежала initialize будет лежать число 10. Так как сигнатуры и содержимое функций в целом идентичны, то uninitialize во второй раз будет располагаться на том же самом месте, где и была переменная initialize. Соответственно, она будет содержать то же значение.
А учитывая, что до пользовательского кода выполняется некий "скрытый код", то даже в "начале" программы вы будете видеть на стеке мусор.
Reuse resources. Stay cool.
#OS #compiler
Программа без main?
Все мы знаем, что функция main - входная точка в программу. С нее начинается исполнение программы, если не считать глобальные переменные.
Без функции main программа просто не запустится.
Или нет?
Может быть мы можем что-нибудь нахимичить, чтобы, например, написать Hello, World без main?
Оказывается, можем. Однако, естественно, это все непереносимо. Но она на то и магия, что у разных магов свои заклинания.
Скомпилируем вот такую программу под gcc с флагом -nostartfiles:
И на консоли появится наша горячо-любимая надпись:
Для любителей поиграться с кодом вот вам ссылочка на годболт.
А вот что за такая функция _start и какой все-таки код выполняется до main, мы поговорим в следующий раз.
Make impossible things. Stay cool.
#fun #cppcore
Все мы знаем, что функция main - входная точка в программу. С нее начинается исполнение программы, если не считать глобальные переменные.
Без функции main программа просто не запустится.
Или нет?
Может быть мы можем что-нибудь нахимичить, чтобы, например, написать Hello, World без main?
Оказывается, можем. Однако, естественно, это все непереносимо. Но она на то и магия, что у разных магов свои заклинания.
Скомпилируем вот такую программу под gcc с флагом -nostartfiles:
#include <iostream>
int my_fun();
void _start()
{
int x = my_fun();
exit(x);
}
int my_fun()
{
std::cout << "Hello, World!\n";
return 0;
}
И на консоли появится наша горячо-любимая надпись:
Hello, World!
Для любителей поиграться с кодом вот вам ссылочка на годболт.
А вот что за такая функция _start и какой все-таки код выполняется до main, мы поговорим в следующий раз.
Make impossible things. Stay cool.
#fun #cppcore
std::cout
Кажется, что на начальном этапе становления про-с++-ером, вывод в использование конструкции:
воспринимается, как "штука, которая выводит текст на консоль".
Даже со временем картинка не до конца складывается и на вопрос "что такое std::cout?", многие плывут. Сегодня закроем этот вопрос.
В этой строчке мы вызываем такой оператор:
Получается, что std::cout - объект класса std::ostream. И ни какой-то там временный. Раз он принимается по левой ссылке, значит он уже где-то хранится в памяти.
Но мы же ничего не делаем для его создания? Откуда он взялся?
Мы говорили о том, что есть "невидимые" для нас вещи, которые происходят при старте программы. Так вот, это одна из таких вещей.
std::cout - глобальный объект типа std::ostream. За его создание отвечает класс std::ios_base::Init, инстанс которого явно или неявно определяется в библиотеке <iostream>.
Но это все слова. И новичкам будет достаточно этого. Но мы тут глубоко закапываемся, поэтому давайте закопаемся в код.
Полазаем по исходникам gcc. Ссылочки кликабельные для пытливых умов.
А в хэдэре iostream мы можем найти вот это:
Здесь определяются символы стандартных потоков и создается глобальная переменная класса ios_base::Init. Пойдемте тогда в конструктор:
Немножко разберем происходящее.
В условии проверяется ref_count, чтобы предотвратить повторную инициализацию. Так как не предполагается, что такие объекты, как cout будут удалены, они просто создаются через placement new с помощью инстансов stdio_sync_filebuf<char>. Это внутренний буфер для объектов потоков, который ассоциирован с "файлами" stdout, stdin, stderr. Буферы как раз и предназначены для получения/записи io данных.
Хорошо. Мы видим как и где создаются объекты. Но это же placement new. Для объектов уже должная быть подготовлена память для их размещения. Где же она?
В файлике globals_io.cc:
то есть, объекты - это пустые символьные массивы правильного размера и выравнивания.
Все это должно вам дать довольно полное представление, что такое стандартные потоки ввода-вывода.
#cppcore #compiler
Кажется, что на начальном этапе становления про-с++-ером, вывод в использование конструкции:
std::cout << "Print something in consol\n";
воспринимается, как "штука, которая выводит текст на консоль".
Даже со временем картинка не до конца складывается и на вопрос "что такое std::cout?", многие плывут. Сегодня закроем этот вопрос.
В этой строчке мы вызываем такой оператор:
std::ostream& operator<< (std::ostream& stream, const char * str)
Получается, что std::cout - объект класса std::ostream. И ни какой-то там временный. Раз он принимается по левой ссылке, значит он уже где-то хранится в памяти.
Но мы же ничего не делаем для его создания? Откуда он взялся?
Мы говорили о том, что есть "невидимые" для нас вещи, которые происходят при старте программы. Так вот, это одна из таких вещей.
std::cout - глобальный объект типа std::ostream. За его создание отвечает класс std::ios_base::Init, инстанс которого явно или неявно определяется в библиотеке <iostream>.
Но это все слова. И новичкам будет достаточно этого. Но мы тут глубоко закапываемся, поэтому давайте закопаемся в код.
Полазаем по исходникам gcc. Ссылочки кликабельные для пытливых умов.
А в хэдэре iostream мы можем найти вот это:
extern istream cin; ///< Linked to standard input
extern ostream cout; ///< Linked to standard output
extern ostream cerr; ///< Linked to standard error (unbuffered)
extern ostream clog; ///< Linked to standard error (buffered)
...
static ios_base::Init __ioinit;
Здесь определяются символы стандартных потоков и создается глобальная переменная класса ios_base::Init. Пойдемте тогда в конструктор:
ios_base::Init::Init()
{
if (__gnu_cxx::__exchange_and_add_dispatch(&_S_refcount, 1) == 0)
{
// Standard streams default to synced with "C" operations.
_S_synced_with_stdio = true;
new (&buf_cout_sync) stdio_sync_filebuf<char>(stdout);
new (&buf_cin_sync) stdio_sync_filebuf<char>(stdin);
new (&buf_cerr_sync) stdio_sync_filebuf<char>(stderr);
// The standard streams are constructed once only and never
// destroyed.
new (&cout) ostream(&buf_cout_sync);
new (&cin) istream(&buf_cin_sync);
new (&cerr) ostream(&buf_cerr_sync);
new (&clog) ostream(&buf_cerr_sync);
cin.tie(&cout);
cerr.setf(ios_base::unitbuf);
// _GLIBCXX_RESOLVE_LIB_DEFECTS
// 455. cerr::tie() and wcerr::tie() are overspecified.
cerr.tie(&cout);
...
__gnu_cxx::__atomic_add_dispatch(&_S_refcount, 1);
Немножко разберем происходящее.
В условии проверяется ref_count, чтобы предотвратить повторную инициализацию. Так как не предполагается, что такие объекты, как cout будут удалены, они просто создаются через placement new с помощью инстансов stdio_sync_filebuf<char>. Это внутренний буфер для объектов потоков, который ассоциирован с "файлами" stdout, stdin, stderr. Буферы как раз и предназначены для получения/записи io данных.
Хорошо. Мы видим как и где создаются объекты. Но это же placement new. Для объектов уже должная быть подготовлена память для их размещения. Где же она?
В файлике globals_io.cc:
// Standard stream objects.
// NB: Iff <iostream> is included, these definitions become wonky.
typedef char fake_istream[sizeof(istream)]
attribute ((aligned(alignof(istream))));
typedef char fake_ostream[sizeof(ostream)]
attribute ((aligned(alignof(ostream))));
fake_istream cin;
fake_ostream cout;
fake_ostream cerr;
fake_ostream clog;
то есть, объекты - это пустые символьные массивы правильного размера и выравнивания.
Все это должно вам дать довольно полное представление, что такое стандартные потоки ввода-вывода.
#cppcore #compiler
Линкуем массивы к объектам
Опытные читатели могли заметить кое-что странное в этом посте. И заметили кстати. Изначально cin, cout и тд определены, как простые массивы. А в iostream они уже становятся объектами потоков и линкуются как онные. То есть в одной единице трансляции
А в другой
Что за приколы такие? Почему массивы нормально линкуются на объекты кастомных классов?
В С++ кстати запрещены такие фокусы. Типы объявления и определения сущности должны совпадать.
Все потому что линкер особо не заботится о типах, выравнивании и даже особо о размерах объектов. То есть я буквально могу прилинковать объект одного кастомного класса к другому и мне никто никакого предупреждения не влепит. Такой код вполне нормально компилится и запускается:
На консоли появится "1 2". Но ни типы, ни размеры типов, ни выравнивания у объектов из объявления и определения не совпадают. Поэтому здесь явное UB.
Но в исходниках GCC так удачно сложилось, что массивы реально представляют собой идеальные сосуды для объектов io-потоков. На них даже сконструировали реальные объекты. Поэтому такие массивы можно интерпретировать как сами объекты.
Это, естественно, все непереносимо. Но поговорка "спички детям - не игрушка" подходит только для тех, кто плохо понимает, что делает. А разработчики компилятора явно не из этих ребят.
Take conscious risks. Stay cool.
#cppcore #compiler
Опытные читатели могли заметить кое-что странное в этом посте. И заметили кстати. Изначально cin, cout и тд определены, как простые массивы. А в iostream они уже становятся объектами потоков и линкуются как онные. То есть в одной единице трансляции
extern std::ostream cout;
extern std::istream cin;
...
А в другой
// Standard stream objects.
// NB: Iff <iostream> is included, these definitions become wonky.
typedef char fake_istream[sizeof(istream)]
attribute ((aligned(alignof(istream))));
typedef char fake_ostream[sizeof(ostream)]
attribute ((aligned(alignof(ostream))));
fake_istream cin;
fake_ostream cout;
fake_ostream cerr;
fake_ostream clog;
Что за приколы такие? Почему массивы нормально линкуются на объекты кастомных классов?
В С++ кстати запрещены такие фокусы. Типы объявления и определения сущности должны совпадать.
Все потому что линкер особо не заботится о типах, выравнивании и даже особо о размерах объектов. То есть я буквально могу прилинковать объект одного кастомного класса к другому и мне никто никакого предупреждения не влепит. Такой код вполне нормально компилится и запускается:
// header.hpp
#pragma once
struct TwoFields {
int a;
int b;
};
struct ThreeFields {
char a;
int b;
long long c;
};
// source.cpp
ThreeFields test = {1, 2, 3};
// main.cpp
#include <iostream>
#include "header.hpp"
extern TwoFields test;
int main() {
std::cout << test.a << " " << test.b << std::endl;
}
На консоли появится "1 2". Но ни типы, ни размеры типов, ни выравнивания у объектов из объявления и определения не совпадают. Поэтому здесь явное UB.
Но в исходниках GCC так удачно сложилось, что массивы реально представляют собой идеальные сосуды для объектов io-потоков. На них даже сконструировали реальные объекты. Поэтому такие массивы можно интерпретировать как сами объекты.
Это, естественно, все непереносимо. Но поговорка "спички детям - не игрушка" подходит только для тех, кто плохо понимает, что делает. А разработчики компилятора явно не из этих ребят.
Take conscious risks. Stay cool.
#cppcore #compiler
Квиз
Вчера в комментах наш подписчик @d7d1cd задал очень интересную задачку, которой мне захотелось с вами поделиться. Да, кто-то уже ее обсудил, но тем, кто не участвовал в дискуссии, тоже будет интересно проверить свои знания в #quiz.
Как всегда это бывает с плюсами, задачка не простая и сходу вгоняет в ступор. Но без паники! Вдох, выдохи мы опять играем в любимых, успокоили разум, подумали и ответили.
Подписчику спасибо за контент, а у меня для вас всего один вопрос. Как правильно вызвать конструктор у такого класса?
Challenge your knowledge. Stay cool.
Вчера в комментах наш подписчик @d7d1cd задал очень интересную задачку, которой мне захотелось с вами поделиться. Да, кто-то уже ее обсудил, но тем, кто не участвовал в дискуссии, тоже будет интересно проверить свои знания в #quiz.
Как всегда это бывает с плюсами, задачка не простая и сходу вгоняет в ступор. Но без паники! Вдох, выдох
Подписчику спасибо за контент, а у меня для вас всего один вопрос. Как правильно вызвать конструктор у такого класса?
struct Type {
template <typename>
Type() {}
};
Challenge your knowledge. Stay cool.
Как правильно вызвать конструктор у такого класса?
Anonymous Poll
13%
Type obj;
11%
auto obj = Type{};
20%
Type<int> obj{};
11%
Type<> obj{};
9%
Я пытался, у меня не получилось. Значит, это невозможно!
16%
Type::Type<int> obj{};
8%
Type::Type<> obj{};
13%
auto obj = Type::Type<>{};
31%
Может я и не знаю ответа, зато мне мама говорит, что я красивый!
Ответ
Самый главный результат опроса - почти треть канала состоит из красивых людей. In mom's humble opinion. И это прекрасно! И настоящие профессионалы, и внешне обаятельны, и в душе поэты!
Но ладно, это была лирика(поэт из меня так себе). Перейдем к правильному ответу.
Он был замаскирован, видимо поэтому набрал меньше всего голосов.
Стандарт говорит:
Невозможно явно указать шаблонные аргументы для шаблонного конструктора. Компилятор должен суметь вывести эти типы на основе переданных в конструктор аргументов.
Но так как в нашем случае конструктор не принимает никаких параметров - компилятор никак не сможет вывести типы.
Поэтому невозможно вызвать конструктор у такого класса:
Но! Объект такого класса создать можно.
Некоторые функции в С++ неявно создают объекты. Например std::bit_cast.
Спасибо @cppnyasha за пример.
Но конкретно в нашем случае была задача вызвать конструктор, а это невозможно.
Solve problems. Stay cool.
#cppcore #cpp20
Самый главный результат опроса - почти треть канала состоит из красивых людей. In mom's humble opinion. И это прекрасно! И настоящие профессионалы, и внешне обаятельны, и в душе поэты!
Но ладно, это была лирика(поэт из меня так себе). Перейдем к правильному ответу.
Он был замаскирован, видимо поэтому набрал меньше всего голосов.
Стандарт говорит:
Because the explicit template argument list follows the function template name,
and because constructor templates are named without using a function name,
there is no way to provide an explicit template argument list for these function templates.
Невозможно явно указать шаблонные аргументы для шаблонного конструктора. Компилятор должен суметь вывести эти типы на основе переданных в конструктор аргументов.
Но так как в нашем случае конструктор не принимает никаких параметров - компилятор никак не сможет вывести типы.
Поэтому невозможно вызвать конструктор у такого класса:
struct Type {
template <typename>
Type() {}
};
Но! Объект такого класса создать можно.
Некоторые функции в С++ неявно создают объекты. Например std::bit_cast.
struct Type {
template <typename>
Type() {};
};
struct Other {};
int main() {
Type t = std::bit_cast<Type>(Other{});
}
Спасибо @cppnyasha за пример.
Но конкретно в нашем случае была задача вызвать конструктор, а это невозможно.
Solve problems. Stay cool.
#cppcore #cpp20
Что происходит до main?
Рассмотрим простую программу:
Все очень просто. Объявляем две глобальные переменные, в main() присваиваем им значения и выводим их сумму на экран.
Скомпилировав эту программу, мы сможем посмотреть ее ассемблер и увидеть просто набор меток, соответствующих разным сущностям кода(переменным a и b, функции main). Но вы не увидите какого-то "скрипта". Типа как в питоне. Если питонячий код не оборачивать в функции, то мы точно будем знать, что выполнение будет идти сверху вниз. Так вот, такой простыни ассемблера вы не увидите. Код будет организован так, как будто бы им кто-то будет пользоваться.
И это действительно так! Убирая сложные детали, можем увидеть вот такое:
Суть программы состоит из меток. Метки нужны, чтобы обращаться к сущностям программы. Да, они и внутри основного кода используются. Но то, что на главной функции стоит метка, говорит нам о том, что ее кто-то вызывает!
Но даже до того, как начнет работу сущность, которая вызывает main, нужно проделать большую работу по подготовке программы к исполнению. Давайте просто перечислю, что должно быть сделано:
💥 Программа загружается в оперативную память.
💥 Аллокация памяти для стека. Для исполнения функций и хранения локальных переменных обязательно нужен стек.
💥 Аллокация памяти для кучи. Для программы нужна дополнительная память, которую она берет из кучи.
💥 Инициализация регистров. Там их большое множество. Например, нужно установить текущий указатель на вершину стека(stack pointer), указатель на инструкции(instruction pointer) и тд.
💥 Замапить виртуальное адресное пространство процесса. Процессы не работают с железной памятью напрямую. Они делают это через абстракцию, называемую виртуальная память.
💥 Положить на стек аргументы argc, argv(мб envp). Это аргументы для функции main.
💥 Загрузка динамических библиотек. Программа всегда линкуется с разными динамическими либами, даже если вы этого явно не делаете)
💥 Вызов всякий преинициализирующих функций.
Важная оговорка, что это все суперсильное упрощение. В реале все намного сложнее. Не претендую на полноту изложения и правильность порядка шагов. К тому же я говорю только про эквайромент полноценных ОС типа окон и пингвина. В эмбеде могут быть сильные отличия. Обязательно оставляйте свои дополнения процесса старта программы в комментариях.
В этих полноценных осях всю эту грязную работу на себя берет загрузчик программ.
После того, как эти шаги выполнены, загрузчик может вызывать ту самую функцию _start(название условное, зависит от реализации).
Она уже выполняет более прикладные чтоли вещи:
👉🏿 Статическая инициализация глобальных переменных. Это и недавно обсуждаемая zero-инициализация и константная инициализация(когда объект инициализирован константным выражением). То есть инициализируется все, что можно было узнать на этапе компиляции.
👉🏿 Динамическая инициализация глобальных объектов. Выполняется код конструкторов глобальных объектов.
👉🏿 Инициализация стандартного ввода-вывода. Об этом мы говорили тут.
👉🏿 Инициализация еще бог знает чего. Начальное состояние рандомайзера, malloc'а и прочего. Так-то это часть первых шагов, но привожу отдельно, чтобы вы не думали, что только ваши глобальные переменные инициализируются.
И только вот после этого всего, когда состояние программы приведено в соответствие с ожиданиями стандарта С++, функция _start вызывает main.
Так что, чтобы вы смогли выполнить свою программу, кому-то нужно очень мощно поднапрячься...
See what's underneath. Stay cool.
#OS #compiler
Рассмотрим простую программу:
#include <iostream>
#include <random>
int a;
int b;
int main() {
a = rand();
b = rand();
std::cout << (a + b);
}
Все очень просто. Объявляем две глобальные переменные, в main() присваиваем им значения и выводим их сумму на экран.
Скомпилировав эту программу, мы сможем посмотреть ее ассемблер и увидеть просто набор меток, соответствующих разным сущностям кода(переменным a и b, функции main). Но вы не увидите какого-то "скрипта". Типа как в питоне. Если питонячий код не оборачивать в функции, то мы точно будем знать, что выполнение будет идти сверху вниз. Так вот, такой простыни ассемблера вы не увидите. Код будет организован так, как будто бы им кто-то будет пользоваться.
И это действительно так! Убирая сложные детали, можем увидеть вот такое:
a:
.zero 4
b:
.zero 4
main:
push rbp
mov rbp, rsp
call rand
...
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov eax, 0
pop rbp
ret
Суть программы состоит из меток. Метки нужны, чтобы обращаться к сущностям программы. Да, они и внутри основного кода используются. Но то, что на главной функции стоит метка, говорит нам о том, что ее кто-то вызывает!
Но даже до того, как начнет работу сущность, которая вызывает main, нужно проделать большую работу по подготовке программы к исполнению. Давайте просто перечислю, что должно быть сделано:
💥 Программа загружается в оперативную память.
💥 Аллокация памяти для стека. Для исполнения функций и хранения локальных переменных обязательно нужен стек.
💥 Аллокация памяти для кучи. Для программы нужна дополнительная память, которую она берет из кучи.
💥 Инициализация регистров. Там их большое множество. Например, нужно установить текущий указатель на вершину стека(stack pointer), указатель на инструкции(instruction pointer) и тд.
💥 Замапить виртуальное адресное пространство процесса. Процессы не работают с железной памятью напрямую. Они делают это через абстракцию, называемую виртуальная память.
💥 Положить на стек аргументы argc, argv(мб envp). Это аргументы для функции main.
💥 Загрузка динамических библиотек. Программа всегда линкуется с разными динамическими либами, даже если вы этого явно не делаете)
💥 Вызов всякий преинициализирующих функций.
Важная оговорка, что это все суперсильное упрощение. В реале все намного сложнее. Не претендую на полноту изложения и правильность порядка шагов. К тому же я говорю только про эквайромент полноценных ОС типа окон и пингвина. В эмбеде могут быть сильные отличия. Обязательно оставляйте свои дополнения процесса старта программы в комментариях.
В этих полноценных осях всю эту грязную работу на себя берет загрузчик программ.
После того, как эти шаги выполнены, загрузчик может вызывать ту самую функцию _start(название условное, зависит от реализации).
Она уже выполняет более прикладные чтоли вещи:
👉🏿 Статическая инициализация глобальных переменных. Это и недавно обсуждаемая zero-инициализация и константная инициализация(когда объект инициализирован константным выражением). То есть инициализируется все, что можно было узнать на этапе компиляции.
👉🏿 Динамическая инициализация глобальных объектов. Выполняется код конструкторов глобальных объектов.
👉🏿 Инициализация стандартного ввода-вывода. Об этом мы говорили тут.
👉🏿 Инициализация еще бог знает чего. Начальное состояние рандомайзера, malloc'а и прочего. Так-то это часть первых шагов, но привожу отдельно, чтобы вы не думали, что только ваши глобальные переменные инициализируются.
И только вот после этого всего, когда состояние программы приведено в соответствие с ожиданиями стандарта С++, функция _start вызывает main.
Так что, чтобы вы смогли выполнить свою программу, кому-то нужно очень мощно поднапрячься...
See what's underneath. Stay cool.
#OS #compiler
Вызываем функцию в глобальном скоупе
В отличие от С в С++ можно вызывать код до входа в main. В С нельзя вызывать функции в глобальном скоупе, и глобальные переменные там должны быть инициализированы константным выражение. Однако для С++ пришлось ослабить правила. Это было нужно для возможности создания объектов кастомных классов, как глобальных переменных. Для создания объектов нужны конструкторы. А это обычные функции.
Поэтому в плюсах можно выполнять пользовательский код до main. Но этот код должен содержаться внутри конструкторов и вызываемых ими функциях.
Но просто так вызвать рандомную функция я не могу. Это запрещено.
"Ну блин. Мне очень надо. Может как-то договоримся?"
Со стандартом не договоришься. Но обходные пути все же можно найти.
Например так:
Здесь мы пользуемся уникальными свойствами оператора запятая: результат первого операнда вычисляется до вычисления второго и после просто отбрасывается. А значение всего выражения задается вторым операндом.
Получается, что здесь мы создаем статическую переменную-пустышку, чтобы получить возможность оперировать в глобальном скоупе. Инициализаторы глобальных переменных в С++ могут быть вычисляемыми. Поэтому мы используем свойство оператора запятой, чтобы беспоследственно вычислить some_function, а инициализировать dummy нулем.
Вероятнее всего, вам никогда не понадобиться так вызывать функцию. Однако оператор запятая - уникальный инструмент и может выручить даже в таких непростых ситуациях.
Have unique tools in your arsenal. Stay cool.
#cppcore #goodoldc
В отличие от С в С++ можно вызывать код до входа в main. В С нельзя вызывать функции в глобальном скоупе, и глобальные переменные там должны быть инициализированы константным выражение. Однако для С++ пришлось ослабить правила. Это было нужно для возможности создания объектов кастомных классов, как глобальных переменных. Для создания объектов нужны конструкторы. А это обычные функции.
Поэтому в плюсах можно выполнять пользовательский код до main. Но этот код должен содержаться внутри конструкторов и вызываемых ими функциях.
Но просто так вызвать рандомную функция я не могу. Это запрещено.
"Ну блин. Мне очень надо. Может как-то договоримся?"
Со стандартом не договоришься. Но обходные пути все же можно найти.
Например так:
static int dummy = (some_function(), 0);
int main() {}
Здесь мы пользуемся уникальными свойствами оператора запятая: результат первого операнда вычисляется до вычисления второго и после просто отбрасывается. А значение всего выражения задается вторым операндом.
Получается, что здесь мы создаем статическую переменную-пустышку, чтобы получить возможность оперировать в глобальном скоупе. Инициализаторы глобальных переменных в С++ могут быть вычисляемыми. Поэтому мы используем свойство оператора запятой, чтобы беспоследственно вычислить some_function, а инициализировать dummy нулем.
Вероятнее всего, вам никогда не понадобиться так вызывать функцию. Однако оператор запятая - уникальный инструмент и может выручить даже в таких непростых ситуациях.
Have unique tools in your arsenal. Stay cool.
#cppcore #goodoldc
Принтуем уникальный указатель
#новичкам
Умные указатели делают нашу жизнь намного проще. Они не только такие умные, что нам не стоит беспокоиться о менеджменте памяти. Они еще и очень удобные с точки зрения использования. Мы пользуемся умным указателем почти также, как мы бы пользовались обычным указателем. Все операторы перегружены так, чтобы мы вообще не видели разницу.
И мы понимаем, что это обертка, когда нам нужно достать сам сырой указатель и вызвать метод get().
Один из таких случаев - вывод указателя в поток. Если мы захотим вывести на консоль адрес объекта, то нам нужно будет вызывать метод get.
Но это же странно!
Да, это скорее нужно в каких-то отладочно-логировачных целях и ничего более.
Но нужно же!
Че им стоило перегрузить оператор<<, чтобы мы могли непосредственно сам объект уникального указателя сериализовывать?
Видимо больших затрат. Но ребята поднатужились и справились. В С++20 мы можем выводить сам объект без метода get()!
Ну и конечно в этом случае на консоли появится адрес объекта.
Смысл вывода умного указателя на поток всегда был понятен, поэтому хорошо, что комитет включил такое небольшое изменение в стандарт. Теперь уникальный указатель стал еще более тонкой оберткой.
Make small changes. Stay cool.
#cpp20 #cppcore
#новичкам
Умные указатели делают нашу жизнь намного проще. Они не только такие умные, что нам не стоит беспокоиться о менеджменте памяти. Они еще и очень удобные с точки зрения использования. Мы пользуемся умным указателем почти также, как мы бы пользовались обычным указателем. Все операторы перегружены так, чтобы мы вообще не видели разницу.
И мы понимаем, что это обертка, когда нам нужно достать сам сырой указатель и вызвать метод get().
Один из таких случаев - вывод указателя в поток. Если мы захотим вывести на консоль адрес объекта, то нам нужно будет вызывать метод get.
auto u_ptr = std::make_unique<Type>();
std::cout << u_ptr.get() << std::endl;
Но это же странно!
Да, это скорее нужно в каких-то отладочно-логировачных целях и ничего более.
Но нужно же!
Че им стоило перегрузить оператор<<, чтобы мы могли непосредственно сам объект уникального указателя сериализовывать?
Видимо больших затрат. Но ребята поднатужились и справились. В С++20 мы можем выводить сам объект без метода get()!
auto u_ptr = std::make_unique<Type>();
std::cout << u_ptr << std::endl;
Ну и конечно в этом случае на консоли появится адрес объекта.
Смысл вывода умного указателя на поток всегда был понятен, поэтому хорошо, что комитет включил такое небольшое изменение в стандарт. Теперь уникальный указатель стал еще более тонкой оберткой.
Make small changes. Stay cool.
#cpp20 #cppcore
Различаем преинкремент и постинкремент
#новичкам
Новичкам всегда не просто даются перегрузки операторов. Не то, чтобы кому-то часто приходится перегружать операторы. Но уж эти *нкременты точно редко. Из-за этого особенности постоянно забываются. Давайте же во всем разберемся.
Есть операторы инкремента и декремента. Первые увеличивают значение на какую-то условную единицк, вторые - уменьшаю. Они подразделяются на 2 формы: пре- и пост-. Далее будем все разбирать на примере операторов инкремента, для декремента все аналогично, только меняете плюс на минус.
Преинкремент - увеличивает значение на единицу и возвращает ссылку на уже увеличенное значение. Синтаксис использования такой:
Постинкремент - делает копию исходного числа, увеличивает на единицу оригинал, и возвращает копию по значению. Синтаксис использования такой:
Обычно внешний вид перегрузки операторов внутри описания класса такой:
Например, оператор копирования вызывается с помощью оператора присваивания(=). Поэтому подставляем в шаблон на место
Но теперь возникает вопрос. С++ не различает функции, у которых то же имя, то же те же аргументы, но разные возвращаемые значения. У нас как раз такая ситуация: названия одинаковые(operator++), возвращаемые значения разные(в одном случае возвращаем по ссылке, во втором - по значению) и одинаково отсутствуют аргументы. Если бы мы определили операторы так:
То была бы примерно такая ошибка компиляции:
Что же делать?
Примерно таким же вопросом задался Страуструп и сделал ход конем(правда конь ходил на костылях). Он связал префиксный оператор с человеческой формой объявления, а постфиксный - с вот такой:
Он ввел безымянный интовый параметр функции. Теперь чисто технически, компилятор сможет различить 2 вызова этих операторов и правильно заметчить эти вызовы на нужные определения.
Если видит префикс - выбирает оператор без аргумента, видит постфикс - выбирает с аргументом.
Вопрос, почему тип аргумента именно int - остается открытым. Наверное так было проще.
Итого, так перегружаются операторы инкремента:
Think of your decisions twice. Stay cool.
#cppcore
#новичкам
Новичкам всегда не просто даются перегрузки операторов. Не то, чтобы кому-то часто приходится перегружать операторы. Но уж эти *нкременты точно редко. Из-за этого особенности постоянно забываются. Давайте же во всем разберемся.
Есть операторы инкремента и декремента. Первые увеличивают значение на какую-то условную единицк, вторые - уменьшаю. Они подразделяются на 2 формы: пре- и пост-. Далее будем все разбирать на примере операторов инкремента, для декремента все аналогично, только меняете плюс на минус.
Преинкремент - увеличивает значение на единицу и возвращает ссылку на уже увеличенное значение. Синтаксис использования такой:
++value
.Постинкремент - делает копию исходного числа, увеличивает на единицу оригинал, и возвращает копию по значению. Синтаксис использования такой:
value++
.Обычно внешний вид перегрузки операторов внутри описания класса такой:
возвращаемое_значение operator{символы_вызова_оператора}(аргументы)
.Например, оператор копирования вызывается с помощью оператора присваивания(=). Поэтому подставляем в шаблон на место
возвращаемое_значение
ссылку на объект, на место символы_вызова_оператора
подставляем =, на место аргументов - const Type&. Получается так:Type& operator=(const Type&);
Но теперь возникает вопрос. С++ не различает функции, у которых то же имя, то же те же аргументы, но разные возвращаемые значения. У нас как раз такая ситуация: названия одинаковые(operator++), возвращаемые значения разные(в одном случае возвращаем по ссылке, во втором - по значению) и одинаково отсутствуют аргументы. Если бы мы определили операторы так:
Type& operator++() {
value_ += 1;
return *this;
}
Type operator++() {
auto tmp_value = *this;
value_ += 1;
return tmp_value;
}
То была бы примерно такая ошибка компиляции:
functions that differ only in their return type cannot be overloaded
.Что же делать?
Примерно таким же вопросом задался Страуструп и сделал ход конем(правда конь ходил на костылях). Он связал префиксный оператор с человеческой формой объявления, а постфиксный - с вот такой:
Type operator++(int) {
auto tmp_value = *this;
value_ += 1;
return tmp_value;
}
Он ввел безымянный интовый параметр функции. Теперь чисто технически, компилятор сможет различить 2 вызова этих операторов и правильно заметчить эти вызовы на нужные определения.
Если видит префикс - выбирает оператор без аргумента, видит постфикс - выбирает с аргументом.
Вопрос, почему тип аргумента именно int - остается открытым. Наверное так было проще.
Итого, так перегружаются операторы инкремента:
struct Type {
Type& operator++() {
value_ += 1;
return *this;
}
Type operator++(int) {
auto tmp_value = *this;
value_ += 1;
return tmp_value;
}
private:
IntegerLikeType value_;
};
Think of your decisions twice. Stay cool.
#cppcore
Может ли дружественная функция быть виртуальной?
#опытным
Еще один вопрос из серии "а могут ли рыбы быть рыжими?" - "Да вроде как по цвету могут, но рыжие это обычно про волосы, а волос у рыб нет. Да и вообще, кому это знание нужно?!". Что-то типа такого. Но раз такие вопросы задают, то пора в ваше кунг-фу добавить и этот приемчик.
Давайте начнем с начала.
Дружественная функция - обычная функция, которая имеет доступ к полям класса-друга. И все. Никаких других особенностей у нее нет. И ограничений тоже никаких нет. Ее сигнатура может быть любой. Объект класса-друга может в ней вообще не фигурировать.
А для виртуальности необходим экземпляр класса, в котором укромно затаился vptr, который и отвечает за диспатчинг вызовов в рантайме.
Вот и получается, что виртуальных дружественных функций не бывает.
Конечно вопрос о виртуальных friend-функциях может заходить только тогда, когда в параметрах есть ссылка или указатель на объект полиморфного класса. Но в общем случае на это не накладываются ограничения. Поэтому и в общем случае виртуальных подруг у класса не бывает.
Теперь сможете смело парировать интервьюеровчетким ударом в глазницу быстрым точным ответом
Но осадочек-то остался. Хочется менять работу дружественной функции в зависимости от того, какой конкретный объект ей передается. Такое мероприятие можно организовать достаточно просто, но это в следующем посте.
Have a real friends. Stay cool.
#interview #cppcore
#опытным
Еще один вопрос из серии "а могут ли рыбы быть рыжими?" - "Да вроде как по цвету могут, но рыжие это обычно про волосы, а волос у рыб нет. Да и вообще, кому это знание нужно?!". Что-то типа такого. Но раз такие вопросы задают, то пора в ваше кунг-фу добавить и этот приемчик.
Давайте начнем с начала.
Дружественная функция - обычная функция, которая имеет доступ к полям класса-друга. И все. Никаких других особенностей у нее нет. И ограничений тоже никаких нет. Ее сигнатура может быть любой. Объект класса-друга может в ней вообще не фигурировать.
А для виртуальности необходим экземпляр класса, в котором укромно затаился vptr, который и отвечает за диспатчинг вызовов в рантайме.
Вот и получается, что виртуальных дружественных функций не бывает.
Конечно вопрос о виртуальных friend-функциях может заходить только тогда, когда в параметрах есть ссылка или указатель на объект полиморфного класса. Но в общем случае на это не накладываются ограничения. Поэтому и в общем случае виртуальных подруг у класса не бывает.
Теперь сможете смело парировать интервьюеров
Но осадочек-то остался. Хочется менять работу дружественной функции в зависимости от того, какой конкретный объект ей передается. Такое мероприятие можно организовать достаточно просто, но это в следующем посте.
Have a real friends. Stay cool.
#interview #cppcore
Виртуальная дружественная функция
#новичкам
Вчера мы поговорили о том, что таких функций не бывает, но очень хочется получить альтернативу. Хочется изменять работу функции в зависимости от динамического типа, который в нее передается.
Ну и на самом деле ничего сложного здесь нет. Можно ведь сделать полиморфный метод, который будет вызываться в этой функции. И вот его поведение мы можем изменять примерно как нашей душеньке захочется.
Рассмотрим банальный пример - сериализация объекта в поток. Обычно для такой задачи используют дружественный оператор <<. Но вот хотелось бы сериализовать в зависимости от динамического типа. Фигня вопрос.
Есть у нас класс человека с его именем и фамилией. Есть класс налогоплательщика, который наследуется от человека и добавляет к полям ИНН. Допустим, мы хотим как-то выводить на консоль содержимое этих людей(в смысле поля; расчленением мы не занимаемся, не в Питере живем все-таки). Можно конечно в каждом классе определять свой оператор или выводить на консоль результат работы метода to_str, но это лишний апи и лишний код.
Мы просто в базовом классе определяем другана и говорим, чтобы он вызывал виртуальный метод. И все.
Да, это очень просто. Но подход скрытия деталей реализации за закрытым виртуальным методом используется, например в идиоме невиртуального интерфейса и еще невесть где. Поэтому о даже о таком приеме надо знать и применять его в подходящих ситуациях.
Have a real friends. Stay cool.
#cppcore
#новичкам
Вчера мы поговорили о том, что таких функций не бывает, но очень хочется получить альтернативу. Хочется изменять работу функции в зависимости от динамического типа, который в нее передается.
Ну и на самом деле ничего сложного здесь нет. Можно ведь сделать полиморфный метод, который будет вызываться в этой функции. И вот его поведение мы можем изменять примерно как нашей душеньке захочется.
Рассмотрим банальный пример - сериализация объекта в поток. Обычно для такой задачи используют дружественный оператор <<. Но вот хотелось бы сериализовать в зависимости от динамического типа. Фигня вопрос.
struct Person {
Person(const std::string &first_name,
const std::string &last_name) : first_name_{first_name},
last_name_{last_name} {}
friend std::ostream& operator<<(std::ostream& o, const Person& b) {
return o << b.to_str();
}
protected:
virtual std::string to_str() const{
return first_name_ + " " + last_name_;
}
private:
std::string first_name_;
std::string last_name_;
};
struct TaxPayer : Person {
TaxPayer(const std::string first_name,
const std::string last_name,
const std::string itn) : Person{first_name, last_name}, itn_{itn} {}
protected:
virtual std::string to_str() const{
return Person::to_str() + " " + itn_;
}
private:
std::string itn_;
};
int main() {
auto prs1 = std::make_unique<Person>("Ter", "Minator");
auto prs2 = std::make_unique<TaxPayer>("Ace", "Ventura", "0000");
std::cout << *prs1 << std::endl;
std::cout << *prs2 << std::endl;
}
// OUTPUT:
// Ter Minator
// Ace Ventura 0000
Есть у нас класс человека с его именем и фамилией. Есть класс налогоплательщика, который наследуется от человека и добавляет к полям ИНН. Допустим, мы хотим как-то выводить на консоль содержимое этих людей(в смысле поля; расчленением мы не занимаемся, не в Питере живем все-таки). Можно конечно в каждом классе определять свой оператор или выводить на консоль результат работы метода to_str, но это лишний апи и лишний код.
Мы просто в базовом классе определяем другана и говорим, чтобы он вызывал виртуальный метод. И все.
Да, это очень просто. Но подход скрытия деталей реализации за закрытым виртуальным методом используется, например в идиоме невиртуального интерфейса и еще невесть где. Поэтому о даже о таком приеме надо знать и применять его в подходящих ситуациях.
Have a real friends. Stay cool.
#cppcore