using namespace std
#новичкам
Десятки новичков спрашиваются в комментариях что это за конструкция и почему никто в более менее серьезном коде не использует ее. Ну чтож. Мы долго игнорировали этот запрос, но пора это исправить. Пост с разъяснениями просто обязан существовать. Поэтому сегодня все по полочкам разложим.
Начнем с namespace
В большинстве современных языков так или иначе реализована возможность разделения подключаемой функциональности на какие-то логические части. Отчасти это сделано для того, чтобы непосредственно в коде можно было увидеть к какой части принадлежит функциональность.
В С++ тоже есть похожий механизм. Он называется пространство имен или namespace. Можно сказать, что это область, где вы можете определять сущности, логически связанные друг с другом.
Работает это примерно так:
Мы определяем пространство имен MyLib, в котором будут все функции, классы и переменные, которые относятся только к этой либе. И при использовании функции hello мы обязаны указать перед ее именем неймспейс MyLib, чтобы компилятор понимал, где ему искать эту функцию.
Заодно и все, кто читает код, теперь понимают, что мы использует функцию hello именно из пространства MyLib и ни откуда-нибудь еще. Потому что может случиться ситуация, когда функция с именем hello будет не только в этом пространстве:
Вот сейчас критически важно указать, из какого пространства функция вызывается, потому что вы просто можете вызывать не ту функцию, если ничего не укажите.
Теперь std.
Это пространство имен для сущностей, которые уже реализованы за вас в стандартной библиотеке.
Допустим, вы хотите что-то на консоль вывести. Вы подключаете заголовочник
Вы сами не реализовывали cout. Вы лишь использовали то, что уже существует в стандартной библиотеке. И сказали об этом с помощью указания пространства имен.
Теперь using.
Иногда у пространств имен бывают очень длинные названия. Плюс они могут быть вложены друг в друга. И не всегда хочется писать:
И если вы знаете, что:
1️⃣ в данном конкретном небольшом месте кода вы используете много сущностей из какого-то пространства имен
2️⃣ и нет никакой неоднозначности, как в примере с функцией hello
вы можете использовать конструкцию using namespace:
С ее помощью вы говорите компилятору: "Внутри этого блока кода я могу использовать сущности из этого пространства имен без префикса. Ищи неизвестные тебе имена там."
Ну а конструкция
Почему же так никто не делает, если это избавляет от головной боли писать постоянно
👉🏿 Код пишется для того, чтобы его читали. При указании неймспейса читатель сразу понимает, откуда берется данная сущность. Это улучшает читаемость.
👉🏿 Обузинг
компилятор просто не поймет, какую функцию вы хотите реально вызывать. В данном случае будет ошибка компиляции. Но при определенных условиях компилятор может вызвать не ту версию и даже никак не сообщить вам об этом. Дебагаться потом будете долго.
👉🏿 Положите
В боевом коде почти никто не использует эту конструкцию, поэтому учитесь писать сразу так, как это будет требоваться в будущем.
Ease is not always the best choice. Stay cool.
#cppcore #badpractice
#новичкам
Десятки новичков спрашиваются в комментариях что это за конструкция и почему никто в более менее серьезном коде не использует ее. Ну чтож. Мы долго игнорировали этот запрос, но пора это исправить. Пост с разъяснениями просто обязан существовать. Поэтому сегодня все по полочкам разложим.
Начнем с namespace
В большинстве современных языков так или иначе реализована возможность разделения подключаемой функциональности на какие-то логические части. Отчасти это сделано для того, чтобы непосредственно в коде можно было увидеть к какой части принадлежит функциональность.
В С++ тоже есть похожий механизм. Он называется пространство имен или namespace. Можно сказать, что это область, где вы можете определять сущности, логически связанные друг с другом.
Работает это примерно так:
namespace MyLib {
void hello() { ... }
}
MyLib::hello();Мы определяем пространство имен MyLib, в котором будут все функции, классы и переменные, которые относятся только к этой либе. И при использовании функции hello мы обязаны указать перед ее именем неймспейс MyLib, чтобы компилятор понимал, где ему искать эту функцию.
Заодно и все, кто читает код, теперь понимают, что мы использует функцию hello именно из пространства MyLib и ни откуда-нибудь еще. Потому что может случиться ситуация, когда функция с именем hello будет не только в этом пространстве:
void hello() { ... }
namespace MyLib {
void hello() { ... }
}
namespace OtherLib {
void hello() { ... }
}
MyLib::hello();Вот сейчас критически важно указать, из какого пространства функция вызывается, потому что вы просто можете вызывать не ту функцию, если ничего не укажите.
Теперь std.
Это пространство имен для сущностей, которые уже реализованы за вас в стандартной библиотеке.
Допустим, вы хотите что-то на консоль вывести. Вы подключаете заголовочник
<iostream> и используете глобальный объект cout из пространства имен std:#include <iostream>
int main() {
std::cout << "Hello, World!\n";
}
Вы сами не реализовывали cout. Вы лишь использовали то, что уже существует в стандартной библиотеке. И сказали об этом с помощью указания пространства имен.
Теперь using.
Иногда у пространств имен бывают очень длинные названия. Плюс они могут быть вложены друг в друга. И не всегда хочется писать:
my_library::my_cool_module::my_class obj;
И если вы знаете, что:
1️⃣ в данном конкретном небольшом месте кода вы используете много сущностей из какого-то пространства имен
2️⃣ и нет никакой неоднозначности, как в примере с функцией hello
вы можете использовать конструкцию using namespace:
void foo() {
using namespace my_library::my_cool_module;
my_cool_class obj;
my_other_cool_class = my_cool_function(obj);
}С ее помощью вы говорите компилятору: "Внутри этого блока кода я могу использовать сущности из этого пространства имен без префикса. Ищи неизвестные тебе имена там."
Ну а конструкция
using namespace std говорит о том, что мы можем использовать абсолютно все стандартные сущности без указания префикса.Почему же так никто не делает, если это избавляет от головной боли писать постоянно
std::?👉🏿 Код пишется для того, чтобы его читали. При указании неймспейса читатель сразу понимает, откуда берется данная сущность. Это улучшает читаемость.
👉🏿 Обузинг
using namespace особенно в глобальном скоупе(вне функции и другого пространства имен) чреват конфликтом имен:void hello() {}
namespace MyLib {
void hello() {}
}
using namespace MyLib;
hello();компилятор просто не поймет, какую функцию вы хотите реально вызывать. В данном случае будет ошибка компиляции. Но при определенных условиях компилятор может вызвать не ту версию и даже никак не сообщить вам об этом. Дебагаться потом будете долго.
👉🏿 Положите
using namespace std в свой хэдэр и теперь проблемы будут у всего кода, который его подключает.В боевом коде почти никто не использует эту конструкцию, поэтому учитесь писать сразу так, как это будет требоваться в будущем.
Ease is not always the best choice. Stay cool.
#cppcore #badpractice
❤25👍11🔥3❤🔥2🗿1
sync_with_stdio
#опытным
std::cout, std::cerr и std::cin - стандартные объекты потоков для работы с вводом-выводом в С++.
Но из С++ программы мы также можем вызывать и сишное апи для работы с IO. Например
Но как связаны С++ и С апи для работы с IO? Мешают ли они друг другу и скремблят результаты операций? Или все хитрее?
Все хитрее)
И разбирать все будем на примере потока вывода std::cout, для других все аналогично, просто так нагляднее будет.
В базовой конфигурации std::cout и запись в stdout с помощью С апи синхронизированы. То есть следующие вызовы полностью эквивалентны(
Запись символа в буфер С++ объекта имеет тот же эффект, что и запись символа в буфер С потока.
На практике это означает, что синхронизированные потоки C++ не буферизуются, и каждая операция ввода-вывода на потоке C++ немедленно применяется к буферу соответствующего потока C. Это позволяет свободно смешивать C++ и C I/O операции.
Обращу внимание. Мы до сих пор говорим только об одном символе. И не спроста: для записи конкретного символа гарантируется синхронизация и потокобезопасность(thread-safety). Но записи в stdout из разных тредов могут перемешивать символы этих записей.
То есть при вызове:
по сути имплементация посимвольно записывает приветствие миру в stdout в цикле, вызывая std::putc(stdout, c). Обычно такие функции(типа putc) реализованы с помощью внутренних механизмов синхронизации, обеспечивая потокобезопасность.
И это предательски медленно! Поэтому дефолтные операции ввода-вывода в С++ такие медленные.
Синхронизируют запись каждого символа только трусы! Но мы-то с вами не трусы.
Можно отвязать С++ потоки от С потоков и сделать их независимыми. Тогда у С++ потоков появляется свой буфер, который работает оптимальнее, чем посимвольная запись. Это может дать сильный буст к производительности стандартных IO операций и по скорости они могут сравниться с сишными.
Чтобы отвязать потоки нужно самой первой строчкой main вызывать следующую функцию. Например так
Вывод может изменяться в разных ситуациях, но вот здесь получился такой вывод:
Видно, что разное апи работает независимо и непоследовательно.
Если вам не нужна такая синхронизация, то выключайте ее полетите на третьей космической скорости бороздить просторы галактики.
Be fast. Stay cool.
#cppcore #optimization #goodoldc #compiler
#опытным
std::cout, std::cerr и std::cin - стандартные объекты потоков для работы с вводом-выводом в С++.
Но из С++ программы мы также можем вызывать и сишное апи для работы с IO. Например
scanf, printf. Они также могут читать из консоли и писать в нее.Но как связаны С++ и С апи для работы с IO? Мешают ли они друг другу и скремблят результаты операций? Или все хитрее?
Все хитрее)
И разбирать все будем на примере потока вывода std::cout, для других все аналогично, просто так нагляднее будет.
В базовой конфигурации std::cout и запись в stdout с помощью С апи синхронизированы. То есть следующие вызовы полностью эквивалентны(
c- символьная переменная):std::fputc(stdout, c);
// and
std::cout.rdbuf()->sputc(c);
Запись символа в буфер С++ объекта имеет тот же эффект, что и запись символа в буфер С потока.
На практике это означает, что синхронизированные потоки C++ не буферизуются, и каждая операция ввода-вывода на потоке C++ немедленно применяется к буферу соответствующего потока C. Это позволяет свободно смешивать C++ и C I/O операции.
Обращу внимание. Мы до сих пор говорим только об одном символе. И не спроста: для записи конкретного символа гарантируется синхронизация и потокобезопасность(thread-safety). Но записи в stdout из разных тредов могут перемешивать символы этих записей.
То есть при вызове:
std::cout << "Hello, World!" << std::endl;
по сути имплементация посимвольно записывает приветствие миру в stdout в цикле, вызывая std::putc(stdout, c). Обычно такие функции(типа putc) реализованы с помощью внутренних механизмов синхронизации, обеспечивая потокобезопасность.
И это предательски медленно! Поэтому дефолтные операции ввода-вывода в С++ такие медленные.
Синхронизируют запись каждого символа только трусы! Но мы-то с вами не трусы.
Можно отвязать С++ потоки от С потоков и сделать их независимыми. Тогда у С++ потоков появляется свой буфер, который работает оптимальнее, чем посимвольная запись. Это может дать сильный буст к производительности стандартных IO операций и по скорости они могут сравниться с сишными.
Чтобы отвязать потоки нужно самой первой строчкой main вызывать следующую функцию. Например так
int main()
{
std::ios::sync_with_stdio(false);
std::cout << "a\n";
std::printf("b\n");
std::cout << "c\n";
}
Вывод может изменяться в разных ситуациях, но вот здесь получился такой вывод:
a
c
b
Видно, что разное апи работает независимо и непоследовательно.
Если вам не нужна такая синхронизация, то выключайте ее полетите на третьей космической скорости бороздить просторы галактики.
Be fast. Stay cool.
#cppcore #optimization #goodoldc #compiler
1❤23🔥12👍8🤔5
Конфликт в действии
#опытным
Спасибо, @Ivaneo, за любезно предоставленный примерчик.
Посмотрите на этот код:
Как думаете, что случится при попытке компиляции со статической линковкой системных библиотек и запуска этого кода под linux? Поразмышляйте несколько секунд.
А получите ошибку сегментации: прилетит сигнал SIGSEGV.
Почему так? Мы же ничего незаконного не делали! Просто пытаемся читать в переменную, заблаговременно отвязав С++ потоки от сишных.
На самом деле сделали кое-что незаконное. Назвали переменную в честь системного вызова. Давайте по порядку.
Когда у нас потоки синхронизированы, для операций с потоками используется стандартное сишное апи.
Когда мы отвязываем потоки, то С++ ввод-вывод начинает работать самостоятельно и независимо. Имеются полноценные буферы и сложная система их менеджмента. А для общения с операционной системой можно использовать непосредственно системные вызовы.
Посмотрим на примере gcc.
В исходниках есть такое определение библиотечного вызова чтения:
Таким образом символ read, соответствующий функции чтения данных из файлового дискриптора, является слабым(weak).
А переменная read из нашего кода является сильным символом из сегмента неинициализированных данных(bss).
Естественно, что сильный символ всегда переписывает слабый. Поэтому в итоговом исполняемом файле символ read будет указывать не на функцию, а на переменную read.
То есть при запуске программы и попытке вызвать функцию read, будет "вызываться" переменная. Отсюда и сегфолт.
Все системные функции - это С функции. У них нет никаких неймспейсов, чтобы предотвращать клаш имен. Поэтому всегда избегайте имен, которые могут конфликтовать со стандартными библиотечными функциями(например
Avoid name clash. Stay cool.
#cppcore #goodoldc #OS
#опытным
Спасибо, @Ivaneo, за любезно предоставленный примерчик.
Посмотрите на этот код:
#include <iostream>
int read;
int main()
{
std::ios_base::sync_with_stdio(false);
std::cin >> read;
}
Как думаете, что случится при попытке компиляции со статической линковкой системных библиотек и запуска этого кода под linux? Поразмышляйте несколько секунд.
Почему так? Мы же ничего незаконного не делали! Просто пытаемся читать в переменную, заблаговременно отвязав С++ потоки от сишных.
На самом деле сделали кое-что незаконное. Назвали переменную в честь системного вызова. Давайте по порядку.
Когда у нас потоки синхронизированы, для операций с потоками используется стандартное сишное апи.
Когда мы отвязываем потоки, то С++ ввод-вывод начинает работать самостоятельно и независимо. Имеются полноценные буферы и сложная система их менеджмента. А для общения с операционной системой можно использовать непосредственно системные вызовы.
Посмотрим на примере gcc.
В исходниках есть такое определение библиотечного вызова чтения:
/* Read NBYTES into BUF from FD. Return the number read or -1. */
ssize_t
__libc_read (int fd, void *buf, size_t nbytes)
{
return SYSCALL_CANCEL (read, fd, buf, nbytes);
}
weak_alias (__libc_read, read)
Таким образом символ read, соответствующий функции чтения данных из файлового дискриптора, является слабым(weak).
А переменная read из нашего кода является сильным символом из сегмента неинициализированных данных(bss).
Естественно, что сильный символ всегда переписывает слабый. Поэтому в итоговом исполняемом файле символ read будет указывать не на функцию, а на переменную read.
То есть при запуске программы и попытке вызвать функцию read, будет "вызываться" переменная. Отсюда и сегфолт.
Все системные функции - это С функции. У них нет никаких неймспейсов, чтобы предотвращать клаш имен. Поэтому всегда избегайте имен, которые могут конфликтовать со стандартными библиотечными функциями(например
read, open, close, write, exit).Avoid name clash. Stay cool.
#cppcore #goodoldc #OS
❤17🔥11👍7🤯4❤🔥1
Объявления функций vs объявление переменной или зачем нужен extern
#новичкам
В С/С++ существует интересная механика - разделение на объявление сущности и ее определение. У этого есть серьезные причины:
👉🏿 Ускорение компиляции: изменения в реализации не требуют перекомпиляции всех файлов, использующих объявление.
👉🏿 Сокрытие реализации: инкапсуляция, предоставление только интерфейса (в .hpp) и скрытие деталей в .cpp.
👉🏿 Разрешение циклических зависимостей и возможность ссылаться на типы до их полного определения.
👉🏿 Организация кода и разделение ответственности.
И если с функциями более менее понятно. Есть объявление в виде обозначения сигнатуры функции. И определение в виде полного предоставления тела функции:
С переменными все немного сложнее. Но начнем с легкого:
Это определение хоть глобальной, хоть локальной переменной
Если думать в аналогии с функциями, то легко дойти до мысли что:
это объявление переменной.
Но это не так!
Это в любом случае определение!
Если вы встретите такую строчку вне функции, то это определение переменной и ее инициализация нулем. А внутри функции - определение без инициализации(значение будет мусорное). Определение переменной всегда связано с созданием объекта и выделением памяти под него.
Тогда как сказать компилятору, что я буду использовать переменную с каким-то именем и типом, но не хочу создавать объект а буду сслаться на определение в другом месте?
Ровно для этого и нужно ключевое слово extern.
До мейна мы сказали, что будем ссылаться на переменную i, определение которой лежит в другом месте. И сразу после мейна предоставляем это определение. Компилятор все понял и в итоге мы получаем 42 на консоли, а не ноль.
В этом случае строчка
Определение переменной может находиться хоть в другой единице трансляции. Просто тогда зависимости будут разрешаться на этапе линковки.
Для локальных переменных функций объявление не предусмотрено(оно и не нужно). Если попытаетесь сделать так:
То вы опять объявите глобальную переменную
То есть чисто объявить вы можете только глобальную переменную (про полям класса речь не идет) и только с помощью
Declare your intentions. Stay cool.
#cppcore
#новичкам
В С/С++ существует интересная механика - разделение на объявление сущности и ее определение. У этого есть серьезные причины:
👉🏿 Ускорение компиляции: изменения в реализации не требуют перекомпиляции всех файлов, использующих объявление.
👉🏿 Сокрытие реализации: инкапсуляция, предоставление только интерфейса (в .hpp) и скрытие деталей в .cpp.
👉🏿 Разрешение циклических зависимостей и возможность ссылаться на типы до их полного определения.
👉🏿 Организация кода и разделение ответственности.
И если с функциями более менее понятно. Есть объявление в виде обозначения сигнатуры функции. И определение в виде полного предоставления тела функции:
void foo(int a); // declaration
void foo(int a) {
std::cout << a << std::endl; // definition
}
С переменными все немного сложнее. Но начнем с легкого:
int i = 0;
Это определение хоть глобальной, хоть локальной переменной
i и ее инициализация нулем. Все все понимают.Если думать в аналогии с функциями, то легко дойти до мысли что:
int i;
это объявление переменной.
Но это не так!
Это в любом случае определение!
Если вы встретите такую строчку вне функции, то это определение переменной и ее инициализация нулем. А внутри функции - определение без инициализации(значение будет мусорное). Определение переменной всегда связано с созданием объекта и выделением памяти под него.
Тогда как сказать компилятору, что я буду использовать переменную с каким-то именем и типом, но не хочу создавать объект а буду сслаться на определение в другом месте?
Ровно для этого и нужно ключевое слово extern.
extern int i; // declaration
int main() {
std::cout << i << std::endl;
}
int i = 42;
// OUTPUT
// 42
До мейна мы сказали, что будем ссылаться на переменную i, определение которой лежит в другом месте. И сразу после мейна предоставляем это определение. Компилятор все понял и в итоге мы получаем 42 на консоли, а не ноль.
В этом случае строчка
extern int i; является объявлением глобальной переменной i.Определение переменной может находиться хоть в другой единице трансляции. Просто тогда зависимости будут разрешаться на этапе линковки.
Для локальных переменных функций объявление не предусмотрено(оно и не нужно). Если попытаетесь сделать так:
int main() {
extern int i;
std::cout << i << std::endl;
}То вы опять объявите глобальную переменную
i.То есть чисто объявить вы можете только глобальную переменную (про полям класса речь не идет) и только с помощью
extern. Declare your intentions. Stay cool.
#cppcore
1🔥21👍11❤7🤣3
Почему локальные переменные инициализируются мусором?
#новичкам
Изучать С++ больно по многим причинам. Одна из них - есть вещи, которые просто надо принять на веру. И чтобы понять, почему эти вещи работают так, а не иначе, нужно много времени за кодингом и качать сиплюсплюсные и computer science бицепсы.
Сегодня разберем вопрос, который лично меня мучал в начале пути: почему локальная переменная будет заполнена мусором, если ее не инициализировать?
Ну и в дополнение: Откуда берется этот мусор и почему нельзя просто нулями заполнить?
На самом деле мусор - не значит какие-то рандомные числа, никто их не генерирует. И чтобы понять, откуда берется мусор, нужно знать о стеке вызовов.
Стек вызовов - пространство в памяти, которое содержит информацию об исполнении функций. Представим аналогию со стопкой книг. Каждая книга - небольшой кусочек памяти с информацией об отдельной функции(фрейм функции). Как только вы вызываете функцию, на стопку кладется новая книга(фрейм). Когда исполнение доходит до конца функции - сверху снимается книга. Это происходит автоматически на уровне машинных инструций.
Так вот локальные переменные как раз хранятся на стеке. Если точнее, то значения локальных переменных конкретной функции лежат внутри фрейма этой функции. И нас интересует процесс выделения памяти под локальные переменные.
При исполнении программы есть специальный указатель, который указывает на вершину стека. Выделение памяти под локальную переменную - это просто сдвиг этого указателя. И когда мы говорим:
Мы говорим, что хотим иметь переменную
Но значение-то все равно будет. И будет оно браться их тех битиков-байтиков, которые находились в куске памяти от X до X-4.
А кто-то из вас заранее знает, какие байтики там будут храниться?
Нет
На этом месте почти наверняка уже располагались данные какой-нибудь другой функции, которая уже выполнилась ранее и ее фрейм снялся со стека.
Так как для вас все эти процессы невидимы, вы и получаете мусор в неинициализированной переменной. Но это не совсем мусор: это просто данные ранее выполнившейся функции.
Чтобы более наглядно представить себе процесс работы со стеком вызовов, можете воспользоваться этим визуализатором. Там примеры фиксированные. Если же хотите на собственных примерах посмотреть - можете поиграться тут.
"Ну окей. Мусор берется из данных предыдущих вызовов функций. Но до main'а-то ничего не вызывается! Откуда там мусор?"
На самом деле вызывается, вы просто не видите этого. Чтобы подготовить программу к запуску, надо немало потрудиться и эти труды обязательно будут отражены на стеке.
"Ладно, выкрутился. Но зачем вообще этот мусор оставлять, почему нельзя память занулять?"
Да можно, почему нельзя. Просто это не делается. Концепция С/С++ - не плати за то, чем не пользуешься.
"Очищение" памяти отнимает дополнительное драгоценное время исполнения программы. И никто его не хочет тратить на такую ненужную вещь, как обнуление памяти. Инициализируйте свои переменные и проблем с вывозом мусора не будет.
Understand the root cause. Stay cool.
#cppcore #os
#новичкам
Изучать С++ больно по многим причинам. Одна из них - есть вещи, которые просто надо принять на веру. И чтобы понять, почему эти вещи работают так, а не иначе, нужно много времени за кодингом и качать сиплюсплюсные и computer science бицепсы.
Сегодня разберем вопрос, который лично меня мучал в начале пути: почему локальная переменная будет заполнена мусором, если ее не инициализировать?
int main() {
int i;
std::cout << i << std::endl;
}
// POSSIBLE OUTPUT:
// 64Ну и в дополнение: Откуда берется этот мусор и почему нельзя просто нулями заполнить?
На самом деле мусор - не значит какие-то рандомные числа, никто их не генерирует. И чтобы понять, откуда берется мусор, нужно знать о стеке вызовов.
Стек вызовов - пространство в памяти, которое содержит информацию об исполнении функций. Представим аналогию со стопкой книг. Каждая книга - небольшой кусочек памяти с информацией об отдельной функции(фрейм функции). Как только вы вызываете функцию, на стопку кладется новая книга(фрейм). Когда исполнение доходит до конца функции - сверху снимается книга. Это происходит автоматически на уровне машинных инструций.
Так вот локальные переменные как раз хранятся на стеке. Если точнее, то значения локальных переменных конкретной функции лежат внутри фрейма этой функции. И нас интересует процесс выделения памяти под локальные переменные.
При исполнении программы есть специальный указатель, который указывает на вершину стека. Выделение памяти под локальную переменную - это просто сдвиг этого указателя. И когда мы говорим:
int main() {
int i;
...
}Мы говорим, что хотим иметь переменную
i и надо выделить под нее память, то есть просто передвинуть указатель на стек с позиции X на позицию X+4. Само значение мы при этом не задаем.Но значение-то все равно будет. И будет оно браться их тех битиков-байтиков, которые находились в куске памяти от X до X-4.
А кто-то из вас заранее знает, какие байтики там будут храниться?
Нет
На этом месте почти наверняка уже располагались данные какой-нибудь другой функции, которая уже выполнилась ранее и ее фрейм снялся со стека.
Так как для вас все эти процессы невидимы, вы и получаете мусор в неинициализированной переменной. Но это не совсем мусор: это просто данные ранее выполнившейся функции.
Чтобы более наглядно представить себе процесс работы со стеком вызовов, можете воспользоваться этим визуализатором. Там примеры фиксированные. Если же хотите на собственных примерах посмотреть - можете поиграться тут.
"Ну окей. Мусор берется из данных предыдущих вызовов функций. Но до main'а-то ничего не вызывается! Откуда там мусор?"
На самом деле вызывается, вы просто не видите этого. Чтобы подготовить программу к запуску, надо немало потрудиться и эти труды обязательно будут отражены на стеке.
"Ладно, выкрутился. Но зачем вообще этот мусор оставлять, почему нельзя память занулять?"
Да можно, почему нельзя. Просто это не делается. Концепция С/С++ - не плати за то, чем не пользуешься.
"Очищение" памяти отнимает дополнительное драгоценное время исполнения программы. И никто его не хочет тратить на такую ненужную вещь, как обнуление памяти. Инициализируйте свои переменные и проблем с вывозом мусора не будет.
Understand the root cause. Stay cool.
#cppcore #os
👍35❤11🔥10❤🔥3
Почему тогда глобальные переменные зануляются?
#новичкам
Внимательные читатели 2-х последних постов могут задаться вопросом: "Почему в неинициализированных локальных переменных лежит мусор, а в глобальных - нули? Откуда такие двойные стандарты?".
Можно конечно сказать, что такие двойные стандарты определены в стандарте С++ и на этом можно закончить.
Ответ, как и в случае с локальными переменными, лежит за пределами языка.
Все пошло от языка С. Там глобальные переменные занулялись, поэтому для совместимости с С С++ также перенял эту особенность.
"Это конечно хорошо, но на вопрос вы так и не ответили, а только стрелками кидаетесь."
Это был важный переход, который и приведет нас к ответу.
Язык С зарождался исходя из потребностей развития операционной системы Unix. Поэтому некоторые особенности ОС интегрировались в язык.
Конкретно неинициализированные глобальные переменные в Unix хранятся в сегменте глобальной памяти .bss . При запуске программы ОС выделяла память под программу. Для сегмента bss ядру нужно было выделить участок памяти запрошенного размера. Но что должно было лежать в этой памяти? По соображениям безопасности и изоляции процессов ядро не могло отдать программе память с остаточными данными от предыдущих процессов. Самый простой и эффективный способ гарантировать это — заполнить выделенную память нулями.
И программисты на С стали естественным образом пользоваться этой особенностью: неинициализированные глобальные переменные занулялись.
Когда пришло время стандартизации С, то обнуление глобальных переменных органично перекочевало в стандарт языка.
Вот так обнуление стало стандартом С, а затем и С++.
Understand the root cause. Stay cool.
#cppcore #os
#новичкам
Внимательные читатели 2-х последних постов могут задаться вопросом: "Почему в неинициализированных локальных переменных лежит мусор, а в глобальных - нули? Откуда такие двойные стандарты?".
Можно конечно сказать, что такие двойные стандарты определены в стандарте С++ и на этом можно закончить.
Ответ, как и в случае с локальными переменными, лежит за пределами языка.
int i;
int main() {
std::cout << i << std::endl;
}
// OUTPUT is always
// 0
Все пошло от языка С. Там глобальные переменные занулялись, поэтому для совместимости с С С++ также перенял эту особенность.
"Это конечно хорошо, но на вопрос вы так и не ответили, а только стрелками кидаетесь."
Это был важный переход, который и приведет нас к ответу.
Язык С зарождался исходя из потребностей развития операционной системы Unix. Поэтому некоторые особенности ОС интегрировались в язык.
Конкретно неинициализированные глобальные переменные в Unix хранятся в сегменте глобальной памяти .bss . При запуске программы ОС выделяла память под программу. Для сегмента bss ядру нужно было выделить участок памяти запрошенного размера. Но что должно было лежать в этой памяти? По соображениям безопасности и изоляции процессов ядро не могло отдать программе память с остаточными данными от предыдущих процессов. Самый простой и эффективный способ гарантировать это — заполнить выделенную память нулями.
И программисты на С стали естественным образом пользоваться этой особенностью: неинициализированные глобальные переменные занулялись.
Когда пришло время стандартизации С, то обнуление глобальных переменных органично перекочевало в стандарт языка.
Вот так обнуление стало стандартом С, а затем и С++.
Understand the root cause. Stay cool.
#cppcore #os
8👍29❤10🔥9😱1
Как запретить объекту создаваться на стеке? Классика
#новичкам
Вопрос относится скорее к категории "из собеседований", но новичкам все равно полезно задавать себе такие вопросы, чтобы проверить глубину понимания С++.
Можно удалить все конструкторы с помощью пометки delete и тогда объект вообще нигде нельзя будет создавать. Но вопрос скорее всего про другое: как запретить объекту создаваться на стеке, но не на куче или статической памяти?
Очевидно, что-то надо колдовать с конструкторами, потому что именно они создают объект.
Прямолинейный подход - сделаем конструктор приватным. Тогда внешний код не сможет его вызвать.
Но создавать объект-то нам нужно все равно как-то. И для этого существуют фабричные методы - статические методы класса, которые дают доступ с объекту/его созданию.
Если мы хотим создавать объект только на куче можно написать так:
Здесь фабричный метод создает std::unique_ptr, который и будет хранить указатель на класс. std::make_unique нельзя использовать, потому что внутри нее мы не сможем вызвать приватный конструктор.
Можно также удалить операции копирования и перемещения, потому что скорее всего в таком виде логика операций с MyClass не подразумевает копирования и перемещения.
Такой подход может быть полезен, если:
👉🏿 размер объекта очень большой и просто не влезет в стек
👉🏿 хотите реализовать всякие паттерны, типа object pool или оопшной фабрики.
👉🏿 нужно обрабатывать ошибки создания объекта без исключений - в случае ошибки просто вернем nullptr.
👉🏿 вы хотите какой-то особый контроль времени жизни.
Мы также можем разрешить создавать объект только в глобальной области. Такой паттерн называется синглтон:
Конструктор и деструктор приватные, копирование и перемещение вообще запрещены. И есть статический метод, который возвращает ссылку на инстанс класса. За счет того, что используется статическая локальная переменная inst, инициализация объекта гарантировано потокобезопасна(только один поток в итоге создаст объект). В этом воплощении паттерн получил название синглтон Майерса.
Синглтоны могут быть полезны, например, при реализации пула соединений, логгера или сборщика метрик.
Обсудили по сути самые классические практически применимые способы. В следующем посте будет уже экзотика. Если знаете какие-то необычные способы - пишите в комментах, разберем их в следующий раз.
Limit wrong uses. Stay cool.
#design #cppcore
#новичкам
Вопрос относится скорее к категории "из собеседований", но новичкам все равно полезно задавать себе такие вопросы, чтобы проверить глубину понимания С++.
Можно удалить все конструкторы с помощью пометки delete и тогда объект вообще нигде нельзя будет создавать. Но вопрос скорее всего про другое: как запретить объекту создаваться на стеке, но не на куче или статической памяти?
Очевидно, что-то надо колдовать с конструкторами, потому что именно они создают объект.
Прямолинейный подход - сделаем конструктор приватным. Тогда внешний код не сможет его вызвать.
Но создавать объект-то нам нужно все равно как-то. И для этого существуют фабричные методы - статические методы класса, которые дают доступ с объекту/его созданию.
Если мы хотим создавать объект только на куче можно написать так:
class MyClass {
private:
MyClass() { std::cout << "MyClass created\n"; }
public:
~MyClass() { std::cout << "MyClass destroyed\n"; }
static std::unique_ptr<MyClass> create() {
return std::unique_ptr<MyClass>(new MyClass());
}
void hello() const {
std::cout << "Hello, MyClass!\n";
}
};
auto obj = MyClass::create();
obj->hello();Здесь фабричный метод создает std::unique_ptr, который и будет хранить указатель на класс. std::make_unique нельзя использовать, потому что внутри нее мы не сможем вызвать приватный конструктор.
Можно также удалить операции копирования и перемещения, потому что скорее всего в таком виде логика операций с MyClass не подразумевает копирования и перемещения.
Такой подход может быть полезен, если:
👉🏿 размер объекта очень большой и просто не влезет в стек
👉🏿 хотите реализовать всякие паттерны, типа object pool или оопшной фабрики.
👉🏿 нужно обрабатывать ошибки создания объекта без исключений - в случае ошибки просто вернем nullptr.
👉🏿 вы хотите какой-то особый контроль времени жизни.
Мы также можем разрешить создавать объект только в глобальной области. Такой паттерн называется синглтон:
class Singleton {
public:
static Singleton& instance() {
static Singleton inst;
return inst;
}
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(Singleton&&) = delete;
void foo() {}
private:
Singleton() = default;
~Singleton() = default;
};Конструктор и деструктор приватные, копирование и перемещение вообще запрещены. И есть статический метод, который возвращает ссылку на инстанс класса. За счет того, что используется статическая локальная переменная inst, инициализация объекта гарантировано потокобезопасна(только один поток в итоге создаст объект). В этом воплощении паттерн получил название синглтон Майерса.
Синглтоны могут быть полезны, например, при реализации пула соединений, логгера или сборщика метрик.
Обсудили по сути самые классические практически применимые способы. В следующем посте будет уже экзотика. Если знаете какие-то необычные способы - пишите в комментах, разберем их в следующий раз.
Limit wrong uses. Stay cool.
#design #cppcore
❤23👍14🔥7
Как запретить объекту создаваться на стеке? Экзотика
#опытным
В этом посте будут не самые обычные способы запретов, которые вряд ли полезны на практике в таком виде, но полезны их отдельные элементы. Да и просто прикольные по своей идее.
Начнем с конца. Жизни объекта, конечно. Давайте сделаем приватным не конструктор, а деструктор. Тогда внешний код не сможет разместить такой объект на стеке, ведь он не сможет его удалить потом.
Однако удалять объект хочется. Для этого сделаем публичный статический метод destroy и фабричный метод, возвращающий умный указатель с кастомным делитером:
При выходе из скоупа юник разрушится и его деструктор дернет destroy, который сам дернет delete.
Публичный конструктор здесь не имеет большого смысла, но и так и так работает.
Но вообще говоря, зачем все эти запреты? Можно ли как-то добиться запрета при публичном конструкторе и деструкторе?
Можно(почти).
Давайте сделаем публичный деструктор и конструктор с параметром, который не может создать внешний код:
Технически, все методы класса Object публичные. Но инстанс Key может создать только фабрика. Поэтому и создание инстанса Object возможно только через нее.
Есть даже идиома, называется passkey, которая приписывает примерно так и создавать объекты.
Класс ключа может быть определен и внутри фабрики, как приватный класс. Фабрики может и не быть в принципе, Key может находиться внутри Object. Но суть одна.
И напоследок совсем гадкий утенок. Делаем в заголовке forward declaration типа и объявляем фабричную функцию. В cpp определяем класс и функцию.
В итоге да, мы запретили объекту создаваться, где угодно, кроме как через createObject. Но при этом пользоваться объектом мы никак не сможем. Только создать и удалить.
Explore exotic things. Stay cool.
#design #cppcore
#опытным
В этом посте будут не самые обычные способы запретов, которые вряд ли полезны на практике в таком виде, но полезны их отдельные элементы. Да и просто прикольные по своей идее.
Начнем с конца. Жизни объекта, конечно. Давайте сделаем приватным не конструктор, а деструктор. Тогда внешний код не сможет разместить такой объект на стеке, ведь он не сможет его удалить потом.
Однако удалять объект хочется. Для этого сделаем публичный статический метод destroy и фабричный метод, возвращающий умный указатель с кастомным делитером:
class MyClass {
public:
MyClass() { std::cout << "MyClass constructor\n"; }
private:
~MyClass() { std::cout << "MyClass destructor\n"; }
public:
static void destroy(MyClass* ptr) {
delete ptr;
}
static std::unique_ptr<MyClass, decltype(&destroy)> create() {
return std::unique_ptr<MyClass, decltype(&destroy)>(new MyClass(), destroy);
}
};При выходе из скоупа юник разрушится и его деструктор дернет destroy, который сам дернет delete.
Публичный конструктор здесь не имеет большого смысла, но и так и так работает.
Но вообще говоря, зачем все эти запреты? Можно ли как-то добиться запрета при публичном конструкторе и деструкторе?
Можно(почти).
Давайте сделаем публичный деструктор и конструктор с параметром, который не может создать внешний код:
class Key {
private:
Key() = default;
friend class Factory;
};
class Object {
public:
explicit Object(const Key&) {} // требует ключ
};
class Factory {
public:
static std::unique_ptr<Object> create() {
return std::make_unique<Object>(Key());
}
};Технически, все методы класса Object публичные. Но инстанс Key может создать только фабрика. Поэтому и создание инстанса Object возможно только через нее.
Есть даже идиома, называется passkey, которая приписывает примерно так и создавать объекты.
Класс ключа может быть определен и внутри фабрики, как приватный класс. Фабрики может и не быть в принципе, Key может находиться внутри Object. Но суть одна.
И напоследок совсем гадкий утенок. Делаем в заголовке forward declaration типа и объявляем фабричную функцию. В cpp определяем класс и функцию.
// object.hpp
class Object;
std::unique_ptr<Object> createObject();
// object.cpp
#include "object.h"
class Object {
public:
Object() = default;
~Object() = default;
};
std::unique_ptr<Object> createObject() {
return std::make_unique<Object>();
}
В итоге да, мы запретили объекту создаваться, где угодно, кроме как через createObject. Но при этом пользоваться объектом мы никак не сможем. Только создать и удалить.
Explore exotic things. Stay cool.
#design #cppcore
🔥17❤7👍7🤯1
Как запретить объекту создаваться на куче?
#новичкам
Про стек поговорили, не будем и кучу обижать.
Начать можно со знакомого подхода: приватный конструктор и фабрика, возвращающая объект по значению:
Если объект может быть создан некорректно, то вернуть можно std::optional, с помощью которого ошибку можно будет отловить:
"На приватных конструкторах мы уже собаку съели. Даешь способ по-интереснее!"
Хорошо. Давайте будем радикальными. Просто удалим перегрузку оператора new для этого класса. Тогда вообще никто не будет в состоянии объект на куче создать:
Вот так просто и без дополнительных приседаний.
Но можно играть не радикально, а хитро. Не удалим, а переопределим operator new так, чтобы он размещал объекты на готовом существующем статическом буфере. Реально рабочий примерчик довольно большой будет, плюс чтобы не углубляться в пучины кастомных аллокаторов, покажем только идею:
Типа линейный аллокатор, при запросе нового объекта просто сдвигаем offset буфера.
Это конечно все интересно. Но вспомните пост, где мы разбирали вопрос "Где аллоцируются элементы std::array?" и задумайтесь. А что если целевой объект будет полем другого класса, который мы создаем на куче?
Тогда его расположение будет определяться тем, где находится объемлющий объект. То есть, если мы создаём объект
Пишите в комментах, если знаете, как обойти эту проблему)
Don't be so radical. Stay cool.
#cppcore #memory #design
#новичкам
Про стек поговорили, не будем и кучу обижать.
Начать можно со знакомого подхода: приватный конструктор и фабрика, возвращающая объект по значению:
class OnlyStack {
OnlyStack() = default;
public:
static OnlyStack Create() { return {}; }
};Если объект может быть создан некорректно, то вернуть можно std::optional, с помощью которого ошибку можно будет отловить:
class OnlyStack {
OnlyStack() {
if (rand() % 2) {
std::cout << "ERROR" << std::endl;
}
}
public:
static std::optional<OnlyStack> Create() { return {}; }
};"На приватных конструкторах мы уже собаку съели. Даешь способ по-интереснее!"
Хорошо. Давайте будем радикальными. Просто удалим перегрузку оператора new для этого класса. Тогда вообще никто не будет в состоянии объект на куче создать:
class OnlyStack {
public:
OnlyStack() = default;
~OnlyStack() = default;
static void* operator new(std::size_t) = delete;
static void* operator new = delete;
};
OnlyStack obj; // OK
OnlyStack* p = new OnlyStack(); // ERRORВот так просто и без дополнительных приседаний.
Но можно играть не радикально, а хитро. Не удалим, а переопределим operator new так, чтобы он размещал объекты на готовом существующем статическом буфере. Реально рабочий примерчик довольно большой будет, плюс чтобы не углубляться в пучины кастомных аллокаторов, покажем только идею:
class PooledObject {
static char pool[1024];
static size_t offset;
public:
void* operator new(size_t s) {
if (offset + s > 1024) throw std::bad_alloc();
void* ptr = pool + offset;
offset += s;
return ptr;
}
void operator delete(void*) noexcept {
// Complicated logic
// or just ignore freeing memory
}
};
PooledObject* obj = new PooledObject();Типа линейный аллокатор, при запросе нового объекта просто сдвигаем offset буфера.
Это конечно все интересно. Но вспомните пост, где мы разбирали вопрос "Где аллоцируются элементы std::array?" и задумайтесь. А что если целевой объект будет полем другого класса, который мы создаем на куче?
Тогда его расположение будет определяться тем, где находится объемлющий объект. То есть, если мы создаём объект
Container на куче, то и все его поля, включая OnlyStack, окажутся на куче. Получается, что наш запрет на new OnlyStack не спасает от ситуации, когда OnlyStack становится членом другого класса, который кто-то создаёт через new.class OnlyStack {
OnlyStack() = default;
public:
static OnlyStack Create() { return {}; }
};
struct Container {
Container() : obj{OnlyStack::Create()} {}
OnlyStack obj;
};
Container* p = new Container(); // OKПишите в комментах, если знаете, как обойти эту проблему)
Don't be so radical. Stay cool.
#cppcore #memory #design
👍19❤9🔥9
make_unique и приватный конструктор
#новичкам
Если вы хотите как-то по-особенному контролировать время жизни объекта, то с функциями std::make_unique и std::make_shared у вас могут быть проблемы.
Этот код не скомпилируется. Все потому что для класса std::make_unique - это внешний код и ей нужен публичный конструктор для работы.
Это можно обойти, просто использовав явный вызов new:
Но это же явный вызов new! Из всех утюгов твердят, что сырые указатели - наши враги и с ними надо вести ожесточенную войну!
Есть один вариант, как этого можно избежать(если сырые указатели вызывают у вас диарею). Давайте сделаем публичный конструктор, но сделаем один из его параметров приватным типом класса. Сделаем у подтипа приватный конструктор и добавим Type в друзья. Тогда создавать объект по-прежнему можно будет только с помощью фабричного статического метода, но разблокируется возможность использовать std::make_unique:
По сути, это та же идиома passkey из этого поста. Только здесь она раскрывается с еще одной стороны.
Спасибо комментаторам из поста про запрет создания объектов на стеке за идею для публикации!
Know your enemies. Stay cool.
#cppcore #design #memory
#новичкам
Если вы хотите как-то по-особенному контролировать время жизни объекта, то с функциями std::make_unique и std::make_shared у вас могут быть проблемы.
struct Type {
static std::unique_ptr<Type> Create() {
return std::make_unique<Type>();
}
private:
Type() = default;
};
int main()
{
auto obj = Type::Create();
}Этот код не скомпилируется. Все потому что для класса std::make_unique - это внешний код и ей нужен публичный конструктор для работы.
Это можно обойти, просто использовав явный вызов new:
struct Type {
static std::unique_ptr<Type> Create() {
return std::unique_ptr<Type>(new Type);
}
private:
Type() = default;
};
int main()
{
auto obj = Type::Create();
}Но это же явный вызов new! Из всех утюгов твердят, что сырые указатели - наши враги и с ними надо вести ожесточенную войну!
Есть один вариант, как этого можно избежать(если сырые указатели вызывают у вас диарею). Давайте сделаем публичный конструктор, но сделаем один из его параметров приватным типом класса. Сделаем у подтипа приватный конструктор и добавим Type в друзья. Тогда создавать объект по-прежнему можно будет только с помощью фабричного статического метода, но разблокируется возможность использовать std::make_unique:
class Type {
class PrivateKey { // private struct of Type
PrivateKey() = default;
friend Type;
};
public:
Type(PrivateKey) {}
static std::unique_ptr<Type> Create() {
return std::make_unique<Type>(PrivateKey{});
}
};
int main() {
auto obj = Type::Create(); // OK
auto obj2 = Type(PrivateKey{}); // ERROR: PrivateKey is private
}По сути, это та же идиома passkey из этого поста. Только здесь она раскрывается с еще одной стороны.
Спасибо комментаторам из поста про запрет создания объектов на стеке за идею для публикации!
Know your enemies. Stay cool.
#cppcore #design #memory
👍22🔥9❤7
std::terminate
#опытным
Эта функция даже звучит страшно. Как будто бы даже есть немного опасений, что придет Шварц и заберет мою одежду, если в программе она вызовется.
Большинство из вас скорее всего знает, что вызов std::terminate приводит к завершению программы из-за какой-то ошибки.
Но как именно приводит? И почему это очень плохо? - На эти вопросы постараемся сегодня ответить.
Термин "ошибка" - слишком неоднозначный. На самом деле есть как минимум 2 категории ошибок: от которых можно восстановиться и от которых нельзя.
К первой категории можно отнести ситуации, от которых можно восстановиться. Когда системные вызовы возвращают отрицательное значение. Обычно это значит, что что-то пошло не так. Но жизнь программы продолжается: мы можем сделать ретрай или вообще прекратить исполнение задачи.
Туда же можно отнести выброс исключения. Ну ничего страшного: поймаем исключение, раскрутим стек, разрушим локальные объекты и продолжим исполнение дальше.
Ситуации же из второй категории намного серьезней. Вы в принципе нарушаете правила исполнения программы. Выход за границы массива - сегфолт. Слишком глубокая рекурсия - переполнение стека.
Таких ситуаций можно придумать много, и в каком-то небольшом их подмножестве вызывается функция std::terminate.
Эта функция, которая не может бросать исключений и не возвращает никакого значения. Непонятно, как можно обработать ошибку при аварийном завершении программы. И так, как программа завершается при вызове terminate, никакой другой код не должен продолжится после этого вызова.
Внутренний механизм вызова std::terminate() достаточно общий для такого рода функций. std::terminate() не выполняет всю работу сама. Вместо этого она вызывает обработчик события "уничтожения программы". Обработчик настраивается через функцию std::set_terminate и по умолчанию там стоит вызов std::abort. А это уже функция которая немедленно посылает сигнал
В чем проблемы такого завершения программы? Ну помимо того, что мы уже довольно сильно накосячили, что вызвался std::terminate.
Не раскручивается стек и не вызываются деструкторы ни локальных, ни глобальных объектов. Не вызываются никакие cleanup операции. Так или иначе при завершении программы ОС все равно заберет себе все ресурсы, но никакие процессы, происходящие в программе, грамотно не завершаются. Не флашатся потоки ввода-вывода и буферы, не возвращается нормальный ответ на запрос, не дообрабатываются уже готовые к обработке события. Батя ушел за хлебом и не вернулся... Ни свидетелей, ни весточки.
Поэтому очевидно, что просто не надо допускать ситуаций, которые приводят к вызову std::teminate. Их конечно много, но они вполне четко определены и их довольно просто избегать. Об этих ситуациях мы и поговорим в следующем посте.
Recover from your mistakes. Stay cool.
#cppcore
#опытным
Эта функция даже звучит страшно. Как будто бы даже есть немного опасений, что придет Шварц и заберет мою одежду, если в программе она вызовется.
Большинство из вас скорее всего знает, что вызов std::terminate приводит к завершению программы из-за какой-то ошибки.
Но как именно приводит? И почему это очень плохо? - На эти вопросы постараемся сегодня ответить.
Термин "ошибка" - слишком неоднозначный. На самом деле есть как минимум 2 категории ошибок: от которых можно восстановиться и от которых нельзя.
К первой категории можно отнести ситуации, от которых можно восстановиться. Когда системные вызовы возвращают отрицательное значение. Обычно это значит, что что-то пошло не так. Но жизнь программы продолжается: мы можем сделать ретрай или вообще прекратить исполнение задачи.
Туда же можно отнести выброс исключения. Ну ничего страшного: поймаем исключение, раскрутим стек, разрушим локальные объекты и продолжим исполнение дальше.
Ситуации же из второй категории намного серьезней. Вы в принципе нарушаете правила исполнения программы. Выход за границы массива - сегфолт. Слишком глубокая рекурсия - переполнение стека.
Таких ситуаций можно придумать много, и в каком-то небольшом их подмножестве вызывается функция std::terminate.
[[noreturn]] void terminate() noexcept;
Эта функция, которая не может бросать исключений и не возвращает никакого значения. Непонятно, как можно обработать ошибку при аварийном завершении программы. И так, как программа завершается при вызове terminate, никакой другой код не должен продолжится после этого вызова.
Внутренний механизм вызова std::terminate() достаточно общий для такого рода функций. std::terminate() не выполняет всю работу сама. Вместо этого она вызывает обработчик события "уничтожения программы". Обработчик настраивается через функцию std::set_terminate и по умолчанию там стоит вызов std::abort. А это уже функция которая немедленно посылает сигнал
SIGABRT текущему процессу. Стандартная реакция на этот сигнал — аварийное завершение программы.В чем проблемы такого завершения программы? Ну помимо того, что мы уже довольно сильно накосячили, что вызвался std::terminate.
Не раскручивается стек и не вызываются деструкторы ни локальных, ни глобальных объектов. Не вызываются никакие cleanup операции. Так или иначе при завершении программы ОС все равно заберет себе все ресурсы, но никакие процессы, происходящие в программе, грамотно не завершаются. Не флашатся потоки ввода-вывода и буферы, не возвращается нормальный ответ на запрос, не дообрабатываются уже готовые к обработке события. Батя ушел за хлебом и не вернулся... Ни свидетелей, ни весточки.
Поэтому очевидно, что просто не надо допускать ситуаций, которые приводят к вызову std::teminate. Их конечно много, но они вполне четко определены и их довольно просто избегать. Об этих ситуациях мы и поговорим в следующем посте.
Recover from your mistakes. Stay cool.
#cppcore
👍25❤9🔥5🤣1