Привет, я backend-разработчик IT-компании SimbirSoft Леонид. В этой статье расскажу про 8 нюансов, которые я обнаружил при изучении шаблонов С++. Честно признаюсь, что наткнувшись на некоторые из них, я был удивлен: «Хм, SFINAE есть, а слова нет?» или «А что, есть разница между шаблоном в шаблоне и шаблоном с двумя параметрами?». Материал будет полезен начинающим разработчикам, которые знакомятся с шаблонами, а также специалистам уровня middle, которые используют шаблоны время от времени. Некоторые из примеров были описаны в cpp-referernce чуть ли не в самом первом абзаце, некоторые потребовали пошерстить stackoverflow, и в конце концов все есть в стандарте. Но кто учит язык по документации? У кого из нас не было такой ситуации: «Сейчас я код потыкаю, а там разберемся, что к чему». Так вот, сейчас пришло время узнать, как это работает и почему именно так.  Предлагаю начать с терминологии, на всякий случай. В контексте рассматриваемой темы про шаблоны я предлагаю остановиться на следующем определении. Инстанцирование (англ. instance — создание экземпляра чего-то) — процесс порождения специализации. Компилятор или разработчик где-то порождает код через подстановку параметров в шаблон и соответственно, инстанцирование может быть неявным и явным. Специализация — непосредственное указание частного случая для конкретного типа. А теперь перейдем к примерам. Несмотря на то, что комбинация букв SFINAE звучит отовсюду, где начинают говорить про шаблоны, самой комбинации букв SFINAE в стандарте нет. Конечно, всё, что мы имеем в виду, когда говорим про SFINAE, там описано, но если вы планируете найти в стандарте вот прям SFINAE, то вас ждет разочарование или удивление. Впрочем, как и меня.  Есть расхожее мнение, что «= delete» означает, что компилятор не будет генерировать эти конструкторы для вас, но… Можно запрещать специализации через «=delete».  Если ожидали вывода “2” или даже “1”, то вы ошибаетесь. Будет ошибка компиляции (CE). В этом случае вы сами не хотите писать специализацию и компилятору запрещаете. Даже можно запретить общий  шаблон и разрешить только специализации(-ю). В данном случае тоже CE, потому что запретили общий, и разрешили только для int и short. И попытка создать для double провалена. Кстати, перегрузки функций тоже можно запрещать. У нас есть 2 класса X1 и X2. и Казалось бы, что в X1, что в Х2 надо натолкать два типа, но есть нюанс.  При первом знакомстве с шаблонами я задался вопросом: «А как написать реализацию метода вне класса для обоих случаев?». И судя по stackoverflow, я не один такой. И какая вообще разница между этими двумя случаями? В первом случае setA является шаблоном функции — члена шаблона класса, где шаблон класса требует один тип и метод требует один тип: Здесь при использовании будет инстанцирована требуемая специализация метода класса. Во втором случае это не шаблонная функция — член шаблона класса, в которой шаблон класса хочет сразу два типа: В этом случае метод зависит от инстанцированного класса и его параметров. И метод для нужного нам типа при использовании будет всегда инстанцирован вместе с классом (в отличие от первого случая, где будет инстанцирована только нужная специализация метода, а не весь класс). И еще один нюанс, про который можно забыть (а я забыл). Может возникнуть ощущение, что setB() никак не зависит от параметров шаблона. А если есть ощущения, что все-таки что-то тут не так, то так и есть. Надо помнить, что каждый метод имеет доступ к this (во время вызова метода this фактически передается в метод). А в зависимости от параметра шаблона, указанного при создании экземпляра объекта, this будет разным. Получается, что все методы должны соответствовать всем формам this. Хотя сам метод явно и не выглядит каким-то особенным. Как известно, при инстанцировании шаблона функции можно явно не указывать те аргументы шаблона, которые могут быть выведены из типов фактических аргументов функции.  При вызове f(1.0f); компилятор умен настолько, чтобы понять, что T – это float;, но при этом он не может вывести тип возвращаемого значения. Поэтому вот так не получится: Чтобы получить такой результат, можно просто указать эти типы: А можно указать только один — для возвращаемого типа, а второй из параметра выведет компилятор.  Таким образом, при проектировании становится важным порядок расположения шаблонных аргументов. Те, которые компилятор может вывести, стоит располагать в конце списка. Если задать параметры шаблонной функции по умолчанию, то они не будут участвовать в выводе типов. Продолжая тему вывода типов, рассмотрим такой пример: Если есть ожидания, что вызов func() будет работать, то ваши ожидания – это ваши проблемы. Компилятор так не сможет. Но он сможет, если сделать так: Параметры по умолчанию у шаблонной функции не участвуют в выводе типов, поэтому компилятору нужно указать тип по умолчанию. Примеры из этого пункта для разных компиляторов ведут себя по-разному. Например, MSVC сам до всего догадался, а gcc — нет. Итак, у нас есть вот такая конструкция: Так вот будет CE из-за того, что компилятор не знает, что x.setA это шаблонная функция, и сделает вывод, что это поле и поэтому x.setA < T (где «<» — это меньше, а дальше («>()») синтаксическая ошибка). Поэтому компилятору надо подсказать и надо сделать так:  Для этого, оказывается, есть своё слово — «disambiguatioin» (устранение неодназначности). Та же самая техника и то же самое слово, когда мы используем typename.  Данный код будет собираться и выполняться без ошибок: Мы можем использовать данный код, и все будет хорошо. Но как только будет вызван (инстанцирован) метод f(), все сломается. Компилятор не будет инстанцировать f(), пока его об этом не попросят. Таким образом, про ошибку узнаем только тогда, когда начнем использовать метод f(). Неявное инстанцирование шаблона функции происходит в тот момент, когда компилятор первый раз видит, что вызывается требуемая специализация в данной единице трансляции. Таким образом, если есть два или более модулей, в каждом из которых будет вызвана одна и та же специализация для шаблонной функции, то компилятор сделает инстанцирование в каждом модуле. У нас есть возможность заставить компилятор делать так, как надо нам. Имеем шаблонную функцию: А вот теперь компилятор не будет инстанцировать в этой единице трансляции, потому что, скорее всего, мы будем инстанцировать в другом месте. Если компилятор неявно создал специализацию в данной единице трансляции, то второй раз не получится ее написать. Хотя это правило известное и простое, приведу пример, который лично мне показался интересным и не сразу понятным. В случае func(a) будет выбран четвертый, и вопросов не возникает. Чтобы понять, как интересно комбинируются звездочки для func<int**>(a) – пришлось взглянуть пристальнее.  — это Т, а тип аргумента  функции — это int ***, следовательно, не хватает одной звездочки: [int**] * = T* (выбирается второй вариант). В качестве заключения хотелось бы сказать, что приступая к изучению шаблонов, надо быть готовым к тому, что вся «глубина глубин» С++ и здесь никуда не пропадёт. Чем глубже ты погружаешься, тем больше тебе предстоит увидеть и возможно даже изучить. Но С++ хорош тем, что ты можешь продолжать писать хороший код и в течение долгого времени не сталкиваться ни с чем, о чём я написал.  Спасибо за внимание!  Авторские материалы для разработчиков мы также публикуем в наших соцсетях – ВКонтакте и Telegram.