Язык программирования C Пятое издание
Скачать 1.85 Mb.
|
Упражнение 16.54. Что происходит при вызове функции print() для типа, не имеющего оператора <<? Упражнение 16.55. Объясните, как выполнилась бы версия функции print() с переменным количеством аргументов, если бы обычная версия функции print() была объявлена после определения версии с переменным количеством аргументов. 16.4.2. Развертывание пакета Кроме выяснения размера, единственное, что можно еще сделать с пакетом параметров, — это развернуть (pack expansion) его. При развертывании пакета предоставляется схема (pattern), используемая для каждого развернутого элемента. Развертывание пакета разделяет его на элементы с применением схемы к каждому из них. Для запуска Page 882/1103 развертывания справа от схемы помещают многоточие (...). Например, функция print() содержит два развертывания: template <typename Т, typename... Args> ostream & print(ostream &os, const T &t, const Args&... rest) // развертывание // Args { os << t << ", "; return print(os, rest...); // развертывание rest } В первом случае развертывание пакета параметров шаблона создает список параметров функции print(). Второй случай развертывания находится в вызове функции print(). Эта схема создает список аргументов для вызова. Развертывание пакета Args применяет схему const Args& к каждому элементу в пакете параметров шаблона Args. Результатом этой схемы будет разделенный запятыми список из любого количества типов параметров в формате const тип &. Например: print(cout, i, s, 42); // два параметра в пакете Типы последних двух аргументов, наряду со схемой, определяют типы замыкающих параметров. Этот вызов создает следующий экземпляр: ostream& print(ostream&, const int&, const strings, const int&); Второе развертывание происходит в рекурсивном вызове функции print(). В данном случае схема — это имя пакета параметров функции (т.е. rest). Эта схема развертывается в разделяемый запятыми список элементов пакета. Таким образом, этот вызов эквивалентен следующему: print(os, s, 42); Концепция развертывания пакета Развертывание пакета параметров функции print() только разворачивало пакет на его составные части. При развертывании пакета параметров функции возможны и более сложные схемы. Например, можно было бы написать вторую функцию с переменным количеством аргументов, которая вызывает функцию debug_rep() (см. раздел 16.3) для каждого из своих аргументов, а затем вызывает функцию print(), чтобы вывести полученные строки: Page 883/1103 // вызвать debug_rep() для каждого аргумента в вызове print() template <typename... Args> ostream &errorMsg(ostream &os, const Args&... rest) { // print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an) return print(os, debug_rep(rest)...); } Вызов функции print() использует схему debug_rep(rest). Эта схема означает, что функцию debug_rep() следует вызвать для каждого элемента в пакете параметров функции rest. Получившийся развернутый пакет будет разделяемым запятыми списком вызовов функции debug_rep(). Таким образом, вызов errorMsg(cerr, fcnName, code.num(), otherData, "other", item); выполняется, как будто было написано: print(cerr, debug_rep(fcnName), debug_rep(code.num()), debug_rep(otherData), debug_rep("otherData"), debug_rep(item)); Следующая схема, напротив, не была бы откомпилирована: // передает пакет debug_rep(); print(os, debug_rep(a1, a2, an)) print(os, debug_rep(rest...)); // ошибка: нет функции, соответствующей // вызову Проблема здесь в том, что пакет rest развернут в вызове функции debug_rep(). Этот вызов выполнился бы так, как будто было написано: print(cerr, debug_rep(fcnName, code.num(), otherData, "otherData", item)); В этом развертывании осуществляется попытка вызова функции debug_rep() со списком из пяти аргументов. Нет никакой версии функции debug_rep(), соответствующей этому вызову. Функция debug_rep() имеет постоянное количество аргументов, и нет никакой ее версии с пятью параметрами. Схема при развертывании применяется по отдельности к каждому элементу в пакете. Упражнения раздела 16.4.2 Page 884/1103 Упражнение 16.56. Напишите и проверьте версию функции errorMsg() с переменным количеством аргументов. Упражнение 16.57. Сравните свою версию функции errorMsg() с переменным количеством аргументов с функцией error_msg() из раздела 6.2.6. Каковы преимущества и недостатки каждого подхода? 16.4.3. Перенаправление пакетов параметров По новому стандарту можно использовать шаблоны с переменным количеством аргументов совместно с функцией forward() для написания функций, которые передают свои аргументы неизменными некой другой функции. Чтобы проиллюстрировать такие функции, добавим в класс StrVec (см. раздел 13.5) функцию-член emplace_back(). Такая функция-член библиотечных контейнеров является шаблоном-членом с переменным количеством аргументов (см. раздел 16.1.4), которая использует их для создания элементов непосредственно в области, управляемой контейнером. Версия функции emplace_back() для класса StrVec также должна быть с переменным количеством аргументов, поскольку у класса string много конструкторов, которые отличаются своими параметрами. Поскольку желательно быть в состоянии использовать конструктор перемещения класса string, необходимо будет также сохранять всю информацию о типах аргументов, переданных функции emplace_back(). Как уже упоминалось, сохранение информации типа — двухступенчатый процесс. Во-первых, для сохранения информации типа аргументов параметры функции emplace_back() следует определить как ссылки на r-значение параметра типа шаблона (см. раздел 16.2.7): class StrVec { public: template <class... Args> void emplace_back(Args&&...); // остальные члены, как в разделе 13.5 }; Схема && в развертывании пакета параметров шаблона означает, что каждый параметр функции будет ссылкой на r-значение на соответствующий ей аргумент. Во-вторых, функцию forward() следует использовать для сохранения первоначальных типов аргументов, когда функция emplace_back() передает их функции construct() (см. раздел 16.2.7): template <class... Args> inline void StrVec::emplace_back(Args&&... args) { Page 885/1103 chk_n_alloc(); // пересоздает StrVec при необходимости alloc.construct(first_free++, std::forward<Args>(args)...); } Тело функции emplace_back() вызывает функцию chk_n_alloc() (см. раздел 13.5), чтобы гарантировать наличие достаточного места для элемента, и вызывает функцию construct(), чтобы создать элемент в позиции, на которую указывает указатель first_free. std::forward<Args>(args)... Развертывание в вызове функции construct() разворачивает оба пакета: параметров шаблона Args и параметров функции args. Эта схема создает элементы в формате: std::forward< Ti >( ti ) где Ti представляет тип i -го элемента в пакете параметров шаблона, a ti представляет i -й элемент в пакете параметров функции. Например, если svec имеет тип StrVec, то при вызове svec.emplace_back(10, 'c'); // добавит cccccccccc как новый последний // элемент схема в вызове функции construct() развернется в std::forward<int>(10), std::forward<char>(c) Использование функции forward() в этом вызове гарантирует, что если функция emplace_back() будет вызвана с r-значением, то функция construct() также получит r-значение. Например, в вызове svec.emplace_back(s1 + s2); // использует конструктор перемещения аргумент функции emplace_back() является r-значением, которое передается функции construct() как std::forward<string>(string("the end")) Page 886/1103 Типом результата вызова forward<string> будет strings&, поэтому функция construct() будет вызвана со ссылкой на r-значение. Функция construct(), в свою очередь, перенаправит этот аргумент конструктору перемещения класса string, чтобы создать этот элемент. Совет. Перенаправление и шаблоны с переменным количеством аргументов Функции с переменным количеством аргументов зачастую перенаправляют свои параметры другим функциям. Форма таких функций, как правило, подобна функции emplace_back(): // у функции fun() может быть любое количество параметров, каждый // из которых является ссылкой r-значения на тип параметра шаблона template<typename... Args> void fun(Args&&... args) // развертывание Args в список ссылок // на r-значения { // аргумент work() развертывает как Args, так и args work(std::forward<Args>(args)...); } Здесь предполагается перенаправить все аргументы функции fun() другой функции, work(), которая, по-видимому, осуществляет реальную работу. Как и вызов функции construct() в функции emplace_back(), развертывание в вызове функции work() разворачивает и пакет параметров шаблона, и пакет параметров функции. Поскольку параметры функции fun() являются ссылками на r-значение, функции fun() можно передать аргументы любого типа; поскольку для передачи этих аргументов используется функция std::forward(), вся информация о типах этих аргументов будет сохраняться в вызове функции work(). Упражнения раздела 16.4.3 Упражнение 16.58. Напишите функцию emplace_back() для собственного класса StrVec и для класса Vec, написанного в упражнении раздела 16.1.2. Упражнение 16.59. С учетом того, что s имеет тип string, объясните вызов svec.emplace_back(s). Упражнение 16.60. Объясните, как работает функция make_shared() (см. раздел 12.1.1). Упражнение 16.61. Определите собственную версию функции make_shared(). Page 887/1103 16.5. Специализация шаблона Не всегда можно написать один шаблон, который наилучшим образом подходит для всех возможных типов аргументов шаблона, для которых может быть создан его экземпляр. В некоторых случаях общий шаблон просто не подходит для типа: он либо приводит к ошибке при компиляции, либо к неправильным действиям. С другой стороны, иногда можно воспользоваться уникальными возможностями определенного типа для создания более эффективной функции, чем та, которой снабжен экземпляр общего шаблона. Функция compare() — хороший пример шаблона функции, общее определение которого не подходит для специфического типа, а именно символьных указателей. Хотелось бы, чтобы функция compare() сравнивала символьные указатели, используя функцию strcmp(), а не сравнивала значения указателей. Действительно, ведь уже есть перегруженная функция compare(), обрабатывающая символьные строковые литералы (см. раздел 16.1.1): // первая версия; может сравнить любые два типа template <typename Т> int compare(const T&, const T&); // вторая версия, для обработки строковых литералов template<size_t N, size_t M> int compare(const char (&)[N], const char (&)[M]); Однако версия функции compare() с двумя параметрами значения шаблона будет вызвана только при передаче строкового литерала или массива. Если происходит вызов функции compare() с символьными указателями, будет вызвана первая версия шаблона: const char *p1 = "hi", *p2 = "mom"; compare(p1, p2); // вызывает первый шаблон compare("hi", "mom"); // вызывает шаблон с двумя параметрами значения Нет никакого способа преобразовать указатель в ссылку на массив, поэтому вторая версия функции compare() не подходит для передачи указателей p1 и p2 как аргументов. Для обработки символьных указателей (в отличие от массивов) можно определить специализацию шаблона (template specialization) для первой версии функции compare(). Специализация — это отдельное определение шаблона, в котором определяется один или несколько параметров шаблона для получения специфического типа.Специализация шаблона функции При специализации шаблона функции следует предоставить аргументы для каждого параметра первоначального шаблона. Для указания специализации шаблона используется ключевое слово template, сопровождаемое парой пустых угловых скобок (<>). Пустые Page 888/1103 скобки означают, что аргументы будут предоставлены для всех параметров первоначального шаблона: // специальная версия compare() для работы с указателями на символьные // массивы template <> int compare(const char* const &p1, const char* const &p2) { return strcmp(p1, p2); } Трудная для понимания часть этой специализации относится к типам параметра функции. При определении специализации типы параметров функции должны совпадать с соответствующими типами ранее объявленного шаблона: template <typename Т> int compare(const T&, const T&); В этой специализации параметры функции являются ссылками на константные типы. Подобно псевдонимам типа, взаимодействие между типами параметра шаблона, указателями и константами может удивить (см. раздел 2.5.1). Необходимо определить специализацию шаблона этой функции с типом const char* для параметра Т. Функция потребует ссылки на константную версию этого типа. Константная версия типа указателя — это константный указатель, а не указатель на константу (см. раздел 2.4.2). В данной специализации следует использовать тип const char* const &, являющийся ссылкой на константный указатель на константный символ. Перегрузка функций или специализация шаблона При определении специализации шаблона функции разработчик, по существу, выполняет задачу компилятора. Таким образом, определение предоставляется для использования специфического экземпляра первоначального шаблона. Важно понимать, что специализация — это создание экземпляра функции; а не перегрузка ее экземпляра. Специализация создает экземпляр шаблона, а не перегружает его. В результате специализация не затрагивает механизм подбора функций. Может ли определение некой функции как специализации шаблона или как независимой, не шаблонной функции повлиять на подбор функций? Предположим, например, что имеется определение двух версий шаблонной функции compare(): той, что получает параметры как ссылки на массив, и другой, которая получает тип const T&. Факт наличия специализации для символьных указателей никак не влияет на подбор функции: compare("hi", "mom") Когда функция compare() вызывается для строкового литерала, оба шаблона функции оказываются подходящими и обеспечивают одинаково хорошее (т.е. точное) соответствие вызову. Однако версия с параметрами символьного массива более специализирована (см. раздел 16.3), она и выбирается для этого вызова. Page 889/1103 Если бы была определена версия функции compare(), получающая указатели на символы, как простая, не шаблонная функция (а не как специализация шаблона), то этот вызов разрешится по-другому. В данном случае было бы три подходящих функции: эти два шаблона и не шаблонная версия указателя на символ. Все три одинаково хорошо подходят для этого вызова. Как уже упоминалось, когда нешаблонная функция обеспечивает одинаково хорошее соответствие с шаблонной, выбирается нешаблонная функция (см. раздел 16.3). Ключевая концепция. Обычные правила области видимости относятся и к специализации Чтобы специализировать шаблон, объявление его оригинала должно быть в области видимости. Кроме того, объявление специализации должно быть в области видимости перед любым кодом, использующим экземпляр шаблона. Пропуск объявления обычных классов и функций найти очень просто — компилятор не сможет обработать такой код. Но при отсутствии объявления специализации компилятор обычно создает код, используя первоначальный шаблон. Поэтому ошибки в порядке объявления шаблона и его специализации довольно просто допустить, но очень трудно найти. Использование специализации и экземпляра первоначального шаблона с тем же набором аргументов шаблона является ошибкой. Но компилятор вряд ли обнаружит эту ошибку. Шаблоны и их специализации должны быть объявлены в том же файле заголовка. Объявления всех шаблонов с данным именем должны располагаться сначала, а затем все специализации этих шаблонов. Специализация шаблона класса Кроме специализации шаблонов функций, вполне можно также специализировать шаблоны классов. В качестве примера определим специализацию библиотечного шаблона hash, который можно использовать для хранения объектов класса Sales_data в неупорядоченном контейнере. По умолчанию неупорядоченные контейнеры используют для организации своих элементов класс hash<key_type> (см. раздел 11.4). Чтобы использовать его с собственным типом данных, следует определить специализацию шаблона hash. Специализированный класс hash должен определять следующее. • Перегруженный оператор вызова (см. раздел 14.8), возвращающий тип size_t и получающий объект типа ключа контейнера. • Два члена-типа result_type и argument_type, соответствующие типу возвращаемого значения и типу аргумента оператора вызова. • Стандартный конструктор и оператор присвоения копии, которые могут быть определены неявно (см. раздел 13.1.2). Единственное осложнение в определении этой специализации класса hash состоит в том, что специализация шаблона должна быть в том же пространстве имен, в котором определяется первоначальный шаблон. Более подробная информация о пространствах имен приведена в разделе 18.2, а пока достаточно знать, что к пространству имен можно добавлять члены. Для этого следует сначала открыть пространство имен: // открыть пространство имен std, чтобы можно было специализировать // класс std::hash namespace std { Page 890/1103 } // закрыть пространство имен std; обратите внимание: никакой точки с // запятой после закрывающей фигурной скобки Любые определения, расположенные между открывающей и закрывающей фигурными скобками, будут частью пространства имен std. Следующий код определяет специализацию класса hash для класса Sales_data: // открыть пространство имен std, чтобы можно было специализировать // класс std::hash namespace std { template <> // определение специализации с параметром struct hash<Sales_data> // шаблона класса Sales_data { // тип, используемый для неупорядоченного контейнера hash, должен // определять следующие типы typedef size_t result_type; typedef Sales_data argument_type; // по умолчанию этому типу // требуется оператор == size_t operator()(const Sales_data& s) const; // класс использует синтезируемые функции управления копированием // и стандартный конструктор Page 891/1103 }; size_t hash<Sales_data>::operator()(const Sales_data& s) const { return hash<string>()(s.bookNo) ^ hash<unsigned>()(s.units_sold) ^ hash<double>()(s.revenue); } } // закрыть пространство имен std; обратите внимание: никакой точки с // запятой после закрывающей фигурной скобки Определение hash<Sales_data> начинается с части template<>, означающей, что определяется полностью специализированный шаблон. Специализируемый шаблон называется hash, а специализированная версия — hash<Sales_data>. Члены класса следуют непосредственно из требований для специализации шаблона hash. Подобно любым другим классам, специализируемые члены можно определить в классе или вне его, как это сделано здесь. Перегруженный оператор вызова должен определять хеш-функцию по значениям заданного типа. Эта функция обязана возвращать каждый раз тот же результат, когда она вызывается для данного значения. Хеш-функция практически всегда возвращает другой результат для не равных объектов. Все сложности определения хорошей хеш-функции делегируем библиотеке. Библиотека определяет специализации класса hash для встроенных типов и для большинства библиотечных типов. Безымянный объект hash<string> используется для создания хеш-кода для переменной-члена bookNo, объект типа hash<unsigned> для создания хеш-кода из переменной-члена units_sold и объекта типа hash<double> для создания хеш-кода из переменной-члена revenue. Применение к этим результатам оператора исключающего ИЛИ (см. раздел 4.8) сформирует общий хеш-код для заданного объекта класса Sales_data. Следует заметить, что хеш-функция определена для хеширования всех трех переменных-членов, чтобы она была совместима с определением оператора operator== класса Sales_data (см. раздел 14.3.1). По умолчанию неупорядоченные контейнеры используют специализацию хеша, соответствующую типу key_type, наряду с оператором равенства типа ключа. С учетом того, что специализация находится в области видимости, она будет использоваться автоматически при использовании класса Sales_data как ключ в одном из этих контейнеров: // использует hash<Sales_data> и оператор operator== класса Sales_data // из раздела 14.3.1 Page 892/1103 unordered_multiset<Sales_data> SDset; Поскольку hash<Sales_data> использует закрытые члены класса Sales_data, этот класс следует сделать другом класса Sales_data: template <class T> class std::hash; // нужно для объявления // дружественным class Sales_data { friend class std::hash<Sales_data>; // другие члены, как прежде }; Здесь указано, что специфический экземпляр hash<Sales_data> является дружественным. Поскольку данный экземпляр определяется в пространстве имен std, следует помнить, что этот тип хеша определяется в пространстве имен std. Следовательно, объявление friend относится к std::hash. Чтобы позволить пользователям класса Sales_data использовать специализацию шаблона hash, следует определить эту специализацию в заголовке Sales_data. Частичная специализация шаблона класса В отличие от шаблона функции, специализация шаблона класса не обязана предоставлять аргументы для каждого параметра шаблона. Можно определить некоторые из них, но не все. Частичная специализация (partial specialization) шаблона класса сама является шаблоном. Пользователи должны предоставить аргументы для тех параметров шаблона, которые не затронуты специализацией. Частично можно специализировать только шаблон класса. Нельзя частично специализировать шаблон функции. Библиотечный тип remove_reference был представлен в разделе 16.2.3, он работает с серией специализаций: // первоначальный, наиболее общий шаблон template <class Т> struct remove_reference { typedef T type; }; // частичные специализации, которые будут использоваться для ссылок Page 893/1103 // на l- и r-значения template <class Т> struct remove_reference<T&> // ссылки на l-значение { typedef Т type; }; template <class T> struct remove_reference<T&&> // ссылки на r-значение { typedef T type; }; Первый шаблон определяет самую общую версию. Его экземпляр может быть создан с любым типом; он использует свой аргумент шаблона как тип для своего члена type. Следующие два класса — это частичные специализации первоначального шаблона. Поскольку частичная специализация — это шаблон, начнем, как обычно, с определения параметров шаблона. Подобно любой другой специализации, у частичной специализации то же имя, что и у специализируемого шаблона. Список параметров специализации шаблона включает элементы для каждого параметра шаблона, тип которого не был определен полностью при частичной специализации. После имени класса располагаются аргументы для параметров специализируемого шаблона. Эти аргументы располагаются в угловых скобках после имени шаблона. Аргументы позиционально соответствуют параметрам первоначального шаблона. Список параметров шаблона частичной специализации — это подмножество или специализация списка параметров первоначального шаблона. В данном случае у специализаций то же количество параметров, что и у первоначального шаблона. Но тип параметров в специализациях отличается от первоначального шаблона. Специализация будут использоваться для ссылок на типы l- и r-значений соответственно: int i; // decltype(42) - это int, используется первоначальный шаблон remove_reference<decltype(42)>::type a; // decltype(i) - это int&, используется первая (Т&) частичная // специализация remove_reference<decltype(i)>::type b; // decltype(std::move(i)) - это int&&, используется вторая (т.е., T&&) // Page 894/1103 частичная специализация remove_reference<decltype(std::move(i))>::type c; У всех трех переменных, a, b и с, тип int. Специализация членов, но не класса Вместо специализации всего шаблона можно специализировать только одну или несколько его функций-членов. Например, если Foo — это шаблон класса с членом Bar, можно специализировать только этот член: template <typename Т> struct Foo { Foo (const T &t = T()): mem(t) { } void Bar() { /* ... */ } T mem; // другие члены класса Foo }; template<> // специализация шаблона void Foo<int>::Bar() // специализация члена Bar класса Foo<int> { // осуществить всю специализированную обработку, относящуюся к целым // числам } Здесь специализируется только один член класса Foo<int>. Другие его члены предоставляются шаблоном Foo: Foo<string> fs; // создает экземпляр Foo<string>::Foo() fs.Bar(); // создает экземпляр Foo<string>::Bar() Foo<int> fi; // создает экземпляр Foo<int>::Foo() Page 895/1103 fi.Bar(); // использует специализацию Foo<int>::Bar() При использовании шаблона Foo с любым типом, кроме int, члены экземпляра создаются, как обычно. При использовании шаблона Foo с типом int все члены экземпляра, кроме Bar, создаются, как обычно. Если использовать член Bar класса Foo<int>, то получится специализированное определение. Упражнения раздела 16.5 Упражнение 16.62. Определите собственную версию класса hash<Sales_data> и контейнер unordered_multiset объектов класса Sales_data. Поместите в контейнер несколько транзакций и выведите его содержимое. Упражнение 16.63. Определите шаблон функции для подсчета количества вхождений заданного значения в векторе. Проверьте программу, передав ей вектор значений типа double, вектор целых чисел и вектор строк. Упражнение 16.64. Напишите специализированную версию шаблона из предыдущего упражнения для обработки вектора vector<const char*> и используйте ее в программе. Упражнение 16.65. В разделе 16.3 были определены две перегруженных версии функции debug_rep(), одна из которых получает параметр типа const char*, а вторая — типа char*. Перепишите эти функции как специализации. |