Язык программирования C Пятое издание
Скачать 1.85 Mb.
|
каким будет параметр шаблона g() при вызове g(i = ci)? Упражнение 16.44. Используя те же три вызова, что и в первом упражнении, определите типы T, если параметр функции g() объявляется как T (а не Т&&) и как const Т&? Упражнение 16.45. С учетом следующего шаблона объясните происходящее при вызове функции g() с таким литеральным значением, как 42, и с переменной типа int? template <typename Т> void g(T&& val) { vector<T> v; } 16.2.6. Функция std::move() Библиотечная функция move() (см. раздел 13.6.1) — хороший пример шаблона, использующего ссылки на r-значение. К счастью, функцию move() можно использовать, не понимая механизма работы используемого ею шаблона. Однако изучение работы функции move() может помочь понять и использовать шаблоны. В разделе 13.6.2 обращалось внимание на то, что, хотя и нельзя непосредственно привязать ссылку на r-значение к l-значению, функцию move() можно использовать для получения ссылки на r-значение, связанной с l-значением. Поскольку функция move() может получать аргументы, по существу, любого типа, нет ничего удивительного в том, что move() — это шаблон функции. Как определена функция std::move() Стандартное определение функции move() таково: // об использовании typename в типе возвращаемого значения и // приведении см. раздел 16.1.3 // remove_reference рассматривается в разделе 16.2.3 Page 867/1103 template <typename Т> typename remove_reference<T>::type&& move(T&& t) { // static_cast рассматривается в разделе 4.11.3 return static_cast<typename remove_reference<T>::type&&>(t); } Этот код короток, но сложен. В первую очередь, параметр функции move(), Т&& является ссылкой на r-значение типа параметра шаблона. Благодаря сворачиванию ссылок этот параметр может соответствовать аргументу любого типа. В частности, функции move() можно передать либо l-, либо r-значение: string s1("hi!"), s2; s2 = std::move(string("bye!")); // ok: перемещение r-значения s2 = std::move(s1); // ok: но после присвоения // значение s1 неопределенно Как работает функция std::move() В первом присвоении аргумент функции move() является r-значением, полученным в результате выполнения конструктора string("bye") класса string. Как уже упоминалось, при передаче r-значения ссылочному r-значению параметра функции выведенный из этого аргумента тип является ссылочным типом (см. раздел 16.2.5). Таким образом, в вызове std::move(string("bye!")): • выведенным типом T будет string; • следовательно, экземпляр шаблона remove_reference создается с типом string; • тип-член type класса remove_reference<string> будет иметь тип string; • типом возвращаемого значения функции move() будет string&&; • у параметра t функции move() будет тип string&&; Соответственно, этот вызов создает экземпляр move<string>, являющийся следующей функцией: string&& move(string &&t) Тело этой функции возвращает тип static_cast<string&&>(t). Типом t уже является string&&, поэтому приведение не делает ничего. Следовательно, результатом этого вызова будет ссылка на r-значение, которое было дано. Теперь рассмотрим второе присвоение, которое вызывает функцию std::move(s1). В этом вызове аргументом функции move() является l-значение. Поэтому на сей раз: Page 868/1103 • выведенным типом Т будет string& (ссылка на тип string, а не просто string); • следовательно, экземпляр шаблона remove_reference создается с типом string&; • тип-член type класса remove_reference<string&> будет иметь тип string; • типом возвращаемого значения функции move() все еще будет string&&; • параметр t функции move() будет создан как экземпляр string& &&, который сворачивается в string&. Таким образом, этот вызов создает экземпляр шаблона move<string&>, который является точно тем, что необходимо для связи ссылки на r-значение с l-значением. string&& move(string &t) Тело этого экземпляра возвращает тип static_cast<string&&>(t). В данном случае типом t является string&, который приведение преобразует в тип string&&. Оператор static_cast поддерживает приведение l-значения к ссылке на r-значение Обычно оператор static_cast может выполнить только доступные преобразования (см. раздел 16.3). Однако для ссылок на r-значение есть специальное разрешение: даже при том, что нельзя неявно преобразовать l-значение в ссылку на r-значение, используя оператор static_cast, можно явно привести l-значение к ссылке на r-значение. Привязка ссылки на r-значение к l-значению создает код, который работает с разрешением ссылке на r-значение заменять l-значение. Иногда, как в случае с функцией reallocate() класса StrVec (см. раздел 13.6.1), известно, что замена l-значения безопасна. Разрешая осуществлять это приведение, язык позволяет его использование. Вынуждая использовать приведение, язык пытается предотвратить его случайное использование. И наконец, хотя такие приведения можно написать непосредственно, намного проще использовать библиотечную функцию move(). Кроме того, использование функции std::move() существенно облегчает поиск в коде места, потенциально способного заменить l-значения. Упражнения раздела 16.2.6 Упражнение 16.46. Объясните, что делает этот цикл из функции StrVec::reallocate() (раздел 13.5): for (size_t i = 0; i != size(); ++i) alloc.construct(dest++, std::move(*elem++)); 16.2.7. Перенаправление Некоторые функции должны перенаправлять другим функциям один или несколько своих аргументов с неизменными типами. В таких случаях необходимо сохранить всю информацию о Page 869/1103 перенаправленных аргументах, включая то, является ли тип аргумента константой и является ли аргумент l- или r-значением. В качестве примера напишем функцию, получающую вызываемое выражение и два дополнительных аргумента. Функция вызовет предоставленное вызываемое выражение с другими двумя аргументами в обратном порядке. Вот первый фрагмент функции обращения: // шаблон, получающий вызываемое выражение и два параметра // вызывает предоставленное выражение с "обращенными" параметрами // flip1 - неполная реализация: спецификатор const верхнего уровня и // ссылки теряются template <typename F, typename T1, typename T2> void flip1(F f, T1 t1, T2 t2) { f(t2, t1); } Этот шаблон работает прекрасно, пока он не используется для вызова функции со ссылочным параметром: void f(int v1, int &v2) // обратите внимание, v2 - ссылка { cout << v1 << " " << ++v2 << endl; } Здесь функция f() изменяет значение аргумента, привязанного к параметру v2. Но если происходит вызов функции f() через шаблон flip1, внесенные функцией f() изменения не затронут первоначальный аргумент: f(42, i); // f() изменяет свой аргумент i flip1(f, j, 42); // вызов f() через flip1 оставляет j неизменным Проблема в том, что j передается параметру t1 шаблона flip1. Этот параметр имеет простой, не ссылочный тип int, а не int&. Таким образом, этот вызов создает следующий экземпляр шаблона flip1: Page 870/1103 void flip1(void(*fcn)(int, int&), int t1, int t2); Значение j копируется в t1. Ссылочный параметр в функции f() связан с t1, а не с j. Определение параметров функции, хранящих информацию типа Чтобы передать ссылку через функцию, необходимо переписать ее так, чтобы параметры сохраняли принадлежность своих аргументов к l-значениям. Немного поразмыслив, можно предположить, что константность аргументов также необходимо сохранить. Всю информацию о типе аргумента можно сохранить, определив соответствующий ему параметр функции как ссылку на r-значение параметра типа шаблона. Использование ссылочного параметра (l- или r-значение) позволяет сохранить константность, поскольку спецификатор const в ссылочном типе нижнего уровня. Благодаря сворачиванию ссылок (см. раздел 16.2.5), если определить параметры функции как T1&& и T2&&, можно сохранить принадлежность к l- или r-значениям аргументов функции (см. раздел 16.2.5): template <typename F, typename T1, typename T2> void flip2(F f, T1 &&t1, T2 &&t2) { f(t2, t1); } Как и прежде, если происходит вызов flip2(f, j, 42), l-значение j передается параметру t1. Однако в функции flip() для T1 выводится тип int&, а значит, тип t1 сворачивается в int&. Ссылка t1 связана с j. Когда функция flip() вызывает функцию f(), ссылочный параметр v2 в функции f() привязан к t1, который, в свою очередь, привязан к j. Когда функция f() осуществляет инкремент v2, это изменяет значение j. Параметр функции, являющийся ссылкой на r-значение параметра типа шаблона (т.е. Т&&), сохраняет константность и принадлежность к l- или r-значениям соответствующих ему аргументов. Эта версия функции flip() решает одну половину проблемы. Она работает прекрасно с функциями, получающими ссылки на l-значение, но неприменима для вызова функций с параметрами ссылок на r-значение. Например: void g(int &&i, int& j) { cout << i << " " << j << endl; } Если попытаться вызывать функцию g() через функцию flip(), то для параметра ссылки на r-значение функции g() будет передан параметр t2. Даже если функции flip() было передано r-значение, функции g() будет передан параметр, носящий в функции flip() имя t2: flip2(g, i, 42); // ошибка: нельзя инициализировать int&& из l-значения Параметр функции, как и любая другая переменная, является выражением l-значения (см. раздел 13.6.1). В результате вызов функции g() в функции flip() передает l-значение параметру ссылки на r-значение функции g(). Использование функции std::forward() для сохранения информации типа в вызове Page 871/1103 Чтобы передать функции flip() параметры способом, сохраняющим типы первоначальных аргументов, можно использовать новую библиотечную функцию forward(). Как и функция move(), функция forward() определяется в заголовке utility. В отличие от функции move(), функцию forward() следует вызывать с явным аргументом шаблона (см. раздел 16.2.2). Для этого явного аргумента типа функция forward() возвращает ссылку на r-значение. Таким образом, типом возвращаемого значения функции forward<T> будет Т&&. Обычно функцию forward() используют для передачи параметра функции, который определен как ссылка на r-значение, параметру типа шаблона. Благодаря сворачиванию ссылок для типа возвращаемого значения функция forward() сохраняет характер (l- или r-значение) переданного ей аргумента: template <typename Type> intermediary(Type &&arg) { finalFcn(std::forward<Type>(arg)); // ... } Здесь Type используется как тип явного аргумента шаблона функции forward() (выводимый из arg). Поскольку arg — это ссылка на r-значение для параметра типа шаблона, параметр Type представит всю информацию типа в аргументе, переданном параметру arg. Если этот аргумент будет r-значением, то параметр Type будет иметь обычный (не ссылочный) тип и функция forward<Type>() возвратит Type&&. Если аргумент будет l-значением, то (благодаря сворачиванию ссылок) типом параметра Type будет ссылка на l-значение. В данном случае типом возвращаемого значения будет ссылка на r-значение для типа ссылки на l-значение. Снова благодаря сворачиванию ссылок (на сей раз для типа возвращаемого значения) функция forward<Type>() возвратит тип ссылки на l-значение. При использовании с параметром функции, являющимся ссылкой на r-значение для параметра типа шаблона (Т&&), функция forward() сохраняет все подробности типа аргумента. Перепишем первоначальную функцию, используя на этот раз функцию forward(): template <typename F, typename T1, typename T2> void flip(F f, T1 &&t1, T2 &&t2) { f(std::forward<T2>(t2), std::forward<T1>(t1)); } Если происходит вызов функции flip(g, i, 42), то параметр i будет передан функции g(), поскольку int& и 42 будут переданы как int&&. Подобно функции std::move(), для функции std::forward() не стоит предоставлять объявление using. Причина описана в разделе 18.2.3. Упражнения раздела 16.2.7 Упражнение 16.47. Напишите собственную версию функции обращения и проверьте ее, вызывав функции с параметрами ссылок на r-значение и l-значение. 16.3. Перегрузка и шаблоны Page 872/1103 Шаблоны функций могут быть перегружены другими шаблонами или обычными, не шаблонными функциями. Как обычно, функция с тем же именем должна отличаться либо количеством, либо типом своих параметров. На подбор функции (см. раздел 6.4) присутствие шаблона функции влияет следующими способами. • В набор функций-кандидатов на вызов включаются любые экземпляры шаблона функции, для которой успешна дедукция аргумента шаблона (см. раздел 16.2). • Шаблоны функций-кандидатов всегда подходящие, поскольку дедукция аргумента шаблона устранит все неподходящие шаблоны. • Как обычно, подходящие функции (шаблонные и нешаблонные) ранжируются по преобразованиям, если таковые вообще имеются. Конечно, набор применимых преобразований при вызове шаблона функции весьма ограничен (см. раздел 16.2.1). • Так же как обычно, если только одна функция обеспечивает наилучшее соответствие, она и выбирается. Но если одинаково хорошее соответствие обеспечивают несколько функций, то: • если в наборе одинаково хороших соответствий есть только одна нешаблонная функция, то выбрана будет она; • если в наборе нет нешаблонных функций, но есть несколько шаблонных, и одна из них более специализированна, чем любые другие, то будет выбран более специализированный шаблон функции; • в противном случае вызов неоднозначен. Правильное определение набора перегруженных шаблонов функций требует хорошего понимания отношений между типами и ограничений на преобразования, применимых к аргументам в шаблонах функций. Создание перегруженных шаблонов В качестве примера создадим набор функций, которые могли бы пригодиться во время отладки. Назовем отладочные функции debug_rep(), каждая из них возвратит строковое представление предоставленного объекта. Начнем с создания самой общей версии этой функции в качестве шаблона, получающего ссылку на константный объект: // выводит любом тип, который иначе не обработать template <typename Т> string debug_rep(const T &t) { ostringstream ret; // см. раздел 8.3 ret << t; // использует оператор вывода Т для вывода представления t return ret.str(); // возвращает копию строки, с которой связан ret } Page 873/1103 Эта функция применяется для создания строки, соответствующей объекту любого типа, у которого есть оператор вывода. Теперь определим версию функции debug_rep() для вывода указателя: // выводит указатели как их значение, сопровождаемое объектом, // на который он указывает // обратите внимание: эта функция не будет работать правильно с char*; // см. раздел 16.3 template <typename Т> string debug_rep(T *p) { ostringstream ret; ret << "pointer: " << p; // выводит собственное значение указателя if (p) ret << " " << debug_rep(*p); // выводит значение, на которое // указывает p else ret << " null pointer"; // или указывает, что p - нулевой return ret.str(); // возвращает копию строки, с которой связан ret } Эта версия создает строку, содержащую собственное значение указателя и вызывает функцию debug_rep() для вывода объекта, на который указывает этот указатель. Обратите внимание, что эта функция не может использоваться для вывода символьных указателей, поскольку библиотека ввода-вывода определяет версию оператора << для значения указателя char*. Эта версия оператора << подразумевала, что указатель обозначает символьный массив с нулевым символом в конце и выводит содержимое массива, а не его адрес. Обработка символьных указателей рассматривается в разделе 16.3. Page 874/1103 Эти функции можно использовать следующим образом: string s("hi"); cout << debug_rep(s) << endl; Подходящей для этого вызова является только первая версия функции debug_rep(). Второй версии требуется параметр в виде указателя, а в этом вызове передан не указатель. Нет никакого способа создать экземпляр шаблона функции, ожидающего тип указателя, из параметра, который не является указателем, поэтому дедукция аргумента терпит неудачу. Поскольку есть только одна подходящая функция, она и используется. Если происходит вызов функции debug_rep() с указателем: cout << debug_rep(&s) << endl; то обе функции создают подходящие экземпляры: • debug_rep(const string*&) — экземпляр первой версии функции debug_rep() с привязкой параметра Т к типу string*; • debug_rep(string*) — экземпляр второй версии функции debug_rep() с привязкой параметра Т к типу string. Точным соответствием для этого вызова является экземпляр второй версии функции debug_rep(). Создание экземпляра первой версии требует преобразования простого указателя в указатель на константу. Обычный подбор функции гласит, что следует предпочесть второй шаблон, и в действительности так и происходит. Несколько подходящих шаблонов В качестве другого примера рассмотрим следующий вызов: const string *sp = &s; cout << debug_rep(sp) << endl; Здесь подходящими являются оба шаблона, и оба обеспечивают точное соответствие: • debug_rep(const string*&) — экземпляр первой версии шаблона с привязкой параметра Т к типу const string*; • debug_rep(const string*) — экземпляр второй версии шаблона с привязкой параметра Т к типу const string. В данном случае обычный подбор функции не может различить эти два вызова. Можно было бы ожидать, что этот вызов будет неоднозначен. Однако благодаря специальному правилу для перегруженных шаблонов функций этот вызов решается как debug_rep(Т*), поскольку это более специализированный шаблон. Причина для этого правила в том, что без него не было бы никакого способа вызвать версию функции debug_rep() для указателя на константу. Проблема в том, что к шаблону debug_rep(const Т&) подходит практически любой тип, включая типы указателя. Этот шаблон является более общим, чем debug_rep(Т*), который может быть вызван только для типов указателя. Без этого правила вызовы с передачей указателей на константу всегда будут неоднозначны. Когда есть несколько перегруженных шаблонов, предоставляющих одинаково хорошее Page 875/1103 соответствие для вызова, предпочитается наиболее специализированная версия. Не шаблон и перегрузка шаблона Для следующего примера определим обычную, не шаблонную версию функции debug_rep(), выводящую строки в двойных кавычках: // вывод строк в двойных кавычках string debug_rep(const string &s) { return '"' + s + '"'; } Теперь, когда происходит вызов функции debug_rep() для строки: string s("hi"); cout << debug_rep(s) << endl; есть две одинаково хорошо подходящих функции: • debug_rep<string>(const string&) — первый шаблон с привязкой параметра T к типу string; • debug_rep(const string&) — обычная, не шаблонная функция. В данном случае у обеих функций одинаковый список параметров, поэтому каждая из них обеспечивает одинаково хорошее соответствие этому вызову. Однако выбирается нешаблонная версия. По тем же причинам, по которым предпочитаются наиболее специализированные из одинаково хорошо подходящих шаблонов функций, нешаблонная функция предпочитается при одинаково хорошем соответствии с шаблонной функцией. Когда нешаблонная функция обеспечивает одинаково хорошее соответствие с шаблонной функцией, предпочитается нешаблонная версия. Перегруженные шаблоны и преобразования До сих пор не рассматривался случай с указателями на символьные строки в стиле С и строковые литералы. Теперь, когда имеется версия функции debug_rep(), получающая строку, можно было бы ожидать, что ей будет соответствовать вызов, которому переданы символьные строки. Однако рассмотрим этот вызов: cout << debug_rep("hi world!") << endl; // вызов debug_rep(T*) Здесь подходящими являются все три функции debug_rep(): • debug_rep(const Т&) — с привязкой параметра Т к типу char[10]; • debug_rep(Т*) — с привязкой параметра Т к типу соnst char; • debug_rep(const string&) — требующая преобразования из const char* в string. Оба шаблона обеспечивают точное соответствие аргументу — второй шаблон требует (допустимого) преобразования из массива в указатель, и это преобразование считается Page 876/1103 точным соответствием при подборе функции (см. раздел 6.6.1). Нешаблонная версия является подходящей, но требует пользовательского преобразования. Эта функция хуже точного соответствия, поэтому кандидатами остаются два шаблона. Как и прежде, версия Т* более специализирована, она и будет выбрана. Если символьные указатели необходимо обработать как строки, можно определить еще две перегруженные, нешаблонные функции: // преобразовать символьные указатели в строку и вызвать строковую // версию debug_rep() string debug_rep(char *p) { return debug_rep(string(p)); } string debug_rep(const char *p) { return debug_rep(string(p)); } Пропуск объявления может нарушить программу Следует заметить, что для правильной работы версии char* функции debug_rep() объявление debug_rep(const string&) должно находиться в области видимости, когда эти функции определяются. В противном случае будет вызвана неправильная версия функции debug_rep(): template <typename Т> string debug_rep(const T &t); template <typename T> string debug_rep(T *p); // следующее объявление должно быть в области видимости // для правильного определения debug_rep(char *) string debug_rep(const string &); string debug_rep(char *p) { // если объявление для версии, получающей const string&, не находится // в области видимости, return вызовет call debug_rep(const Т&) с // Page 877/1103 экземпляром строки в параметре Т return debug_rep(string(p)); } Обычно, если попытаться использовать функцию, которую забыли объявлять, код не будет откомпилирован. Но с функциями, которые перегружают шаблон функции, все не так. Если компилятор может создать экземпляр вызова из шаблона, то отсутствие объявления не будет иметь значения. В этом примере, если забыть объявлять версию функции debug_rep(), получающую строку, компилятор тихо создаст версию экземпляра шаблона, получающую const Т&. Объявляйте каждую функцию в наборе перегруженных, прежде чем определять их. Таким образом можно гарантировать, что компилятор создаст экземпляр вызова прежде, чем он встретит функцию, которую предполагалось вызвать. Упражнения раздела 16.3 Упражнение 16.48. Напишите собственные версии функций debug_rep(). Упражнение 16.49. Объясните, что происходит в каждом из следующих вызовов: template <typename Т> void f(Т); template <typename T> void f(const T*); template <typename T> void g(T); template <typename T> void g(T*); int i = 42, *p = &i; const int ci = 0, *p2 = &ci; g(42); g(p); g(ci); g(p2); f(42); f(p); f(ci); f(p2); Упражнение 16.50. Определите функции из предыдущего упражнения так, чтобы они выводили идентификационное сообщение. Выполните код этого упражнения. Если вызовы ведут себя не так, как ожидалось, выясните почему. 16.4. Шаблоны с переменным количеством аргументов Шаблон с переменным количеством аргументов (variadic template) — это шаблон функции или класса, способный получать переменное количество параметров. Набор таких параметров называется пакетом параметров (parameter pack). Есть два вида пакетов параметров: пакет параметров шаблона (template parameter pack), представляющий любое количество параметров шаблона, и пакет параметров функции (function parameter pack), представляющий любое количество Page 878/1103 параметров функции. Для указания, что шаблону или функции представлен пакет параметров, используется многоточие. В списке параметров шаблона синтаксис class... или typename... означает, что следующий параметр представляет список любого количества типов; имя типа, сопровождаемое многоточием, представляет список из любого количества параметров значения заданного типа. Параметр в списке параметров функции, типом которого является пакет параметров шаблона, представляет собой пакет параметров функции. Например: // Args - это пакет параметров шаблона; rest - пакет параметров функции // Args представляет любое количество параметров типа шаблона // rest представляет любое количество параметров функции template <typename Т, typename... Args> void foo(const T &t, const Args& ... rest); Этот код объявляет, что fоо() — это функция с переменным количеством аргументов, у которой один параметр типа по имени T и пакет параметров шаблона по имени Args. Этот пакет представляет любое количество дополнительных параметров типа. В списке параметров функции foo() один параметр типа const& для любого типа переданного параметром Т и пакет параметров функции rest. Этот пакет представляет любое количество параметров функции. Как обычно, компилятор выводит типы параметра шаблона из аргументов функции. Для шаблона с переменным количеством аргументов компилятор также выводит количество параметров в пакете. Рассмотрим, например, следующие вызовы: int i = 0; double d = 3.14; string s = "how now brown cow"; foo(i, s, 42, d); // три параметра в пакете foo(s, 42, "hi"); // два параметра в пакете foo(d, s); // один параметр в пакете foo("hi"); // пустой пакет Компилятор создаст четыре разных экземпляра функции fоо(): void foo(const int&, const string&, const int&, const double&); void foo(const string&, const int&, const char[3]&); Page 879/1103 void foo(const double&, const string&); void foo(const char[3]&); В каждом случае тип T выводится из типа первого аргумента. Остальные аргументы (если они есть) представляют количество и типы дополнительных аргументов функции. Оператор sizeof... Когда необходимо узнать, сколько элементов находится в пакете, можно использовать оператор sizeof.... Как и оператор sizeof (см. раздел 4.9), оператор sizeof... возвращает константное выражение (см. раздел 2.4.4) и не вычисляет свой аргумент: template<typename ... Args> void g(Args ... args) { cout << sizeof...(Args) << endl; // количество параметров типа cout << sizeof...(args) << endl; // количество параметров функции } Упражнения раздела 16.4 Упражнение 16.51. Определите, что возвратят операторы sizeof...(Args) и sizeof...(rest) для каждого вызова функции foo() в этом разделе. Упражнение 16.52. Напишите программу, проверяющую ответы на предыдущий вопрос. 16.4.1. Шаблоны функции с переменным количеством аргументов В разделе 6.2.6 упоминалось, что для определения функции, способной получать переменное количество аргументов, можно использовать класс initializer_list. Однако у аргументов должен быть одинаковый тип (или типы, преобразуемые в общий тип). Функции с переменным количеством аргументов используются тогда, когда не известно ни количество, ни типы аргументов. Для примера определим функцию, подобную прежней функции error_msg(), только на сей раз обеспечим и изменение типов аргумента. Начнем с определения функции print() с переменным количеством аргументов, которая выводит содержимое заданного списка аргументов в указанный поток. Функции с переменным количеством аргументов зачастую рекурсивны (см. раздел 6.3.2). Первый вызов обрабатывает первый аргумент в пакете и вызывает себя для остальных аргументов. Новая функция print() будет работать таким же образом — каждый вызов выводит свой второй аргумент в поток, обозначенный первым аргументом. Для остановки рекурсии следует определить также обычную функцию print(), которая получает поток и объект: // Функция для завершения рекурсии и вывода последнего элемента // Page 880/1103 ее следует объявить перед определением версией print() с переменным // количеством аргументов template<typename Т> ostream &print(ostream &os, const T &t) { return os << t; // нет разделителя после последнего элемента в пакете } // эта версия print() будет вызвана для всех элементов в пакете, кроме // последнего template <typename Т, typename... Args> ostream &print(ostream &os, const T &t, const Args&... rest) { os << t << ", "; // выводит первый аргумент return print(os, rest...); // рекурсивный вызов; вывод других // аргументов } Первая версия функции print() останавливает рекурсию и выводит последний аргумент в начальном вызове функции print(). Вторая версия, с переменным количеством аргументов, выводит аргумент, связанный с t, и вызывает себя для вывода остальных значений в пакете параметров функции. Ключевая часть — вызов функции print() в функции с переменным количеством аргументов: return print(os, rest...); // рекурсивный вызов; вывод других // аргументов Версия функции print() с переменным количеством аргументов получает три параметра: ostream&, const T& и пакет параметров. Но в этом вызове передаются только два аргумента. В результате первый аргумент в пакете rest привязывается к t. Остальные аргументы в пакете rest формируют пакет параметров для следующего вызова функции print(). Таким образом, при каждом вызове первый аргумент удаляется из пакета и становится аргументом, связанным с t. Соответственно, получаем: Page 881/1103 print(cout, i, s, 42); // два параметра в пакете Рекурсия выполнится следующим образом: Вызов t rest... print(cout, i, s, 42) i s, 42 print(cout, s, 42) s 42 Вызов print(cout, 42) вызывает обычную версию функции print(). Первые два вызова могут соответствовать только версии функции print() с переменным количеством аргументов, поскольку обычная версия не является подходящей. Эти вызовы передают четыре и три аргумента соответственно, а обычная функция print() получает только два аргумента. Для последнего вызова в рекурсии, print(cout, 42), подходят обе версии функции print(). Этот вызов передает два аргумента, и типом первого являются ostream&. Таким образом, подходящей является обычная версия функции print(). Версия с переменным количеством аргументов также является подходящей. В отличие от обычного аргумента, пакет параметров может быть пустым. Следовательно, экземпляр версии функции print() с переменным количеством аргументов может быть создан только с двумя параметрами: один — для параметра ostream& и другой — для параметра const T&. Обе функции обеспечивают одинаково хорошее соответствие для вызова. Однако нешаблонная версия с переменным количеством аргументов более специализирована, чем шаблонная с переменным количеством аргументов. Поэтому выбирается версия без переменного количества аргументов (см. раздел 16.3). Объявление версии функции print() с постоянным количеством аргументов должно быть в области видимости, когда определяется версия с переменным количеством аргументов. В противном случае функция с переменным количеством аргументов будет рекурсивно вызывать себя бесконечно. Упражнения раздела 16.4.1 Упражнение 16.53. Напишите собственные версии функций print() и проверьте их, выводя один, два и пять аргументов, у каждых из которых должны быть разные типы. |