Давайте вернемся к функции Head и посмотрим на нее повнимательней. Сейчас она у нас работает только для вектора, то есть она может позволять итерироваться по префиксу только вектора. Но при этом мы можем захотеть ее применить, например, для дека или же для сета. Тоже могут возникать ситуации, когда нам захочется проитерироваться по этим контейнерам. Но сейчас функция Head нам это не позволяет. И давайте мы ее перепишем таким образом, чтобы она позволяла итерироваться по префиксу произвольного контейнера. Давайте переименуем параметр T в Container и скажем, что мы теперь принимаем не вектор T, а ссылку на контейнер. И возникает вопрос: что нам написать вот здесь при инстанцировании шаблона IteratorRange? Потому что теперь у нас есть контейнер, у нас нету типа его итератора, у нас есть просто контейнер. Нам нужно понять, чем инстанцировать шаблон IteratorRange. И мы можем попробовать написать здесь вот что: <typename Container:: iterator>. Вот у нас даже компилируется. И давайте попробуем воспользоваться нашей функцией Head, например для, значит, множества — объявим какое-то множество, проинициализируем его какими-то целыми числами, например, такими. И сделаем for (int x : Head(nums, например, 4)), вывести x. И перевод строки. Компилируем. Не компилируется, потому что мы не знаем, что такое set. set и сразу queue — она нам скоро пригодится. Компилируем. Компилируется. Работает: вот оно вывело 1, 5, 6, 7 — потому что у нас элементы в множестве хранятся по порядку. Поэтому она, функция Head, нам дает доступ к четырем минимальным элементам множества. Это оказались 1, 5, 6 и 7. Хорошо. Для множества работает. Ну давайте, например, сделаем то же самое для дека. Объявим deque, назовем его nums2. И тоже применим к нему функцию Head. Вот отлично, у нас все компилируется, запустим. Работает, выводит 5, 7, 12, 8 — все нормально, все отлично, все работает. А теперь давайте, например, возьмем и сделаем наш дек константным. Ну, вот мы берем префикс нашего дека, итерируемся по нему, и читаем, и выводим наши выводим элементы дека. Почему бы нам не разрешить это делать для константных объектов? Запустим компиляцию, и тут вот у нас уже не компилируется. Сообщение об ошибке не то, чтобы сильно помогает понять, в чем дело. Но давайте просто разберемся. Суть-то в чем? Вот здесь мы в качестве типа, с которым мы инстанцируем шаблон, используем контейнер-итератор. А у нас константный дек. И его методы, его метод begin в данном случае, у константного дека метод begin возвращает const iterator, который не разрешает изменять элементы вектора. И, собственно, вот сообщение об ошибке начинается со слов could not convert, а дальше тут что-то не очень понятное, но суть в том, что мы не можем сконвертировать константный итератор в просто итератор по понятным причинам. Потому что просто итератор разрешает изменять элементы вектора, а константный не может. Поэтому константный не может конвертироваться в неконстантный. Поэтому просто вот так вот написать Container::iterator мы не можем. Нам нужно как-то уметь выбирать между константным итератором для константных объектов и неконстантных для неконстантным для неконстантным объектом. Как же мы это будем делать? Давайте обратим внимание на такой интересный факт. Вот здесь вот мы и для константных и для неконстантных объектов используем вызов метода begin. И компилятор сам догадывается, какую версию метода begin вызвать. То есть неважно, константный нам сюда придет контейнер, неконстантный — мы все равно будем вызывать метод begin. То есть компилятор уже умеет делать этот выбор за нас. Кроме того, мы с вами научились перекладывать на компилятор выбор типа, с помощью которого нужно инстанцировать шаблон. Как мы это делаем? Мы можем вот здесь — ой, так сейчас вернусь в нужно место. Вот оно. Вот здесь мы можем написать Iterator Range от вот этих вот, вот от этого выражения. Тем самым мы инициируем вызов конструктора вот этого нашего. Мы инициируем вызов этого конструктора и компилятор за нас выведет тип итератора, с которым надо инстанцировать наш шаблон. Осталось одна проблема: несмотря на то, что вот здесь, вот здесь вот, в 32-й строке, компилятор может вывести тип за нас, нам нужно указать какой-то тип возвращаемого значения для функции Head вот здесь. Нам нужно его напечатать. И нам нужно здесь что-то напечатать, чтобы, значит, у нас получился корректно сформированный шаблон функций. И здесь нам на помощь приходит вот такая вот интересная возможность C++. Мы можем здесь написать ключевое слово auto. Мы пишем auto — давайте я сделаю функцию покомпактнее. Давайте для начала скомпилируем. Компилируется. Тем самым мы говорим, написав auto в качестве типа возвращаемого значения функции, мы говорим компилятору: возьми возвращаемый тип из команды return, потому что вот здесь в команде return мы уже написали, какой тип мы хотим вернуть. И мы просто говорим с помощью вот этого ключевого слова auto: дорогой компилятор найди в реализации функции команду return, посмотри, какой тип там возвращается, и подставь его сюда. И дорогой компилятор это делает за нас. Значит, мы видим, что наш код компилируется, мы его запускаем, он работает, то есть вот здесь для константного дека у нас все скомпилировалось, вывелось 5, 7, 12, 8. Кроме того, у нас работает пример с модификацией вектора v через результат вызова функции Head, то есть, действительно, теперь у нас сам компилятор выводит тип итератора, с которым нужно инстанцировать IteratorRange. И, смотрите, какая интересная вещь: вот здесь, вот в этом вот шаблоне Head мы дважды перекладываем на компилятор необходимость выводить типы. Первый раз мы это делаем вот здесь, вызывая конструктор шаблона IteratorRange. Тем самым компилятор выводит сначала тип итератора, с которым надо инстанцировать наш шаблон, а потом за счет использования auto вот здесь, мы второй раз перекладываем работу на компилятор. Тут мы ему говорим: иди в return и посмотри, что мы там возвращаем. Таким образом, давайте подведем итог. В этом видео мы с вами узнали, что в качестве возвращаемого типа функции можно использовать ключевое слово auto. Пока что мы его использовали только для объявления переменных — вот у него есть еще один контекст использования. Ключевое слово auto говорит компилятору: возьми тип из команды return и посмотри, какой тип мы возвращаем, и подставь его в качестве типа результата функции. Как этим пользоваться? Потому что когда ты узнаешь, что можно не писать тип возвращаемого значения, а написать auto, может возникнуть желание писать его везде. Так вот, по умолчанию следует этой возможностью не пользоваться, а всегда явно указывать тип результата функции. Это нужно для упрощения понимания и читаемости вашего кода. Потому что когда вы видите просто объявления функции без тела и у нее написано auto, то понять, что она возвращает, вы не можете — вам нужно лезть в ее тело и понимать, как эта функция устроена. Кроме того, эта функция может быть громоздкой, и из ее реализации еще может быть не так-то просто понять, что она, собственно, какой тип она возвращает. Поэтому использовать auto в качестве типа результата функции стоит, только если тип результата громоздкий, большой, сложный, и тело функции очень короткое. В этом случае мы видим, что у функции результат сформулирован как auto, но мы можем быстро — вот вернемся в наш пример функции Head, — мы можем сразу вот здесь в эти три строчки посмотреть и понять, что наша функция Head возвращает IteratorRange. Если же функция Head Была бы там на 200–300 строк, то auto в качестве возвращаемого значения очень сильно бы усложнил ее понимание. Таким образом, мы используем auto, только когда он / оно не мешает читабельности и пониманию кода.