Давайте расскажу, кто я такой вообще сначала.
Это вот я с каким-то чуваком.
Я 6 лет работаю в компании ПВС-студио, занимаюсь, собственно, ядром C++-анализатора, та часть, которая прикидывается компилятором его фронтендом.
Ну и, как вы, наверное, заметили, люблюкотиков.
Поговорим мы сегодня про семантику.
Ну не просто про семантику, а с точки зрения того, как изнутри компилятор смотрит на код, чего у него там внутри есть для этого.
Ну, посмотрим, что такое семантика, что такое символы, как организован поиск этих символов,
Компилятор понимает, что какое имя значит, немножко паратипа поговорим, как выбираются перегрузки функции и чуть затронем в тимплейте.
Ну, о чем это собственно?
И в языках программирования такие штуки замечательные, как синтаксис и семантика.
Синтаксис — это для парсера, это способ формировать вообще просто корректное высказывание на языке.
Они могут быть бессмысленными, но они будут корректными грамматически.
А семантика — это такой способ понимать, что там все эти вещи значат.
И вот на семантику мы и посмотрим.
Условно строится стоит на трех катах.
Это символы типа, которые у многих сущностей есть.
И как это все вместе связать, как найти имя, что оно означает.
Там еще есть четвертый код, констекс про вычисления, но его мы касаться не будем.
Там лес с волками, а не с катами.
Есть у нас некие имена в программе, например, функция.
Мы ее параметр внутри трогаем,
И мы должны связать вот это А и вот это А. Более того, потрогать мы его можем в разных местах, например здесь.
И более того, мы можем...
так как имена не уникальна, и сделать ещё одну А, и здесь вот это будет совсем другая уже история, не то, что пришло.
Если посмотреть на это дело, которое здесь нарисовано, то мы получим картин, например, такую.
У нас есть глобальный скоб, там функции.
Внутри одно из функций переменные объявлены, и в теле ЕФА ещё переменная.
Это пример простой на самом деле.
Что у нас еще может быть?
У нас может быть что-то вот такое.
Когда мы функцию тянем из namespace, тогда эта наша схема немножко меняется.
у нас появляется еще один скоб, ну это, в принципе, понятно.
И еще один момент, это мы можем затощить из совершенно другого скоб, четк наш скоб.
И будет, возможно, больно, возможно, нет.
Так, ну, собственно, в этом коде вот значимые куски, это имена.
И символ, это, собственно, метаинформация об имени.
У него имя, айдишник и перент.
И, к сожалению, у нас неминуемо возникает тут иерархия классов.
Здесь не все, а там их тысячи.
Более того, у них разные характеристики.
Например, вот переменные классы там и функция.
И где иерархия классов, там иногда боль, потому что работаем мы в основном с указателем или со ссылкой на базу, а трогать нам приходится наследников.
Ну и как вообще с этой ситуацией мы могли бы разобраться.
Но что-то как-то виртуальными функциями весь интерфейс не обмажешь, и в общем-то это тупиковый путь.
Есть у нас еще замечательные способы разобраться, что там в базе, ну, например, делать вот такой, кстати, каст, неудобно, многословно.
и еще можно делать вот так и словить радость в виде динамического разруливания типов и так далее.
Поэтому я вам сейчас покажу быстренько, как можно
избавиться от всего этого и все равно получить иерархию классов не работать.
В кланге тоже такое сделано.
По-моему, везде в компиляторах такая подобная штука встречается.
Я просто быстренько сейчас пробегусь по этому.
Для начала нам надо знать, что из ссылки на один тип мы можем получить ссылку на другой тип.
Вот эти штуки будут нужны нам дальше.
Мы будем делать их специализации.
Ну, с copy-paste немножко из стандартных трейтов.
И будет у нас функция Get ID.
Эта функция принимает ссылку на что-то и возвращает ID-шник про ассоциированный стипом.
Здесь мы ее оставляем без реализации, потому что мы ее будем тоже специализировать.
Ну и это вот static cast.
Это еще один static cast, который зашаблонен не по типу, а по ID.
А вот это наш динамик cast.
И здесь, давайте свершим небольшое преступление.
У базы мы можем получить ID.
Эта штука будет работать для всей иерархии наследования от этой базы.
И в каждом наследнике у нас свой ID-шник.
И чтобы нам просоциировать его с типом наследником, мы делаем вот такую вот штуку.
Макрос нам генерит специализации структуры.
И дальше, если вспомните вот эти вот в начале динамикасты, статиккаста и так далее, теперь мы можем делать вот так.
Вот эта штука функция Tricast, она уважает константность, то есть она квалификатор не теряет, там не надо писать конст, всякие ссылки, звездочки и так далее.
И более того, мы можем делать вот так.
У нас в парсере много специализаций функций,
Они специализированы по инамам, и вот эта вот штука очень хорошо помогает.
Ну, в принципе, с символами пока разобрались.
Пойдём посмотрим, как мы именно достаём, как мы ещё понимаем, что у нас там понаписано.
Вернём к нашей схемке, которая была изначально
Наша задача, увидев имя, связать его с некой сущностью.
Когда мы связываем какое-то одно значение с каким-то другим, что у нас...
Здесь прекрасный момент, что если имени нет, мы его не нашли сразу.
Если имя в программе не встречалось, мы его в принципе не найдем.
У нас есть первичный ключ, так сказать, это имена.
ассоциировано множество скопов, в которых оно встречается.
Вот эта верхняя строчка с апострофом — это глобальный скоп.
У него специальное имя, он никому не принадлежит, у него 0-pointer-apparent, и он доступен нам всегда.
Эта штука лежит в глобальном скопе.
Это namespace, тоже скоп.
Почему здесь function set?
Потому что перегрузки хранятся внутри одного символа, которая объявлена в одном сколпе.
А здесь нет, потому что мы сейчас на него глянем, как она устроена.
У нас А объявлена внутри функции и внутри фа.
В обоих случаях это переменная.
с каждым именем, вот там вот где синеньким подсвечено, это список символов, которые с этим именем в этом сколпе просоциированы.
Ну и давайте пойдем от самой А поищем, неквалифицированным поиском, сам простой и самый сложный, в том числе этот поиск.
Мы заходим в хэштаблицу, в хэштаблицу мы видим А, а дальше нам надо взять текущий сколп и посмотреть, а не вставлен ли он там во вторую мапу, которая с фасколпами проссорена с именем.
Если мы его там не найдем, то мы пойдем выше, выше, выше, пока текущий сколп не окажется в нашей второй мапе.
Здесь у нас тривиальный случай, мы очень просто находим
Текущий скоп у нас как раз тот, в котором в том числе переменная есть.
И мы находим внутреннюю переменную, которая в эфе.
Вверх мы дальше не пойдем, поэтому, собственно, вот так работает переопределение вещей внутри вложенных скопов.
Но переменно искать не очень интересно, и вообще есть более интересные вещи.
Вот мы там затаскивали функцию с namespace, и вообще весь namespace.
Вот это вот значимое части мы затаскиваем на namespace снаружи откуда-то.
Он в глобальном скопе лежит, мы внутри тела функции, более того внутри FA.
Вот это, собственно, наша функция.
И для того, чтобы мы могли искать через using namespace что-то, мы делаем трюк, затаскиваем этот namespace, так называемый import, просто указатель на scope лежит в scope функции теперь.
И вот эта, собственно, штука, которую мы должны найти.
Пойдем посмотрим, как мы это будем искать.
Как вы думаете, мы в Naim Space заходим здесь?
Мы идем дальше в глобальный скоб, а только потом мы заходим в импортированный Naim Space.
Если посмотреть с другого угла, то получается, путь у нас вот такой.
Импортированные на mSpace, они хитрые.
Если у нас в текущем сколпе есть какое-то имя, которое колизится с именем из импортированного на mSpace, то мы обязаны найти имя ближайшее при неквалифицированном поиске.
После этого только мы пойдем искать в импортированном на mSpace и поиска осуществляется от того сколпа, который ближайший общий предок нашего текущего и того, который мы затощили.
Ну, здесь мы в принципе... А почему мы не можем внести выше лежащий скоб, этот самый импорт?
Потому что тогда мы засорим выше лежащий скоб.
Если бы мы просто его вносили куда-то повыше и искали бы оттуда, то мы бы постоянно ложали на поиски от того скоба верхнего.
Квалифицированный поиск от неквалифицированного, здесь в Neem Space происходит квалифицированный поиск от неквалифицированного, он отличается только тем, что мы не идем вверх.
Если мы в указанном месте символ не нашли, то мы его не нашли, это ошибка.
Посмотрим теперь на типы.
У всего, почти у всего есть тип.
Давайте вот этот пример с функцией нашей усложним до невозможности сделаем его прям вообще сложным.
Вместо инта у нас будет указатель.
Давайте посчитаем здесь типы.
И сама функция у нее тоже есть тип.
Вот это у нас встроенные типы, они очень простые.
Указатель это отдельный тип.
Функции это типы и так далее.
Указатель в данном случае у нас это указатель на конст, билтын тип.
И сама функция, это функция, которая возвращает билтын тип и принимает указатель.
И если на это посмотреть, то у нас опять иерархия и опять дерево.
На самом деле, типы внутри компилятора организованы в дерево.
У кланга, например, типы символы и ноды IST, они вообще организованы в одной большой метод дерева, по которому он ходит.
Они и включены в одну идиную большую структуру.
У нас в принципе тоже так же.
Ну, иерархия классов, так и иерархия классов.
Опять айдишники, опять вот эти касты и так далее.
В базе типа у нас есть вот такая замечательная сущность, каноникол.
Сейчас посмотрим, что она значит.
Там ничего особо интересного не происходит, там просто, по сути, лежат данные, которые характеризуют тип.
Вот указатели, например, лежит внутри тип, на который он указывает.
У функции будет лежать возвращаемый тип из список параметров.
Если заметили там слово «qual type», то это неспроста.
Int — это у нас тип built-in, который int.
Внезапно это тоже тип built-in int.
И вопрос, а где же const?
потому что на самом деле с точки зрения вот этой иерархии типов, инт и констант они вообще ничем не отличаются, потому что здесь тип это именно имит типа само, то есть это инт, вот он тип, а вот эта вот штука это квалификаторы, которые, грубо говоря, могут быть в разных комбинациях и накручиваться направо и налево на все подряд.
Поэтому мы должны, в общем-то,
Использовать, не впихивать квалификаторы в тип, иначе у нас произойдет там комбинаторный взрыв, просто этих типов у нас будет констант, влотай линд, конств, влотай линд и так далее.
CallType под капотом таскает одну штуку, указатель на базовый класс Type.
Но этот указатель немножко хитрый.
На X86 под виндами, линуксами, макосами и вообще под любой операционкой, где есть разделение на ядро и юзерспейс.
В юзерспейсе у нас в указателях всегда есть один байт.
Даже современные процессоры это учитывают в защищенном режиме.
Я вот не знаю, есть уже процессора, на которых это работает или нет, но вообще в защищенном режиме этот байт должен по идее сбрасываться.
Было, по крайней мере, вроде собирались такое делать.
И вот этот байт, который у нас пропадает, грубо говоря, потому что мы работаем всегда в юзерспейсе, его можно использовать в наших грязных целях.
Мы его возьмем и запихнем сюда битики.
Конст, волотаил, рестрикт, отсишечки.
Пять бит мы не используем.
И таскаемый указатель с этими битиками.
Чтобы никто по этому указателю не сходил,
Он хранится там не как указатель, у куал-тайпа есть интерфейс подобный, по интеру со звездочкой, стрелочкой и так далее, он этот битик сбрасывает.
Хранится там здравенный уинд ПТР-1, который кастится туда-сюда.
И в тайпе в самом базе есть вот этот вот замечательный тип каноникл, который на самом деле это
тот же самый тип, который у нас... У нас есть объект, который представляет собой тип экономикал.
Это тот же самый тип, но немножко по-другому выглядящий.
Вот представьте, что у нас есть alias на тот самый int.
И мы сделаем функцию, которая этот alias возвращает и его же принимает.
Типу этой функции будет вот такой.
А чтобы сформировать каноникл, мы разруливаем все алиасы и инстанцирование шаблонов достаём оттуда изнутри.
Ну, собственно, то, что было инстанцировано.
В данном случае мы получим вот такое.
Аалиасы вообще в семантических анализаторах нужны для истории, чтобы понимать, откуда пришло и с именами что-то просвецировать.
Мы никогда с ними не работаем, мы всегда избрасываем.
Давайте посмотрим на перегрузки.
Ну, скажем, есть у нас вот что-то такое.
Где-то мы видим вызов вот что-то подобного.
Ну, как мы уже убедились, да, Алиаса мы сбрасываем, поэтому у нас будут типа без Алиасов, а вот эта штука внизу, это вызов некой функции, которая принимает два рв и аргумента типа int.
И вот эти два рв и int там мы должны впихнуть вот в этот список функций и разобраться, от чего вообще от нас хотят.
И мне доводилось слышать мнение, что компилятор фильтрует аверлоуцет по количеству параметров и так далее и выбирает там на основе каких-то своих хитрых логик, что-то подходящее через систему штрафов и так далее.
Вот этот алгоритм выбора перегрузок довольно простой.
Сначала мы считаем на каждый аргумент, мы по порядку проходим аргументы параметры после подстановок всяких шаблонов и так далее, и считаем conversion sequence.
Это некая величина и последовательность шагов, которые надо произвести, чтобы из аргумента, из типа аргумента получить тип параметра.
Вот в данном случае в первой функции у нас
Первый параметр — это полное соответствие.
А второй — параметр пустой.
Здесь у нас L-value-reference.
Здесь у нас вообще из инта в указатель не явно получить нельзя.
Здесь у нас арифметическая конвертация.
И второй параметр совпадает.
И я вас немножко здесь обманул, потому что у нас R-value, поэтому
лвлю референс из него сделать нельзя не константный, поэтому в функции, которая принимает ссылку, у нас невозможно конвертация.
А дальше мы проходимся по этому списку, по очереди смотрим на параметры и выбираем функцию, у которой значение при сравнении, в общем, это алгоритм, похожий на поиск максимального элемента в списке.
Сравниваем функции между собой, и наиболее подходящие функции всплывают на верх.
мы не можем сконвертировать первый аргумент в те нижние функции, которые сейчас сером подсвечены.
Поэтому функция с рефметической конверсией вплывает наверх.
И здесь, если посмотреть, то второго параметра нет, поэтому она нам тоже не подходит.
И если мы сравним эти две функции, то второй будет лучше, несмотря на то, что у нее нужна конверсия на первом аргументе.
Если бы у нас не получилось посчитать, что лучше, и у нас бы наверх не всплыла одна функция, это было бы неоднозначенность, мы не можем выбрать перегрузку.
И в результате мы выбрали эту функцию, мы молодцы.
в функции там у нас можно притянуть и шаблоны.
Потому что у шабланов, когда вы их инстанцируете, специализируете алгоритм выбора вот этой специализации и существующих.
Он примерно такой же, как у функции, только он попроще.
Мы не считаем никакие конверсии.
У нас всегда должен тип совпадать.
Ну то есть если мы попробуем проинстанцировать шаблон с флоутом, а у нас где-то он инстанцировался интом, то это нам не подойдет вообще никогда.
На кошечках, собачках и прочих животных.
Сделали мы вот такой шаблон.
Зачем-то положили туда инд.
И не хотим просто, чтобы его инстанцировали.
Мы хотим, чтобы люди делали специализацию.
Ну, не знаю зачем, но допустим.
Сами мы сделали специализацию.
Вот нолит у нас, пусть будет котик, он говорит мяу и так далее.
и есть функция, которая принимает что-то вот такое.
Вы, наверное, догадались.
Давайте посмотрим на символы и типы заодно.
Шаблон — это символ с именем.
И со списком параметров шаблона.
Вот этот инт в уголовых скобочках это неинт.
Это тип non-type template-параметр.
специальный тип внутри у него лежит рекорд рекорд это глаз структура юнион и так далее и нам не нам по-моему не рекорд специализацию мы уже приготовили ручками написали она у нас там тоже есть темплейт знает о своих специализациях специализации знает из какого не темплейта мы можем через праймери темплейт найти специализации а вот там функции у нас
Бобор тот самый был с единичкой, а не с нуликом, поэтому мы подставляем эту единичку в non-type параметр, ну как мы с функциями делали, смотрим, подходит, нам не подходит, в этом случае мы не можем выбрать специализацию существующих, потому что не было специализации с единичкой.
Поэтому мы делаем эту специализацию в этом месте.
Вот если мы позовем это дело так, что будет внутри структуры?
Мы подставили аргументы в параметры шаблона, сделали объявление структуры и не делаем тело вообще.
Потому что ссылку на incomplete-тип брать легально.
для того, чтобы создать объект.
Нам надо знать размеры alignment, поэтому мы обязаны уже сделать тело у структуры.
Если помните, там была функция создать кассертом и инт.
И если мы сделаем так, то в структуре у нас будет только поля и не будет функции.
То есть она там будет, она там подразумевается, но мы не сделаем для нее даже объявления.
Шаблоны при инстанцировании и при создании неявной специализации инстанцируются лениво.
Пока вы чего-то из шаблона не используете, вот там нет.
А вот если мы сделаем вот это, то получим вот это.
Такая вот особенность шаблонов нам позволяет делать всякие интересные штуки с ними.
Делаем forward-декларацию шаблона, делаем другой шаблон, в котором мы эту forward-декларацию используем, даже что-то из нее, из того шаблона, достаем.
Если мы напишем, ну, например, что-то подобное, то это скомпилируется, а этого — окей, это нормально.
мы подставили этот int везде, где только можно было.
Но заметьте, здесь форвардекларация функции.
В тело мы не лезем пока и ничего там не возвращаем, потому что если мы попытаемся вернуть объект, то, соответственно, естественно, он не скомпилируется.
Если мы напишем что-то подобное, то у нас будет ошибка, потому что у нас нет тела.
того шаблона, который мы используем для инстанцирования второго шаблона, и, соответственно, мы не можем ничего достать изнутри.
А здесь компилятор пойдет, попробует вот этот тип сделать, у него не получится, потому что второго шаблона, который во второй параметр идет, просто не существует пока что.
Как только у нас появится тело определения,
Вот там-то мы, в общем-то, и нормально скомпилируем вот тот код, который был с ошибкой.
Шаблон должен... Определение шаблона должно появиться перед первым использованием.
Вернемся к этому примеру.
И в C++ есть одна вредная штука.
Сейчас покажу, почему она вредная.
Мы можем написать вот так.
Это explicit instantiation.
Здесь мы говорим компилятору.
Давай это подставишь и пойдешь проинстанцируешь и сделаешь нам специализацию.
А вот эта штука, на нее не распространяются то, что я до этого говорил про ленивое инстанцирование.
Здесь компилятор, хочет он или нет, пойдет и сделает вообще все, что в шаблоне есть.
мы опять получим то же самое, что у нас было, когда мы функцию звали до этого.
Почему это зло и почему так не надо делать?
Вот здесь ссылка на бложек Артура Удвайра.
Он там писал, почему не стоит делать эксплисит инстанцирования типов из стандартной библиотеки.
Кому интересно, почитайте довольно забавная штука.
В общем-то, на этом у меня всё.
Я это пока оставлю, чтобы все смогли сфоткать.
Сейчас через минутку переключусь.
Я думаю, что можно уже переходить к вопросам.
Ну что, поднимайте руки, задавайте вопросы.
Дайте микрофон, пожалуйста, среди на правой секции.
У меня вот такой вопрос, а как по схеме вначале, где namespace разрешается, вот ADL для ADL разрешается?
ADL делает просто, если у нас есть неквалифицированный поиск, но без двух двоих точей вначале, не через точку и так далее, ADL
Ищет символ просто неквалифицированным поиском.
И дополнительно заходит еще в те namespaces, в которых объявлены параметры функции.
То есть не параметры, а аргументы, которые туда пришли.
По основному пути и заглядывает еще в эти.
С функциями там на самом деле все немножко сложнее, потому что мы не ищем один символ, мы ищем все функции, которые попадают.
Поэтому ADL нам возвращает верлоу-цет из основной дорожки, из тех namespace, где аргументы наобявлены.
То есть это как несколько юзингов?
Он в нескольких местах ищет.
Давайте в следующий вопрос из зала.
Человек прямо перед вами тянул руку.
Они уже ответили только что.
Как работают со случаями, когда имен нет?
Например, с конструкторами, деструкторами, операторами, классов?
Есть ли какие-то имена заглушки?
Они лежат в классе, как просто... Если это член класса, то просто есть функция член класса.
Если это не член класса, то это просто функция.
Для деструкторов, кстати, нам никогда не надо их искать.
Мы ищем имя самого класса и для конструкторов тоже.
И потом уже, когда нашли класс, смотрим, какой конструктор нам подходит изнутри.
Но вообще для них есть символы.
Интереснее с операторами, которые друзьяжки внутри класса объявлены.
Потому что друзьяжка, функция, она на самом деле объявляется в обрамляющем скопе класса, но вы ее не можете позвать явно по имени из этого скопа.
То есть как бы имени нет, а как бы функция есть.
И разруливается оно через ADL.
Давайте еще вопросы из зала.
Я плохо вижу зал, девочки.
Если есть зрители, которые подняли руку, вы принесите микрофон, пожалуйста.
Пока вы думаете на вопросами, еще вопрос из чата.
Зачем нужно трогать верхний байт, если есть нижние биты от выравнивания?
Также есть вопрос про старые стандарты.
Не стреляет ли там стати-кассерт слишком рано?
Речь про около 90-х слайд.
В старых стандартах стати-кассерт изменялась в какой-то момент.
По-моему, изменения касались и в условной компиляции с if constexpr, когда теперь можно в той ветке, в которой мы не пойдем, можно стать к ассердс-фоллсу написать, и будет все нормально.
По поводу неинстанцированных функций шаблон, я, честно говоря, не уверен, как в каком стандарте там что-то менялось или не менялось,
Но, тем не менее, это был пример.
По-моему, раньше, если даже в неинстанцированной функции стать кассерт написать, он отваливался.
Но это был пример для иллюстрации.
Он был откровенно тупой, чтобы показать, что оно не скомпилируется в каком случае.
Так что не принимайте его всерьез.
Вижу поднятую руку слева.
Подскажите, пожалуйста, вы начали говорить, я немножко дозадан вопрос.
Если у нас есть функция друг и она реализована при объявлении внутри класса, то кроме как СДЛ, снаружи мы ее общение как-нибудь позовем, адрес не возьмем, ничего.
Что-то хотел сказать, наверное.
Это вот тот же самый механизм, как
со всякими модулями, структурами внутри функции объявленных.
Эта штука у нас речевол, но не визибол.
Спасибо большое за интересный доклад.
Вопрос, как устроен скобл цикла FOR?
Потому что переменные, которые объявляются внутри цикла FOR, их можно использовать внутри объявления, но их можно использовать и внутри цикла, поэтому... Переменные, которые в скобочках объявлены, они попадают просто в скобл самого цикла.
Они объявляются внутри уже.
А то, что снаружи фора видно, ну, через неквалифицированный поиск наружу, то, что в заголовке, и, кстати, в эфет и с инициализатором, или даже если вы в условии эфа просто объявите переменную, она тоже попадает туда, вот внутрь.
Это не только для фора такая система.
Все, что в скобочках попадает, во внутренний скоб.
Вижу понятую руку, вдалеке, в правой части.
Вопрос, как работает специализация.
Когда шаблонный аргумент задает возвращаемый тип, потому что, насколько я знаю, сейчас нельзя, по крайней мере, в GSC задать конкретную специализацию.
То есть мы делаем тимплейт по какому-то Т и этот Т идёт возвращается оттуда.
То есть в MSWC это вроде как можно сделать, а в GST не получалось.
Насколько я помню, даже Антон Палухин писал по вот этого на форуме, что почему это до сих пор нельзя сделать и вроде как в фонарте это тоже не указано.
Насколько я знаю, там эта Т не выводится.
Вот если там авто было, оно бы из ретёрна вывелось.
А здесь надо явно указывать.
привызывая вот этот вот тип.
Ну, вроде как в этом даже случае оно не работало.
А вот это вот не знаю, если честно.
Я с таким особо не сталкивался, чтоб оно прям не работало, если там явно написать его в угловых скобочках.
Может быть, там есть еще какие-то вещи шаблонные.
Например, что-то шаблонное идет в параметр,
и он не может вывести тип.
То есть он путается в шаблонных параметрах, потому что если мы попытаемся вывести тип, который мы передали в функцию, то есть это t раздидюйсять, а у нас еще есть там другой параметр, который идет, например, в Return, то нам надо их два написать.
Иногда вроде такой у них не получается.
Если это вообще имеет способ какой-то?
Ну, вообще, там логика такая, что в аргумент уходит базовый класс, а вернуть ты должен один наследник.
Это вот про табафе такая штука.
А, вот это у меня было, да.
Ничего лучше, чем всех написать не придумал, если честно.
Можно ли подробнее рассказать о порядке функциях кандидата к вызову?
В примере, все же орифметик меньше матч, но в то же время мте меньше матч.
До конца понятная версия с двумя аргументами победила.
Версия с двумя аргументами победила, потому что мте даёт, ну, при сравнении отсутствующего параметра с чем угодно отсутствующий параметр проигрывает гарантированно.
Я хотел сказать слово штраф, но штраф на самом деле нет.
Там идёт именно сравнение двух функций.
По всем конвершен-секвенсам, которые присутствуют при выборе этой перегрузки.
И я не знаю, как подробнее сейчас об этом рассказать, а об этом можно очень долго подробно рассказывать.
Хорошо, надеюсь, зададут вопросы в чате.
И последний вопрос, который волнует наших зрителей в чате.
На слайдах были все ваши котики?
Те, которые были на первом слайде со мной, трое.
А еще там была пара рандомных котов, которые не мои.