Что происходит до 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
❤🔥44👍16❤11🔥6👎1🤔1
Вызываем функцию в глобальном скоупе
В отличие от С в С++ можно вызывать код до входа в 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
👍57🔥15❤🔥7❤3
Принтуем уникальный указатель
#новичкам
Умные указатели делают нашу жизнь намного проще. Они не только такие умные, что нам не стоит беспокоиться о менеджменте памяти. Они еще и очень удобные с точки зрения использования. Мы пользуемся умным указателем почти также, как мы бы пользовались обычным указателем. Все операторы перегружены так, чтобы мы вообще не видели разницу.
И мы понимаем, что это обертка, когда нам нужно достать сам сырой указатель и вызвать метод 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
🔥40👍19❤11❤🔥2👎1🤣1
Различаем преинкремент и постинкремент
#новичкам
Новичкам всегда не просто даются перегрузки операторов. Не то, чтобы кому-то часто приходится перегружать операторы. Но уж эти *нкременты точно редко. Из-за этого особенности постоянно забываются. Давайте же во всем разберемся.
Есть операторы инкремента и декремента. Первые увеличивают значение на какую-то условную единицк, вторые - уменьшаю. Они подразделяются на 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
🔥37👍19❤7😁5👎2❤🔥1
BUG
Еще одно слово, которое мы постоянно используем, вроде как знаем его значение, но понятие не имеем о его ориджине. Даже те, кто знает английский, не сразу доходят до скрытых взаимосвязей.
Дело в том, что на инглише "bug" - это жук и иногда какое-то обобщенное название рандомного насекомого.
И как жуки связаны с программными ошибками?
Словом "баг" еще в позапрошлом столетии нарекали какие-то неполадки с электротехникой. В те времена эти технологии были "новинкой" и страдали от всяких детских проблем. Поэтому скорее всего эти проблемы сравнивали с жуками из-за их схожести с маленькими, надоедливыми насекомыми, мешающими работать. Они постоянно где-то прячутся, неожиданно появляются и непонятно откуда вылезают. Томас Эдисон писал:
Поэтому такое значение слова было в принципе всегда известно. Но по отношению к программной ошибке термин стал употребляться после одного знаменитого случая(по одной из версий).
В 1946 году в работе компьютера Марк-2 ( Harvard Mark-II) обнаружили ошибку. Тогда ЭВМ были размером с баттхертом либералов после победы Трампа. Все работало на больших элементах, лампах и реле и время от времени какой-то из элементов выходил из строя. Это вполне нормальная тема для тех лет. И в этот раз научный сотрудник Грейс Хоппер отследила проблему и была удивлена, обнаружив сгоревшего мотылька, попавшего на контакты. Бездыханный трупик мотылька был извлечен и приклеен к отчету липкой лентой с комментарием «First actual case of bug being found.» («Первый реальный случай нахождения жучка»). Press F.
После этого случая термин разлетелся из уст в уста. Тогда программисты занимались не только софтиной, но и плотно работали с железками и, в принципе, с электроникой. Им уже было знакомо называть багами проблемы в технике. Теперь к значению добавились и программные ошибки.
Так этот жук стал самым знаменитым жуком в мире и увековечил память о себе в умах программистов и не только.
Можно кстати сделать один небольшой шажок вперед и понять, что примерно оттуда же растут ноги у слова «дебаггер» (debugger) – буквально «избавитель от жучков». Так что у нас есть что-то общее с дезинсекторами...
Dig to the origin. Stay cool.
#fun
Еще одно слово, которое мы постоянно используем, вроде как знаем его значение, но понятие не имеем о его ориджине. Даже те, кто знает английский, не сразу доходят до скрытых взаимосвязей.
Дело в том, что на инглише "bug" - это жук и иногда какое-то обобщенное название рандомного насекомого.
И как жуки связаны с программными ошибками?
Словом "баг" еще в позапрошлом столетии нарекали какие-то неполадки с электротехникой. В те времена эти технологии были "новинкой" и страдали от всяких детских проблем. Поэтому скорее всего эти проблемы сравнивали с жуками из-за их схожести с маленькими, надоедливыми насекомыми, мешающими работать. Они постоянно где-то прячутся, неожиданно появляются и непонятно откуда вылезают. Томас Эдисон писал:
Так было со всеми моими изобретениями. Первый шаг — интуиция, которая приходит как вспышка, затем возникают трудности — устройство отказывается работать, и именно тогда проявляются „жучки“ — как называют эти мелкие ошибки и трудности — и требуются месяцы пристального наблюдения, исследований и усилий, прежде чем дело дойдёт до коммерческого успеха или неудачи.
Поэтому такое значение слова было в принципе всегда известно. Но по отношению к программной ошибке термин стал употребляться после одного знаменитого случая(по одной из версий).
В 1946 году в работе компьютера Марк-2 ( Harvard Mark-II) обнаружили ошибку. Тогда ЭВМ были размером с баттхертом либералов после победы Трампа. Все работало на больших элементах, лампах и реле и время от времени какой-то из элементов выходил из строя. Это вполне нормальная тема для тех лет. И в этот раз научный сотрудник Грейс Хоппер отследила проблему и была удивлена, обнаружив сгоревшего мотылька, попавшего на контакты. Бездыханный трупик мотылька был извлечен и приклеен к отчету липкой лентой с комментарием «First actual case of bug being found.» («Первый реальный случай нахождения жучка»). Press F.
После этого случая термин разлетелся из уст в уста. Тогда программисты занимались не только софтиной, но и плотно работали с железками и, в принципе, с электроникой. Им уже было знакомо называть багами проблемы в технике. Теперь к значению добавились и программные ошибки.
Так этот жук стал самым знаменитым жуком в мире и увековечил память о себе в умах программистов и не только.
Можно кстати сделать один небольшой шажок вперед и понять, что примерно оттуда же растут ноги у слова «дебаггер» (debugger) – буквально «избавитель от жучков». Так что у нас есть что-то общее с дезинсекторами...
Dig to the origin. Stay cool.
#fun
❤🔥34👍10🔥9❤6👎3⚡1
Может ли дружественная функция быть виртуальной?
#опытным
Еще один вопрос из серии "а могут ли рыбы быть рыжими?" - "Да вроде как по цвету могут, но рыжие это обычно про волосы, а волос у рыб нет. Да и вообще, кому это знание нужно?!". Что-то типа такого. Но раз такие вопросы задают, то пора в ваше кунг-фу добавить и этот приемчик.
Давайте начнем с начала.
Дружественная функция - обычная функция, которая имеет доступ к полям класса-друга. И все. Никаких других особенностей у нее нет. И ограничений тоже никаких нет. Ее сигнатура может быть любой. Объект класса-друга может в ней вообще не фигурировать.
А для виртуальности необходим экземпляр класса, в котором укромно затаился vptr, который и отвечает за диспатчинг вызовов в рантайме.
Вот и получается, что виртуальных дружественных функций не бывает.
Конечно вопрос о виртуальных friend-функциях может заходить только тогда, когда в параметрах есть ссылка или указатель на объект полиморфного класса. Но в общем случае на это не накладываются ограничения. Поэтому и в общем случае виртуальных подруг у класса не бывает.
Теперь сможете смело парировать интервьюеровчетким ударом в глазницу быстрым точным ответом
Но осадочек-то остался. Хочется менять работу дружественной функции в зависимости от того, какой конкретный объект ей передается. Такое мероприятие можно организовать достаточно просто, но это в следующем посте.
Have a real friends. Stay cool.
#interview #cppcore
#опытным
Еще один вопрос из серии "а могут ли рыбы быть рыжими?" - "Да вроде как по цвету могут, но рыжие это обычно про волосы, а волос у рыб нет. Да и вообще, кому это знание нужно?!". Что-то типа такого. Но раз такие вопросы задают, то пора в ваше кунг-фу добавить и этот приемчик.
Давайте начнем с начала.
Дружественная функция - обычная функция, которая имеет доступ к полям класса-друга. И все. Никаких других особенностей у нее нет. И ограничений тоже никаких нет. Ее сигнатура может быть любой. Объект класса-друга может в ней вообще не фигурировать.
А для виртуальности необходим экземпляр класса, в котором укромно затаился vptr, который и отвечает за диспатчинг вызовов в рантайме.
Вот и получается, что виртуальных дружественных функций не бывает.
Конечно вопрос о виртуальных friend-функциях может заходить только тогда, когда в параметрах есть ссылка или указатель на объект полиморфного класса. Но в общем случае на это не накладываются ограничения. Поэтому и в общем случае виртуальных подруг у класса не бывает.
Теперь сможете смело парировать интервьюеров
Но осадочек-то остался. Хочется менять работу дружественной функции в зависимости от того, какой конкретный объект ей передается. Такое мероприятие можно организовать достаточно просто, но это в следующем посте.
Have a real friends. Stay cool.
#interview #cppcore
😁38❤🔥13👍9🔥4❤1
Виртуальная дружественная функция
#новичкам
Вчера мы поговорили о том, что таких функций не бывает, но очень хочется получить альтернативу. Хочется изменять работу функции в зависимости от динамического типа, который в нее передается.
Ну и на самом деле ничего сложного здесь нет. Можно ведь сделать полиморфный метод, который будет вызываться в этой функции. И вот его поведение мы можем изменять примерно как нашей душеньке захочется.
Рассмотрим банальный пример - сериализация объекта в поток. Обычно для такой задачи используют дружественный оператор <<. Но вот хотелось бы сериализовать в зависимости от динамического типа. Фигня вопрос.
Есть у нас класс человека с его именем и фамилией. Есть класс налогоплательщика, который наследуется от человека и добавляет к полям ИНН. Допустим, мы хотим как-то выводить на консоль содержимое этих людей(в смысле поля; расчленением мы не занимаемся, не в Питере живем все-таки). Можно конечно в каждом классе определять свой оператор или выводить на консоль результат работы метода 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
❤🔥24👍16❤4🔥2😭2🤔1
Возвращаем ссылку в std::optional
#новичкам
В прошлом посте я упоминал, что методы front и back последовательных контейнеров играют в ящик, если их пытаться вызвать на пустом контейнере. Это приводит к UB.
Один из довольно известных приемов для обработки таких ситуаций - возвращать не ссылку на объект, а std::optional. Это такая фича С++17 и класс, который может хранить или не хранить объект.
Теперь, если контейнер пустой - можно возвращать std::nullopt, который создает std::optional без объекта внутри. Если в нем есть элементы, то возвращать ссылку.
Только вот проблема: std::optional нельзя создавать с ссылочным типом. А копировать объект ну вот никак не хочется. А если он очень тяжелый? Мы программисты и тяжести поднимать не любим.
И вроде бы ситуация безвыходная. Но нет! Решение есть!
Можно возвращать std::optional<std::reference_wrapper<T>>. std::reference_wrapper - это такая обертка над ссылками, чтобы они вели себя как кошерные объекты со своими блэкджеком, конструкторами, деструкторами и прочими прелестями.
Это абсолютно легально и теперь у вас никакого копирования нет!
И в добавок есть нормальная безопасная проверка.
Пользуйтесь.
Stay safe. Stay cool.
#cpp17 #STL #goodpractice
#новичкам
В прошлом посте я упоминал, что методы front и back последовательных контейнеров играют в ящик, если их пытаться вызвать на пустом контейнере. Это приводит к UB.
Один из довольно известных приемов для обработки таких ситуаций - возвращать не ссылку на объект, а std::optional. Это такая фича С++17 и класс, который может хранить или не хранить объект.
Теперь, если контейнер пустой - можно возвращать std::nullopt, который создает std::optional без объекта внутри. Если в нем есть элементы, то возвращать ссылку.
Только вот проблема: std::optional нельзя создавать с ссылочным типом. А копировать объект ну вот никак не хочется. А если он очень тяжелый? Мы программисты и тяжести поднимать не любим.
И вроде бы ситуация безвыходная. Но нет! Решение есть!
Можно возвращать std::optional<std::reference_wrapper<T>>. std::reference_wrapper - это такая обертка над ссылками, чтобы они вели себя как кошерные объекты со своими блэкджеком, конструкторами, деструкторами и прочими прелестями.
Это абсолютно легально и теперь у вас никакого копирования нет!
std::optional<std::reference_wrapper<T>> Container::front() {
if (data_.empty()) {
return std::nullopt;
}
return std::ref(data_[0]);
}
И в добавок есть нормальная безопасная проверка.
Пользуйтесь.
Stay safe. Stay cool.
#cpp17 #STL #goodpractice
🔥42👍11❤6🏆3❤🔥1🤪1
Дедлокаем код
#новичкам
Частый вопрос на собесах или даже на скринингах - сколько минимум нужно мьютексов, чтобы гарантировано скрафтить дедлок.
Прежде, чем приступим к разбору, небольшое введение.
Deadlock - это ситуация, когда два или более потока оказываются заблокированными и не могут продолжить свою работу, так как каждый из них ожидает ресурс, который удерживает другой заблокированный поток. В результате, ни один из потоков не может завершиться, а система оказывается в застойном состоянии.
Многопоточка - это мир магии и чудес. Ошибки, которые появляются по причине плохой организации доступа потоков к ресурсам - самые трудновоспроизводимые и сложные для отладки. И никогда не знаешь откуда выстрелит. Да и сами ошибки принимают причудливые облики. Поэтому дедлоки могут по разному быть проявлены в коде, но поведение программы всегда при этом стабильно убогое - потоки не могут дальше продолжать производить работу.
В рамках текущего поста будем обсуждать так называемую циклическую блокировку. Ща поймете, о чем речь.
Стандартный ответ на вопрос из начала - 2. В одном потоке блочим в начале первый мьютекс, потом второй, а в другом потоке наоборот. Таким образом может произойти ситуация, когда оба потока захватили в заложники каждый по одному мьютексу и бесконечно висят в ожидании освобождения второго. Этого никогда не произойдет, потому что каждому нужен замок, который уже захватил другой поток и он его отдавать не собирается, пока пройдет всю критическую секцию. А он ее никогда не пройдет. В общем, собака пытается укусить себя за чресла.
Самый простой пример, который иллюстирует эту ситуацию:
Все канонично. В двух разных потоках пытаемся залочить замки в разном порядке. В итоге вывод будет такой:
И дальше программа будет бесконечно простаивать без дела, как тостер на вашей кухне.
Do useful work. Stay cool.
#concurrency
#новичкам
Частый вопрос на собесах или даже на скринингах - сколько минимум нужно мьютексов, чтобы гарантировано скрафтить дедлок.
Прежде, чем приступим к разбору, небольшое введение.
Deadlock - это ситуация, когда два или более потока оказываются заблокированными и не могут продолжить свою работу, так как каждый из них ожидает ресурс, который удерживает другой заблокированный поток. В результате, ни один из потоков не может завершиться, а система оказывается в застойном состоянии.
Многопоточка - это мир магии и чудес. Ошибки, которые появляются по причине плохой организации доступа потоков к ресурсам - самые трудновоспроизводимые и сложные для отладки. И никогда не знаешь откуда выстрелит. Да и сами ошибки принимают причудливые облики. Поэтому дедлоки могут по разному быть проявлены в коде, но поведение программы всегда при этом стабильно убогое - потоки не могут дальше продолжать производить работу.
В рамках текущего поста будем обсуждать так называемую циклическую блокировку. Ща поймете, о чем речь.
Стандартный ответ на вопрос из начала - 2. В одном потоке блочим в начале первый мьютекс, потом второй, а в другом потоке наоборот. Таким образом может произойти ситуация, когда оба потока захватили в заложники каждый по одному мьютексу и бесконечно висят в ожидании освобождения второго. Этого никогда не произойдет, потому что каждому нужен замок, который уже захватил другой поток и он его отдавать не собирается, пока пройдет всю критическую секцию. А он ее никогда не пройдет. В общем, собака пытается укусить себя за чресла.
Самый простой пример, который иллюстирует эту ситуацию:
std::mutex m1;
std::mutex m2;
std::thread t1([&m1, &m2] {
std::cout << "Thread 1. Acquiring m1." << std::endl;
m1.lock();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::cout << "Thread 1. Acquiring m2." << std::endl;
m2.lock();
std::cout << "Thread 1 perform some work" << std::endl;
});
std::thread t2([&m1, &m2] {
std::cout << "Thread 2. Acquiring m2." << std::endl;
m2.lock();
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::cout << "Thread 2. Acquiring m1." << std::endl;
m1.lock();
std::cout << "Thread 2 perform some work" << std::endl;
});
t1.join();
t2.join();
Все канонично. В двух разных потоках пытаемся залочить замки в разном порядке. В итоге вывод будет такой:
Thread 1. Acquiring m1.
Thread 2. Acquiring m2.
Thread 2. Acquiring m1.
Thread 1. Acquiring m2.
И дальше программа будет бесконечно простаивать без дела, как тостер на вашей кухне.
Do useful work. Stay cool.
#concurrency
👍28❤6🔥4😁1
Зачем локать мьютексы в разном порядке
#новичкам
Можете подумать, что раз всем так известно, что мьютексы опасно лочить в разном порядке, то почему вообще кому-то в голову может прийти наступить на грабли и накалякать такое своими программисткими пальчиками снова? Просто не пишите хреновый код и будет вам счастье.
Дело в том, что иногда это не совсем очевидно. Точнее почти всегда это не очевидно.
Программисты хоть и разные бывают, но ревью никто не отменял и такую броскую ошибку бы явно заметили. Поэтому не все так просто, как на первый взгляд кажется.
Разберем чуть более сложный пример:
Все просто. У нас есть пара объектов, которые используются в качестве разделяемых ресурсов (их могут изменять более 1 потока). Объекты могут обмениваться между собой данными.
Ну и по всем канонам нам же надо защитить оба объекта при свопе. Поэтому после захода в функцию обмена сначала лочим свой мьютекс, а потом мьютекс объекта с которым собираемся свопаться.
И вроде снаружи кажется, что мы всегда лочим в одном порядке. Пока мы не будем обменивать данные одних и тех же объектов в разных потоках в разном порядке. Тогда первый поток может залочить мьютекс первого объекта и, допустим, заснуть. А второй поток первым залочит мьютекс второго объекта. И все. Приплыли.
И это все еще может показаться детским садом и очевидушкой. Только вот сам момент взятия замка уже не выглядит так подозрительно. А сами объекты могут лежать в каких-то страшных структурах данных и могут быть разбросаны по коду. В таком случае все не так очевидно становится.
В следующий раз поговорим, как же не допускать такие ошибки.
Don't be obvious. Stay cool.
#concurrency
#новичкам
Можете подумать, что раз всем так известно, что мьютексы опасно лочить в разном порядке, то почему вообще кому-то в голову может прийти наступить на грабли и накалякать такое своими программисткими пальчиками снова? Просто не пишите хреновый код и будет вам счастье.
Дело в том, что иногда это не совсем очевидно. Точнее почти всегда это не очевидно.
Программисты хоть и разные бывают, но ревью никто не отменял и такую броскую ошибку бы явно заметили. Поэтому не все так просто, как на первый взгляд кажется.
Разберем чуть более сложный пример:
struct SomeSharedResource {
void swap(SomeSharedResource& obj) {
{
std::lock_guard lg{mtx};
// just for results reproducing
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::lock_guard lg1{obj.mtx};
// handle swap
}
}
std::mutex mtx;
};
int main() {
SomeSharedResource resource1;
SomeSharedResource resource2;
std::mutex m2;
std::thread t1([&resource1, &resource2] {
resource1.swap(resource2);
std::cout << "1 Do some work" << std::endl;
});
std::thread t2([&resource1, &resource2] {
resource2.swap(resource1);
std::cout << "2 Do some work" << std::endl;
});
t1.join();
t2.join();
}
Все просто. У нас есть пара объектов, которые используются в качестве разделяемых ресурсов (их могут изменять более 1 потока). Объекты могут обмениваться между собой данными.
Ну и по всем канонам нам же надо защитить оба объекта при свопе. Поэтому после захода в функцию обмена сначала лочим свой мьютекс, а потом мьютекс объекта с которым собираемся свопаться.
И вроде снаружи кажется, что мы всегда лочим в одном порядке. Пока мы не будем обменивать данные одних и тех же объектов в разных потоках в разном порядке. Тогда первый поток может залочить мьютекс первого объекта и, допустим, заснуть. А второй поток первым залочит мьютекс второго объекта. И все. Приплыли.
И это все еще может показаться детским садом и очевидушкой. Только вот сам момент взятия замка уже не выглядит так подозрительно. А сами объекты могут лежать в каких-то страшных структурах данных и могут быть разбросаны по коду. В таком случае все не так очевидно становится.
В следующий раз поговорим, как же не допускать такие ошибки.
Don't be obvious. Stay cool.
#concurrency
❤22👍16🔥4❤🔥2🍾1
Локаем много мьютексов
#опытным
Cтандартное решение этой проблемы дедлока из постов выше - лочить замки в одном и том же порядке во всех потоках. Но как это сделать? Они не же на физре, "по порядку рассчитайсьььь" не делали.
Можно конечно на ифах городить свой порядок на основе, например, адресов мьютексов. Но это какие-то костыли и так делать не надо.
Так как проблема довольно стандартная, то и решение мы скорее всего найдем в том же стандарте.
std::scoped_lock был введен в С++17 и представляет собой RAII обертку над локом множества мьютексов. Можно сказать, что это std::lock_guard на максималках. То есть буквально, это обертка, которая лочит любое количество мьютексов от 0 до "сами проверьте верхнюю границу".
Но есть один важный нюанс. Никак не гарантируется порядок, в котором будут блокироваться замки. Гарантируется лишь то, что выбранный порядок не будет приводить к dead-lock'у.
Пример из прошлого поста может выглядеть теперь вот так:
И все. И никакого дедлока.
Однако немногое лишь знают, что std::scoped_lock - это не только RAII-обертка. Это еще и более удобная обертка над "старой" функцией из С++11 std::lock.
О ней мы поговорим в следующий раз. Ведь не всем доступны самые современные стандарты. Да и легаси код всегда есть.
Be comfortable to work with. Stay cool.
#cpp17 #cpp11 #concurrency
#опытным
Cтандартное решение этой проблемы дедлока из постов выше - лочить замки в одном и том же порядке во всех потоках. Но как это сделать? Они не же на физре, "по порядку рассчитайсьььь" не делали.
Можно конечно на ифах городить свой порядок на основе, например, адресов мьютексов. Но это какие-то костыли и так делать не надо.
Так как проблема довольно стандартная, то и решение мы скорее всего найдем в том же стандарте.
std::scoped_lock был введен в С++17 и представляет собой RAII обертку над локом множества мьютексов. Можно сказать, что это std::lock_guard на максималках. То есть буквально, это обертка, которая лочит любое количество мьютексов от 0 до "сами проверьте верхнюю границу".
Но есть один важный нюанс. Никак не гарантируется порядок, в котором будут блокироваться замки. Гарантируется лишь то, что выбранный порядок не будет приводить к dead-lock'у.
Пример из прошлого поста может выглядеть теперь вот так:
struct SomeSharedResource {
void swap(SomeSharedResource& obj) {
{
std::scoped_lock lg{mtx, obj.mtx};
// handle swap
}
}
std::mutex mtx;
};
int main() {
SomeSharedResource resource1;
SomeSharedResource resource2;
std::mutex m2;
std::thread t1([&resource1, &resource2] {
resource1.swap(resource2);
std::cout << "1 Do some work" << std::endl;
});
std::thread t2([&resource1, &resource2] {
resource2.swap(resource1);
std::cout << "2 Do some work" << std::endl;
});
t1.join();
t2.join();
}
И все. И никакого дедлока.
Однако немногое лишь знают, что std::scoped_lock - это не только RAII-обертка. Это еще и более удобная обертка над "старой" функцией из С++11 std::lock.
О ней мы поговорим в следующий раз. Ведь не всем доступны самые современные стандарты. Да и легаси код всегда есть.
Be comfortable to work with. Stay cool.
#cpp17 #cpp11 #concurrency
❤24🔥15👍8❤🔥2👎1
std::lock
#опытным
Сейчас уже более менее опытные разрабы знают про std::scoped_lock и как он решает проблему блокировки множества мьютексов. Однако и в более старом стандарте С++11 есть средство, позволяющее решать ту же самую проблему. Более того std::scoped_lock - это всего лишь более удобная обертка над этим средством. Итак, std::lock.
Эта функция блокирует 2 и больше объектов, чьи типы удовлетворяют требованию Locable. То есть для них определены методы lock(), try_lock() и unlock() с соответствующей семантикой.
Причем порядок, в котором блокируются объекты - не определен. В стандарте сказано, что объекты блокируются с помощью неопределенной серии вызовов методов lock(), try_lock и unlock(). Однако гарантируется, что эта серия вызовов не может привести к дедлоку. Собстна, для этого все и затевалось.
Штука эта полезная, но не очень удобная. Сами посудите. Эта функция просто блокирует объекты, но не отпускает их. И это в эпоху RAII. Ай-ай-ай.
Поэтому ее безопасное использование выглядит несколько вычурно:
Раз мы все-таки за безопасность и полезные практики, то нам приходится использовать std::unique_lock'и на мьютексах. Только нужно передать туда параметр std::defer_lock, который говорит, что не нужно локать замки при создании unique_lock'а, его залочит кто-то другой. Тем самым мы убиваем 2-х зайцев: и RAII используем для автоматического освобождения мьютексов, и перекладываем ответственность за блокировку замков на std::lock.
Можно использовать и более простую обертку, типа std::lock_guard:
Здесь мы тоже используем непопулярный конструктор std::lock_guard: передаем в него параметр std::adopt_lock, который говорит о том, что мьютекс уже захвачен и его не нужно локать в конструкторе lock_guard.
Можно и ручками вызвать .unlock() у каждого замка, но это не по-православному.
Использование unique_lock может быть оправдано соседством с условной переменной, но если вам доступен C++17, то естественно лучше использовать std::scoped_lock.
Use modern things. Stay cool.
#cpp11 #cpp17 #concurrency
#опытным
Сейчас уже более менее опытные разрабы знают про std::scoped_lock и как он решает проблему блокировки множества мьютексов. Однако и в более старом стандарте С++11 есть средство, позволяющее решать ту же самую проблему. Более того std::scoped_lock - это всего лишь более удобная обертка над этим средством. Итак, std::lock.
Эта функция блокирует 2 и больше объектов, чьи типы удовлетворяют требованию Locable. То есть для них определены методы lock(), try_lock() и unlock() с соответствующей семантикой.
Причем порядок, в котором блокируются объекты - не определен. В стандарте сказано, что объекты блокируются с помощью неопределенной серии вызовов методов lock(), try_lock и unlock(). Однако гарантируется, что эта серия вызовов не может привести к дедлоку. Собстна, для этого все и затевалось.
Штука эта полезная, но не очень удобная. Сами посудите. Эта функция просто блокирует объекты, но не отпускает их. И это в эпоху RAII. Ай-ай-ай.
Поэтому ее безопасное использование выглядит несколько вычурно:
struct SomeSharedResource {
void swap(SomeSharedResource& obj) {
{
// !!!
std::unique_lock<std::mutex> lk_c1(mtx, std::defer_lock);
std::unique_lock<std::mutex> lk_c2(obj.mtx, std::defer_lock);
std::lock(mtx, obj.mtx);
// handle swap
}
}
std::mutex mtx;
};
int main() {
SomeSharedResource resource1;
SomeSharedResource resource2;
std::mutex m2;
std::thread t1([&resource1, &resource2] {
resource1.swap(resource2);
std::cout << "1 Do some work" << std::endl;
});
std::thread t2([&resource1, &resource2] {
resource2.swap(resource1);
std::cout << "2 Do some work" << std::endl;
});
t1.join();
t2.join();
}
Раз мы все-таки за безопасность и полезные практики, то нам приходится использовать std::unique_lock'и на мьютексах. Только нужно передать туда параметр std::defer_lock, который говорит, что не нужно локать замки при создании unique_lock'а, его залочит кто-то другой. Тем самым мы убиваем 2-х зайцев: и RAII используем для автоматического освобождения мьютексов, и перекладываем ответственность за блокировку замков на std::lock.
Можно использовать и более простую обертку, типа std::lock_guard:
struct SomeSharedResource {
void swap(SomeSharedResource& obj) {
{
// !!!
std::lock(mtx, obj.mtx);
std::lock_guard<std::mutex> lk_c1(mtx, std::adopt_lock);
std::lock_guard<std::mutex> lk_c2(obj.mtx, std::adopt_lock);
// handle swap
}
}
std::mutex mtx;
};
Здесь мы тоже используем непопулярный конструктор std::lock_guard: передаем в него параметр std::adopt_lock, который говорит о том, что мьютекс уже захвачен и его не нужно локать в конструкторе lock_guard.
Можно и ручками вызвать .unlock() у каждого замка, но это не по-православному.
Использование unique_lock может быть оправдано соседством с условной переменной, но если вам доступен C++17, то естественно лучше использовать std::scoped_lock.
Use modern things. Stay cool.
#cpp11 #cpp17 #concurrency
😁29🔥14👍7❤4
Спасибо всем, кто поделился своими лав-стори в комментах.
Такие разные истории, но в то же время одинаковые.
И во всех них сквозит одна мысль: "Двигайся по зову своего внутреннего огня. Он приведет тебя к цели"
Если что-то не нравится, никогда не поздно это поменять. Если что-то нравится, делай это и жизнь будет радостней. И это касается не только программирования естественно.
Рад, что у нас здесь собрались такие разные люди под одной двойнопозитивной крышей.
Просто решил поделиться с вами своими эмоциями.
Всем продуктивного дня!
Be passionate. Stay cool.
Такие разные истории, но в то же время одинаковые.
И во всех них сквозит одна мысль: "Двигайся по зову своего внутреннего огня. Он приведет тебя к цели"
Если что-то не нравится, никогда не поздно это поменять. Если что-то нравится, делай это и жизнь будет радостней. И это касается не только программирования естественно.
Рад, что у нас здесь собрались такие разные люди под одной двойнопозитивной крышей.
Просто решил поделиться с вами своими эмоциями.
Всем продуктивного дня!
Be passionate. Stay cool.
❤🔥53❤12🔥8👍3
Порядок взятия замков. Ч1
#опытным
В этом посте я намеренно совершил одну ошибку, как байт на комменты и следующие посты. Но вы как-то пропустили ее, хотя и разбирали ту же самую тему в комментариях.
На самом деле залочивание мьютексов в одном и том же порядке - не общепринятая концепция решения проблемы блокировки множества замков. Это лишь одна из стратегий. И она не используется в стандартной библиотеке!
Когда-то у меня тоже была уверенность, что std::scoped_lock блочит мьютексы в порядоке их адресов. Условно, в начале лочим замок с меньшим адресом. Потом с большим и так далее.
Но как я и написал в середине того же поста, что std::scoped_lock вообще не гарантирует никакого порядка залочивания. Гарантируется только что такой порядок не может привести к дедлоку.
Давайте посмотрим на следующий пример:
Все довольно просто. Определяем класс-обертку вокруг std::mutex, который позволит нам логировать все операции с ним, указывая идентификатор потока. Определяем все методы, включая try_lock, чтобы MyLock можно было использовать с std::scoped_lock.
Также определяем 2 функции, которые будут запускаться в разных потоках и пытаться локнуть сразу 3 замка. И блокируют они их в разных порядках. Все это в циклах, чтобы какую-то статистику иметь. С потоками сложно детерминировано общаться.
Запускаем это дело и смотрим на вывод консоли. Там будет огромное полотно текста, но вы сможете заметить в нем "несостыковочку" с теорией про блокировку по адресам. Возможный кусочек вывода:
Тут наглядно показано, что мьютексы лочатся в противоположном порядке в разных потоках. И в коде мьютексы передаются в скоупд лок в противоположном порядке. А значит дело тут не в адресах, а в чем-то другом. О этом в следующий раз.
Don't get fooled. Stay cool.
#concurrency #cpp17
#опытным
В этом посте я намеренно совершил одну ошибку, как байт на комменты и следующие посты. Но вы как-то пропустили ее, хотя и разбирали ту же самую тему в комментариях.
Cтандартное решение этой проблемы дедлока из постов выше - лочить замки в одном и том же порядке во всех потоках.
На самом деле залочивание мьютексов в одном и том же порядке - не общепринятая концепция решения проблемы блокировки множества замков. Это лишь одна из стратегий. И она не используется в стандартной библиотеке!
Когда-то у меня тоже была уверенность, что std::scoped_lock блочит мьютексы в порядоке их адресов. Условно, в начале лочим замок с меньшим адресом. Потом с большим и так далее.
Но как я и написал в середине того же поста, что std::scoped_lock вообще не гарантирует никакого порядка залочивания. Гарантируется только что такой порядок не может привести к дедлоку.
Давайте посмотрим на следующий пример:
std::mutex log_mutex;
struct MyLock {
MyLock() = default;
void lock() {
mtx.lock();
std::lock_guard lg{log_mutex};
std::cout << "Lock at address " << &mtx << " is acquired." << std::endl;
}
bool try_lock() {
auto result = mtx.try_lock();
std::lock_guard lg{log_mutex};
std::cout << std::this_thread::get_id() << " Try lock at address " << &mtx << ". " << (result ? "Success" : "Failed") << std::endl;
return result;
}
void unlock() {
mtx.unlock();
std::lock_guard lg{log_mutex};
std::cout << "Lock at address " << &mtx << " is released." << std::endl;
}
private:
std::mutex mtx;
};
MyLock lock1;
MyLock lock2;
MyLock lock3;
constexpr size_t iteration_count = 100;
void func_thread1() {
size_t i = 0;
while(i++ < iteration_count) {
{
std::lock_guard lg{log_mutex};
std::cout << std::endl << std::this_thread::get_id() << " Start acquiring thread1" << std::endl << std::endl;
}
std::scoped_lock scl{lock1, lock2, lock3};
std::lock_guard lg{log_mutex};
std::cout << std::endl << std::this_thread::get_id() << " End acquiring thread1" << std::endl << std::endl;
}
}
void func_thread2() {
size_t i = 0;
while(i++ < iteration_count) {
{
std::lock_guard lg{log_mutex};
std::cout << std::endl << std::this_thread::get_id() << " Start acquiring thread2" << std::endl << std::endl;
}
std::scoped_lock scl{lock3, lock2, lock1};
std::lock_guard lg{log_mutex};
std::cout << std::endl << std::this_thread::get_id() << " End acquiring thread2" << std::endl << std::endl;
}
}
int main() {
std::jthread thr1{func_thread1};
std::jthread thr2{func_thread2};
}
Все довольно просто. Определяем класс-обертку вокруг std::mutex, который позволит нам логировать все операции с ним, указывая идентификатор потока. Определяем все методы, включая try_lock, чтобы MyLock можно было использовать с std::scoped_lock.
Также определяем 2 функции, которые будут запускаться в разных потоках и пытаться локнуть сразу 3 замка. И блокируют они их в разных порядках. Все это в циклах, чтобы какую-то статистику иметь. С потоками сложно детерминировано общаться.
Запускаем это дело и смотрим на вывод консоли. Там будет огромное полотно текста, но вы сможете заметить в нем "несостыковочку" с теорией про блокировку по адресам. Возможный кусочек вывода:
129777453237824 Start acquiring thread1
129777453237824 Lock at address 0x595886d571e0 is acquired.
129777453237824 Try lock at address 0x595886d57220
129777453237824 Try lock at address 0x595886d57260
...
129777442752064 Start acquiring thread2
129777442752064 Lock at address 0x595886d57260 is acquired.
129777442752064 Try lock at address 0x595886d57220
129777442752064 Try lock at address 0x595886d571e0
Тут наглядно показано, что мьютексы лочатся в противоположном порядке в разных потоках. И в коде мьютексы передаются в скоупд лок в противоположном порядке. А значит дело тут не в адресах, а в чем-то другом. О этом в следующий раз.
Don't get fooled. Stay cool.
#concurrency #cpp17
🔥11👍7❤5😱3
Порядок взятия замков. Ч2
#опытным
Так в каком же порядке блокируются мьютексы в std::scoped_lock? Как я уже и говорил - в неопределенном. Но и здесь можно немного раскрыть детали.
Mutex-like объекты блочатся недетерминированной серией вызовов методов lock(), unlock() и try_lock().
Алгоритм можно представить некой игрой в поддавки. Мы пытаемся поочереди захватить мьютексы. И если на каком-то из какой-то из них занят, то мы не ждем, пока он освободится. Мы освобождаем все свои мьютексы, давая возможность другим потокам их захватить, и после этого начинаем пытаться захватывать замки заново.
Зачем так сложно?
А просто физически не может произойти ситуации, когда два потока захватили по набору замков и ждут, пока другие освободятся(а это и есть дедлок). Один из потоков точно пожертвует захваченными ресурсами в пользу другого и исполнение продолжится.
При запуске кода из предыдущего поста вы можете увидеть вот такую картину(но не гарантирую):
Надо понимать, что это многопоточка и каких-то упорядоченных логов между потоками быть не может, поэтому надо немного напрячь извилины.
(0x56aef94a31e0 - первый мьютекс, 0x56aef94a3220 - второй, 0x56aef94a3260 - третий)
Смотрим. Поток 128616222426688 локает первый замок, пытается локнуть второй и делает это успешно, а вот третий не получается. Значит он освобождает свои два и пытается начать заново. Дальше видим такую же картину - на третьем мьютексе try_lock прошел неудачно -> освобождаем имеющиеся.
Тут просыпается второй поток 128616211940928. И пишет, что он сразу заполучил третий замок.
То есть поток 128616222426688 пожертвовал своими захваченными замками в пользу потока 128616211940928.
Вот так выглядит реализация функции std::lock(которая лежит под капотом std::scoped_lock) в gcc:
Кто сможет - разберется, но что тут происходит в сущности - я описал выше.
Give something up to get something else. Stay cool.
#concurrency #cpp17
#опытным
Так в каком же порядке блокируются мьютексы в std::scoped_lock? Как я уже и говорил - в неопределенном. Но и здесь можно немного раскрыть детали.
The objects are locked by an unspecified series of calls tolock
,try_lock
, andunlock
.
Mutex-like объекты блочатся недетерминированной серией вызовов методов lock(), unlock() и try_lock().
Алгоритм можно представить некой игрой в поддавки. Мы пытаемся поочереди захватить мьютексы. И если на каком-то из какой-то из них занят, то мы не ждем, пока он освободится. Мы освобождаем все свои мьютексы, давая возможность другим потокам их захватить, и после этого начинаем пытаться захватывать замки заново.
Зачем так сложно?
А просто физически не может произойти ситуации, когда два потока захватили по набору замков и ждут, пока другие освободятся(а это и есть дедлок). Один из потоков точно пожертвует захваченными ресурсами в пользу другого и исполнение продолжится.
При запуске кода из предыдущего поста вы можете увидеть вот такую картину(но не гарантирую):
128616222426688 Lock at address 0x56aef94a31e0 is acquired.
128616222426688 Try lock at address 0x56aef94a3220. Success
128616222426688 Try lock at address 0x56aef94a3260. Failed
128616222426688 Lock at address 0x56aef94a3220 is released.
128616222426688 Lock at address 0x56aef94a31e0 is released.
128616222426688 Lock at address 0x56aef94a31e0 is acquired.
128616222426688 Try lock at address 0x56aef94a3220. Success
128616222426688 Try lock at address 0x56aef94a3260. Failed
128616222426688 Lock at address 0x56aef94a3220 is released.
128616211940928 Lock at address 0x56aef94a3260 is acquired.
128616211940928 Try lock at address 0x56aef94a3220. Success
128616211940928 Try lock at address 0x56aef94a31e0. Success
Надо понимать, что это многопоточка и каких-то упорядоченных логов между потоками быть не может, поэтому надо немного напрячь извилины.
(0x56aef94a31e0 - первый мьютекс, 0x56aef94a3220 - второй, 0x56aef94a3260 - третий)
Смотрим. Поток 128616222426688 локает первый замок, пытается локнуть второй и делает это успешно, а вот третий не получается. Значит он освобождает свои два и пытается начать заново. Дальше видим такую же картину - на третьем мьютексе try_lock прошел неудачно -> освобождаем имеющиеся.
Тут просыпается второй поток 128616211940928. И пишет, что он сразу заполучил третий замок.
На самом деле он заблочил его еще до начала этой ситуации, так как первый поток не мог залочить третий мьютекс. Просто поток 128616211940928 уснул между локом и выводом на консоль.
И дальше пытается захватить второй и первый замки и у него это успешно получается.То есть поток 128616222426688 пожертвовал своими захваченными замками в пользу потока 128616211940928.
Вот так выглядит реализация функции std::lock(которая лежит под капотом std::scoped_lock) в gcc:
template<typename _L1, typename _L2, typename... _L3>
void lock(_L1& __l1, _L2& __l2, _L3&... __l3)
{
#if __cplusplus >= 201703L
if constexpr (is_same_v<_L1, _L2> && (is_same_v<_L1, _L3> && ...))
{
constexpr int _Np = 2 + sizeof...(_L3);
unique_lock<_L1> __locks[] = {
{__l1, defer_lock}, {__l2, defer_lock}, {__l3, defer_lock}...
};
int __first = 0;
do {
__locks[__first].lock();
for (int __j = 1; __j < _Np; ++__j)
{
const int __idx = (__first + __j) % _Np;
if (!__locks[__idx].try_lock())
{
for (int __k = __j; __k != 0; --__k)
__locks[(__first + __k - 1) % _Np].unlock();
__first = __idx;
break;
}
}
} while (!__locks[__first].owns_lock());
for (auto& __l : __locks)
__l.release();
}
else
#endif
{
int __i = 0;
__detail::__lock_impl(__i, 0, __l1, __l2, __l3...);
}
}
Кто сможет - разберется, но что тут происходит в сущности - я описал выше.
Give something up to get something else. Stay cool.
#concurrency #cpp17
❤16👍11🔥8🤯6
Почему не используют стратегию блокировки по адресам?
#опытным
Точного ответа от разработчиков стандартной библиотеки мы не услышим, но я приведу некоторые рассуждения, которые могут натолкнуть на некоторые мысли.
Начнем с того, что локать один мьютекс - это норма. Все так делают, никто от этого не помер.
Проблемы и эти ваши дедлоки начинаются только тогда, когда поток по какой-то причине блокируется с уже захваченным локом.
А именно это и происходит при вызове метода lock(). Поток мытается захватить мьютекс и если не получается - блокируется до того момента, пока мьютекс не освободится.
Поэтому любая схема с последовательным вызовом методов lock() будет подвержена дедлокам.
А в схеме с упорядоченным по адресам блокировкой именно так и происходит.
Да, эта схема безопасна, если все мьютексы будут захватываться только так. Но в реальных системах все намного сложнее.
А что если в одном потоке замки будут лочиться через scoped_lock по адресной схеме, а в другом потоке - одиночно?
В этом случае настанет дедлок. Спасибо за пример Сергею Борисову.
Ну или другую ситуацию рассмотрим: есть 4 замка l1, l2, l3, l4. Поток захватил замок с самым большим адресом l4 и надолго(потенциально навсегда) заблокировался.
Но другие треды продолжают нормально работать. И они иногда захватывают пары мьютексов. Все продолжается нормально, пока один из потоков не пытается залочить l3 и l4. Из-за ордеринга захватится l3, а дальше поток будет ждать освобождения l4 aka заблокируется. Дальше другой поток будет пытаться захватить l2 и l3. Он захватит l2 и будет дожидаться l3.
Логику можно продолжать и дальше. Таким образом из-за одного мьютекса и немного поломанного потока может остановиться вся программа.
Примеры немного преувеличены, но тем не менее они говорят о том, что схема с адресами не совсем безопасна.
Так может тогда вообще не будем блокироваться при уже захваченном мьютексе? Именно это и делают в реализации стандартной библиотеки. Первый захват мьютекса происходит через обычный lock(), а остальные мьютексы пытаются заблокировать через try_lock. Можно сказать, что это lock-free взятие замка. Если мьютекс можно захватить - захватываем, если нет, то не блокируемся и дальше продолжаем исполнение. Так вот в случае, если хотя бы один try_lock для оставшихся замков вернул false, то реализация освобождает все захваченные замки и начинает попытку снова.
Такой алгоритм позволит избежать неприятных последствий обеих ситуаций, представленных выше.
ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ
#concurrency
#опытным
Точного ответа от разработчиков стандартной библиотеки мы не услышим, но я приведу некоторые рассуждения, которые могут натолкнуть на некоторые мысли.
Начнем с того, что локать один мьютекс - это норма. Все так делают, никто от этого не помер.
Проблемы и эти ваши дедлоки начинаются только тогда, когда поток по какой-то причине блокируется с уже захваченным локом.
А именно это и происходит при вызове метода lock(). Поток мытается захватить мьютекс и если не получается - блокируется до того момента, пока мьютекс не освободится.
Поэтому любая схема с последовательным вызовом методов lock() будет подвержена дедлокам.
А в схеме с упорядоченным по адресам блокировкой именно так и происходит.
Да, эта схема безопасна, если все мьютексы будут захватываться только так. Но в реальных системах все намного сложнее.
А что если в одном потоке замки будут лочиться через scoped_lock по адресной схеме, а в другом потоке - одиночно?
1 поток | 2 поток |
-------------------------------|---------------------------------|
lock(mutex2) // УСПЕШНО | |
| scoped_lock() |
| lock(mutex1) // УСПЕШНО |
| lock(mutex2) // ОЖИДАНИЕ ... |
lock(mutex1) // ОЖИДАНИЕ... | |
В этом случае настанет дедлок. Спасибо за пример Сергею Борисову.
Ну или другую ситуацию рассмотрим: есть 4 замка l1, l2, l3, l4. Поток захватил замок с самым большим адресом l4 и надолго(потенциально навсегда) заблокировался.
Но другие треды продолжают нормально работать. И они иногда захватывают пары мьютексов. Все продолжается нормально, пока один из потоков не пытается залочить l3 и l4. Из-за ордеринга захватится l3, а дальше поток будет ждать освобождения l4 aka заблокируется. Дальше другой поток будет пытаться захватить l2 и l3. Он захватит l2 и будет дожидаться l3.
Логику можно продолжать и дальше. Таким образом из-за одного мьютекса и немного поломанного потока может остановиться вся программа.
std::mutex l1, l2, l3, l4;
// Пусть я как-то гарантирую, что они пронумерованы в порядке
возрастания адресов и std::scoped_lock(sc_lock для краткости)
работает с помощью сортировки по адресам
1 поток | 2 поток | 3 поток | 4 поток
-------------|----------------|----------------|----------------
l4.lock(); | | |
//blocks here| | |
|sc_lock(l3, l4);| |
| // lock l3 | |
| // blocks on l4| |
|sc_lock(l2, l3);|
| // lock l2 |
| // blocks on l3|
| | sc_lock(l1, l2);
| // lock l1
| // blocks on l2
Примеры немного преувеличены, но тем не менее они говорят о том, что схема с адресами не совсем безопасна.
Так может тогда вообще не будем блокироваться при уже захваченном мьютексе? Именно это и делают в реализации стандартной библиотеки. Первый захват мьютекса происходит через обычный lock(), а остальные мьютексы пытаются заблокировать через try_lock. Можно сказать, что это lock-free взятие замка. Если мьютекс можно захватить - захватываем, если нет, то не блокируемся и дальше продолжаем исполнение. Так вот в случае, если хотя бы один try_lock для оставшихся замков вернул false, то реализация освобождает все захваченные замки и начинает попытку снова.
Такой алгоритм позволит избежать неприятных последствий обеих ситуаций, представленных выше.
ПРОДОЛЖЕНИЕ В КОММЕНТАРИЯХ
#concurrency
❤15👍15🔥5😱2🫡1
Еще один способ залочить много мьютексов
Последний пост из серии.
Вот у мьютекса есть метод lock, который его захватывает. А разработчики stdlib взяли и сделали функцию std::lock, которая лочит сразу несколько замков.
Также у мьютекса есть метод try_lock, который пытается в неблокирующем режиме его захватить. "Авось да получится". И видимо по аналогии с lock в стандартной библиотеке существует свободная функция std::try_lock, которая пытается захватить несколько мьютексов так же в неблокирующем режиме.
То есть в этой функции банально в цикле на каждом из переданных аргументов вызывается try_lock и, если какой-то из вызовов завершился неудачно, то все занятые замки освобождаются и возвращается индекс мьютекса, на котором выполнение зафейлилось. Если все завершилось успешно aka все замки захвачены, возвращается -1.
Чтобы с помощью std::try_lock наверняка захватить все замки, нужно крутиться в горячем цикле и постоянно вызывать std::try_lock, пока она не вернет -1.
Непонятно, зачем эта функция нужна в прикладном программировании при наличии std::scoped_lock и std::lock, которые еще и удобно оборачивают все эти циклы, скрывая все эти кишки в деталях реализации.
Единственное, что пришло в голову - реализация своего scoped_lock'а с блэкджеком и другим алгоритмом предотвращения дедлока. Этот алгоритм должен быть чем-то похож на поддавки, но как-то изменен. У кого есть кейсы применения - отпишитесь в комменты.
В общем, на мой взгляд, это пример избыточного апи. Если человек сам что-то свое мудрит, то ему особо не сложно самому цикл написать. Однако обычные пользователи стандартных примитивов синхронизации возможно никогда в своей жизни этого не применят. Все-таки стандартная библиотека должна быть сборником решения актуальных проблем реальных, а не воображаемых,людей.
Stay useful. Stay cool.
#concurrency
Последний пост из серии.
Вот у мьютекса есть метод lock, который его захватывает. А разработчики stdlib взяли и сделали функцию std::lock, которая лочит сразу несколько замков.
Также у мьютекса есть метод try_lock, который пытается в неблокирующем режиме его захватить. "Авось да получится". И видимо по аналогии с lock в стандартной библиотеке существует свободная функция std::try_lock, которая пытается захватить несколько мьютексов так же в неблокирующем режиме.
template< class Lockable1, class Lockable2, class... LockableN >
int try_lock( Lockable1& lock1, Lockable2& lock2, LockableN&... lockn );
То есть в этой функции банально в цикле на каждом из переданных аргументов вызывается try_lock и, если какой-то из вызовов завершился неудачно, то все занятые замки освобождаются и возвращается индекс мьютекса, на котором выполнение зафейлилось. Если все завершилось успешно aka все замки захвачены, возвращается -1.
Чтобы с помощью std::try_lock наверняка захватить все замки, нужно крутиться в горячем цикле и постоянно вызывать std::try_lock, пока она не вернет -1.
Непонятно, зачем эта функция нужна в прикладном программировании при наличии std::scoped_lock и std::lock, которые еще и удобно оборачивают все эти циклы, скрывая все эти кишки в деталях реализации.
Единственное, что пришло в голову - реализация своего scoped_lock'а с блэкджеком и другим алгоритмом предотвращения дедлока. Этот алгоритм должен быть чем-то похож на поддавки, но как-то изменен. У кого есть кейсы применения - отпишитесь в комменты.
В общем, на мой взгляд, это пример избыточного апи. Если человек сам что-то свое мудрит, то ему особо не сложно самому цикл написать. Однако обычные пользователи стандартных примитивов синхронизации возможно никогда в своей жизни этого не применят. Все-таки стандартная библиотека должна быть сборником решения актуальных проблем реальных, а не воображаемых,людей.
Stay useful. Stay cool.
#concurrency
❤17🔥9👍6❤🔥3😁1
Могу ли я вызвать функцию main?
#опытным
Вопрос из разряда "а что если" и особо практического смысла не имеет. Но когда это нас останавливало? Без знаний стандарта на этот вопрос вряд ли можно ответить правильно, но я попробую хотя бы приблизится к этому.
Прородителем плюсов был язык С, поэтому давайте сначала посмотрим, что там творится по этому поводу.
В C нет запрета вызывать main(). А все, что не запрещено - разрешено. Вот и вы легко можете вызвать main() из любого места программы.
Можно даже рекурсивно вызвать главную функцию и из этого можно придумать что-то более менее рабочее. Например:
Если это запускать, как 'recursive_main 1 2 3', то вывод будет такой:
Но вы скорее попадете на переполнение стека от неконтролируемой рекурсии, чем сделаете что-то полезное.
Ну и конечно, в С сложно сделать так, чтобы код выполнялся в глобальной области.
А вот в С++ это сделать суперпросто. В конструкторах глобальных объектов. И вот здесь уже интересно. Инициализация глобальных переменных имеет свой определенный порядок. Что будет, если мы в этот порядок вклинимся и запустим программу раньше?
Такое чувство, что ничего хорошего мы не получим.
В случае с рекурсией любой код теоретически может уйти в переполнение, поэтому как будто и не особо важно, main это или нет. Не запрещать же рекурсию из-за возможности ее никогда не остановить.
Но вот если мы можем прервать подготовку программы к вызову main ее преждевременным вызовом - у нас 100% возникнут проблемы.
Скорее всего это одна из мажорных причин, почему в С++ нельзя вызывать main никаким образом. Если происходит обратное, то программа считается ill-formed.
Компиляторы по идее должны тут же прервать компиляцию при упоминании main в коде. Но вы же знаете эти компиляторы. Слишком много им свободы дали. Скорее всего, вы сможете скомпилировать свой код с вызовом main и все заработает. Только если прописать какой-нибудь --pedantic флаг, то вам скажут "атата, так делать низя".
В общем, не думаю, что у вас было желание когда-то вызвать main. Однако сейчас вы точно знаете, что так делать нельзя)
Follow the rules. Stay cool.
#cppcore #goodoldc
#опытным
Вопрос из разряда "а что если" и особо практического смысла не имеет. Но когда это нас останавливало? Без знаний стандарта на этот вопрос вряд ли можно ответить правильно, но я попробую хотя бы приблизится к этому.
Прородителем плюсов был язык С, поэтому давайте сначала посмотрим, что там творится по этому поводу.
В C нет запрета вызывать main(). А все, что не запрещено - разрешено. Вот и вы легко можете вызвать main() из любого места программы.
Можно даже рекурсивно вызвать главную функцию и из этого можно придумать что-то более менее рабочее. Например:
#include <stdio.h>
int main (int argc, char *argv[]) {
printf ("Running main with argc = %d, last = '%s'\n",
argc, argv[argc-1]);
if (argc > 1)
return main(argc - 1, argv);
return 0;
}
Если это запускать, как 'recursive_main 1 2 3', то вывод будет такой:
Running main with argc = 4, last = '3'
Running main with argc = 3, last = '2'
Running main with argc = 2, last = '1'
Running main with argc = 1, last = './recursive_main'
Но вы скорее попадете на переполнение стека от неконтролируемой рекурсии, чем сделаете что-то полезное.
Ну и конечно, в С сложно сделать так, чтобы код выполнялся в глобальной области.
А вот в С++ это сделать суперпросто. В конструкторах глобальных объектов. И вот здесь уже интересно. Инициализация глобальных переменных имеет свой определенный порядок. Что будет, если мы в этот порядок вклинимся и запустим программу раньше?
Такое чувство, что ничего хорошего мы не получим.
В случае с рекурсией любой код теоретически может уйти в переполнение, поэтому как будто и не особо важно, main это или нет. Не запрещать же рекурсию из-за возможности ее никогда не остановить.
Но вот если мы можем прервать подготовку программы к вызову main ее преждевременным вызовом - у нас 100% возникнут проблемы.
Скорее всего это одна из мажорных причин, почему в С++ нельзя вызывать main никаким образом. Если происходит обратное, то программа считается ill-formed.
Компиляторы по идее должны тут же прервать компиляцию при упоминании main в коде. Но вы же знаете эти компиляторы. Слишком много им свободы дали. Скорее всего, вы сможете скомпилировать свой код с вызовом main и все заработает. Только если прописать какой-нибудь --pedantic флаг, то вам скажут "атата, так делать низя".
В общем, не думаю, что у вас было желание когда-то вызвать main. Однако сейчас вы точно знаете, что так делать нельзя)
Follow the rules. Stay cool.
#cppcore #goodoldc
❤27😁18👍13❤🔥6