Трюки со специализацией шаблонов C++

    imageСпециализация шаблонов является одной из «сложных» фичей языка с++ и использутся в основном при создании библиотек. К сожалению, некоторые особенности специализации шаблонов не очень хорошо раскрыты в популярных книгах по этому языку. Более того, даже 53 страницы официального ISO стандарта языка, посвященные шаблонам, описывают интересные детали сумбурно, оставляя многое на «догадайтесь сами — это же очевидно». Под катом я постарался ясно изложить базовые принципы специализации шаблонов и показать как эти принципы можно использовать в построении магических заклинаний.


    Hello World


    Как мы привыкли использовать шаблоны? Используем ключевое слово template, затем в угловых скобках имена параметров шаблона, после чего тип и имя. Для параметров также указывают что это такое: тип (typename) или значение (например, int). Тип самого шаблона может быть класс (class), структура (struct — вообщем-то тоже класс) или функция (bool foo() и так далее). Например, простейший шаблонный класс 'A' можно задать вот так:
    image
    Через некоторое время мы захотим, чтобы наш класс для всех типов работал одинаково, а для какого-нибудь хитрого вроде int — по-другому. Фигня вопрос, пишем специализацию: выглядит так же как объявление но параметры шаблона в угловых скобках не указываем, вместо этого указываем конкретные аргументы шаблона после его имени:

    template<> class A< int > {}; // здесь int - это аргумент шаблона
    

    Готово, можно писать методы и поля специальной реализации для int. Такая специализация обычно называется полной (full specialization или explicit specialization). Для большинства практических задач большего не требуется. А если требуется, то…

    Специализированный шаблон — это новый шаблон


    Если внимательно читать ISO стандарт С++, то можно обнаружить интересное утверждение: создав специализированный шаблонный класс мы создаем новый шаблонный класс (14.5.4.3). Что это нам дает? Специализированный шаблонный класс может содержать методы, поля или объявления типов которых нет в шаблонном классе который мы специализируем. Удобно, когда нужно чтобы метод шаблонного класса работал только для конкретной специализации — достаточно объявить метод только в этой специализации, остальное сделает компилятор:
    метод только в специализации

    Специализированный шаблон может иметь свои параметры шаблона


    Дьявол, как известно, в деталях. То, что специализированный шаблонный класс это совсем-совсем новый и отдельный класс конечно интересно, но магии в этом мало. А магия есть в незначительном следствии — если это отдельный шаблонный класс, то он может иметь отдельные, никак не связанные с неспециализированным шаблонным классом параметры (параметры — это то, что после template в угловых скобках). Например, вот так:

    template< typename S, typename U > class A< int > {};
    

    Правда, именно такой код компилятор не скомпилирует — новые параметры шаблона S и U мы никак не используем, что для специализированного шаблонного класса запрещено (а то что это класс специализированный компилятор понимает потому, что у него такое же имя 'A' как у уже объявленного шаблонного класса). Компилятор даже специальную ошибку скажет: «explicit specialization is using partial specialization syntax, use template<> instead». Намекает, что если сказать нечего — то надо использовать template<> и не выпендриваться. Тогда для чего же в специализированном шаблонном классе можно использовать новые параметры? Ответ странный — для того, чтобы задать аргументы специализации (аргументы — это то, что после имени класса в угловых скобках). То есть специализируя шаблонный класс мы можем вместо простого и понятного int специализировать его через новые параметры:

    template< typename S, typename U > class A< std::map< S, U > > {};
    

    Такая странная запись скомпилируется. И при использовании получившегося шаблонного класса с std::map будет использована специализация, где тип ключа std::map будет доступен как параметр нового шаблона S, а тип значения std::map как U.

    Такая специализация шаблона, при которой задается новый список параметров и через эти параметры задаются аргументы для специализации называется частичной специализацией (partial specialization). Почему «частичной»? Видимо потому, что изначально задумывалась как синтаксис для специализации шаблона не по всем аргументам. Пример, где шаблонный класс с двумя параметрами специализируется только по одному из них (специализация будет работать когда первый аргумент, T, будет указан как int. При этом второй аргумент может быть любым — для этого в частичной специализации введен новый параметр U и указан в списке аргументов для специализации):

    template< typename T, typename S > class B {};
    template< typename U > class B< int, U > {};
    

    Магические последствия частичной специализации


    Из двух вышеописанных свойств специализации шаблонов есть ряд интересных следствий. Например, при использовании частичной специализации можно, вводя новые параметры шаблона и описывая через них специализированные аргументы, разбивать составные типы на простейшие. В приведенном ниже примере специализированный шаблонный класс A будет использован, если аргументов шаблона является тип указателя на функцию. При этом через новые параметры шаблона S и U можно получить тип возвращаемого значения этой функции и тип ее аргумента:

    template< typename S, typename U > class A< S(*)(U) > {};
    

    А если в специализированном шаблоне объявить typedef или static const int (пользуясь тем, что это новый шаблон), то можно использовать его для извлечения нужной информации из типа. Например, мы используем шаблонный класс для хранения объектов и хотим получить размер переданного объекта или 0, если это указатель. В две строчки:

    template< typename T > struct Get { const static int Size = sizeof( T ); };
    template< typename S > struct Get< S* > { const static int Size = 0; };
    
    Get< int >::Size  // например, 4
    Get< int* >::Size // 0 - нашли указатель :)
    

    Магия этого типа используется в основном в библиотеках: stl, boost, loki и так далее. Конечно, при высокоуровневом программировании использовать такие фокусы череповато — думаю, все помнят конструкцию для получения размера массива :). Но в библиотеках частичная специализация позволяет относительно просто реализовывать делегаты, события, сложные контейнеры и прочие иногда очень нужные и полезные вещи.

    Коллеги, если найдете ошибку (а я, к сожалению, не гуру — могу ошибаться) или у Вас есть критика, вопросы али дополнения к вышеизложенному — буду рад комментариям.

    Update: Обещанное продолжение тут.
    Support the author
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 61

      +2
      Ооо! Последователи Александреску :-) Его книжка и библиотека просто показательны в магии шаблонов. Вот только жаль не все фичи шаблонов поддерживаются компиляторами.
        0
        У Александреску на мой взгляд сложно изложено. Большинство современных актуальных компиляторов ( vs2005, intel, gcc, xcode ) частичную специализацию поддерживают, хотя есть нюансы.
          –2
          xcode и vs2005 — IDE, а не компиляторы ;)
            +4
            я думаю все это знают :). Подрозумевается Microsoft Visual C++ Compiler 8.0+ и GCC 4.0+ with
        0
        Си тут ни при чем. Тег поправьте, пожалуйста.
          0
          готово. отсутствие на хабре '+' ввело меня в заблуждение :)
          +1
          Отличная статья. Отчасти материал частичной специализации шаблонов также рассматривал Саттер. У него хорошим, понятным языком изложено, рекомендую :)
            +1
            А не уточните, в каком из его трудов? У меня сейчас на полке exceptional, more exceptional и его совместное с Александреску про coding standards. Что-то я на память у него простого изложения не помню. По крайней мере о том, что частичная специализация — это новый класс я узнал значительно после прочтения трудов Саттера и был очень неприятно удивлен такому «открытию». Зато сразу понял еще часть stl и boost :).
              +1
              В русском варианте — это «Новые сложные задачи на С++», в главе про пример Димова-Абрамса (со специализацией функций) он рассмотрел частично этот вопрос.

              Вообще, про новый класс — это в принципе логически понятно. Ведь по сути — шаблон, это еще не класс как таковой. Не инстанциированный шаблон — вообще не компилируется. Шаблон — это указания компилятору правил по созданию классов во время компиляции. А специализация, частичная специализация, не просто указывает компилятору на то, что нужно сгенерировать код для нового класса/фунции, но и создает новый уровень индирекции при поиске, к примеру, вариантов для перегрузки.
            +11
            меня всегда поражало, что в С++ при должном умении с помощь перегрузки операторов и шаблонной магии можно эмулировать достаточно сложные концепции, свойственные другим языкам… Смотришь на какой-нибудь boost::proto и думаешь даааа! чего только не делают люди лишь бы не писать на Haskell =)

              0
              Тут есть нюанс. В С++ можно не только эмулировать, но и заточить под специфические нужды. В более высокоуровневых языках части приходится довольствоваться только тем, что есть :). Тут вопрос в ресурсах и трубованиях — для каждого языка есть области, для которых он «заточен» и там уделывает все живое.
                +3
                Так вот во всех холливарах постоянно всплывает один и тот же вопрос: а не проще ли взять уже заточенный язык?

                Т.е. я понимаю можно бить в бубны и совершать ритуальные пляски с мощью шаблонов, а можно скажем взять наговнякать парсер для DSLа и сгенерировать скажем С-шный код.

                P.S. Я ничего не имею против С++, но сильно надеюсь, что писать или поддерживать приложения на нём (а не на его подможестве aka С with classes) мне никогда не придётся =) Слишком громоздко. Just a matter of taste.

                  +5
                  Мое мнение — зависит от задачи. Для одних задач лучше один язык + фреймворк, для других — другой. Лично я в разных проектах использую c, c++, c#, java, python и php. По моему опыту — если «не промахнуться» с выбором языка и технологии вообще — то все будет сухо и комфортно. Ну а если промахнуться и попытаться kernel mode windows driver писать на haskell… тут наверное даже бубны не помогут :)
                    +1
                    Я вас абсолютно поддерживаю. Промахнутся и воспользоваться C++ exception в оном драйвере наверное тоже будет не очень приятно =)

                    Мне вот как компиляторщику всегда было интересно, что может дать тот или иной язык при написании собственно компилятора или VM. Почему-то тот же Hotspot (а это ведь фактически rocket science на ниве compiler construction) написан именно в стиле С-with-classes, без особой метамагии…

                    Хочется поэтому понять, где же метамагия действительно по делу?

                      +3
                      Постараюсь через пару недель раскрыть тему про сиськи метамагию. Делегаты, события, invertion of control framework, event driver programming, actor model.
                        0
                        Спасибо. Было бы вообще здорово если бы вы при этом учли «соперников» (например, специализированные языки, либо просто языки, в которых такие фичи уже есть) и показали, почему С++ их делает на некоторой конкретной задаче и по каким параметрам =)

                          0
                          А тут беда — C++ в этой области их вообще не делает :). Максимум что я могу — это показать «как сделать то же самое на c# более просто и в пять раз меньше строк кода» :). Такая магия обычно используется, когда очень хочется применять высокоуровневые абстракции ( делегаты, сообщения ) в языке, который для этого не совсем предназначен но который мы обязаны использовать. Некоторые проекты требуют С++, нельзя ни смешать технологии, ни использовать другие. А хорошо жить хочется. Приходится колдовать.
                            0
                            Я не прав, в том, что применив boost мы получаем все эти радости жизни, разве только без gc, коя неизвестно радость или горесть :)
                              0
                              Ага, получаем радости и к ним лес граблей и кучу трудночитаемого кода.

                              Все-таки каждой области — свой инструмент, цпп хорош для тех нечастых случаев когда нужен кусок очень высокопроизводительного кода (хотя тру аскеты пользуют plain C, и в этом есть немало здравого смысла, но мне в си темплейтов не хватает %). Ну или фреймворк не хочется с собой таскать (shareware).
                                +1
                                Ну нечитаемость кода вещь довольно субъективная :)

                                Я не супер-программист, но ещё ни разу я не сталкивался чтобы c++ ограничивал меня в чём то, точнее когда я натыкаюсь на это, то это скорее всего кривизна архитектуры.

                                Вот что реально бесит, так это сообщения об ошибках в духе

                                In member function ‘boost::tuples::tuple<cvm::basic_scmatrix<double, std::complex>, cvm::basic_cvector<double, std::complex>, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type> Structure::dispeq(int, double)’:

                                Ну и когда подойтёт C++0x, с его концептами, автотипами и прочими радостями, буудет вообще класс.
                                  0
                                  Естессна принципиальных ограничений у цпп нету, можно написать что угодно (как и на машине тьюринга, хехе).

                                  Проблема в том что он слишком сложный (-> труднее найти/обучить программера), содержит кучу неочевидных заподлянок, заставляет думать о низкоуровневых проблемах там, где без этого вполне можно было бы обойтись. Это все делает разработку на цпп более дорогой и менее эффективной.

                                  Ну и многие интересные и полезные (реально!) вещи типа замыканий и лямбд на нем хоть и можно как-то реализовать, но пользоваться ими потом очень неудобно.

                                  Основная сильная сторона — это мощные оптимизирующие компиляторы, и неплохие средства compile-time программирования (шаблоны). Поэтому я например, хоть и заработал большую часть бабла в своей жизни программированием на C++, сейчас его использую только там где скорость выполнения очень важна — для остального есть сишарп, ява и питон, и времени они мне экономят кучу.

                                  (надо заметить, у шароварщиков есть дополнительные причины использовать цпп, но это не по моей теме).

                                    0
                                    еще это достаточно простой способ низкоуровневой интеграции в операционную систему: виртуальное оборудование, сервисы, расширение и изменение функциональности ( тот же punto switcher или radmin ). Отсутствие необходимости разворачивать framework для конечных пользователей.
                                  0
                                  Кривизна архитектуры приложения я имел ввиду.
                                  0
                                  Он по многим корпоративным стандартам не проходит. Кто-то не хочет держать у себя много егабайт кода в котором каждый месяц находят много багов ( для сертификации, например, критично ). Кто-то по стандартам не может использовать исключения. А так я конечно обеими руками за буст, штука очень хорошая.
                            0
                            Ооо сиплюсплюс эксепшены в драйвере (под wince) это было вообще супер! Неделя отладки, незабываемый опыт по изучению внутренностей винды и плюсового CRT!

                            Хотя результат конечно можно было и так предсказать :)
                              0
                              что же вы с компилятором сделали :). Они же обычно отказываются С++ код в драйвер компилить — там бинарный формат исполняемого файла немного другой да и вообще О_О.
                                0
                                Под winCE там драйвер это просто dll (это, видимо, и подкупило автора того кода :), так что с компилятором ничо делать не надо.
                              0
                              > Почему-то тот же Hotspot (а это ведь фактически rocket science на
                              > ниве compiler construction) написан именно в стиле С-with-classes, без особой метамагии…

                              Потому что те, кто его писал, не умеют писать на С++. Во многих местах метамагию можно было бы применить довольно по делу.
                              Есть также мнение, что метамагия не применялась по религиозным (политическим) соображениям.
                              ;)
                        +1
                        Почему-то сразу вспомнились разные метакомпиляторы, используемые поверх C++. С ними функционал языка кажется просто безграничным. Начиная с bison/flex для реализации других языков и заканчивая метаобъектным компилятором Qt.
                          0
                          согласен, сам qt для некоторых проектов использую, очень удобно. Но зависит от задачи — иногда проще написать нужное на шабллоне, чем тянуть метакомпилятор и соответствующие техпроцессы.
                          0
                          Было где-то, что если знаешь c++ то изучить другие языки легко, только вот зачем :)

                          Сам занимаюсь матмоделированием, начинал на MATLAB, пробовал python, но всё таки остановился на c++ чем очень доволен.Всякие дикие извраты с шаблонами использую :)
                          0
                          На первой картинке ошибка:

                          >> A---> void A::foo(int)

                            0
                            благодарствую, исправил
                            +1
                            спасибо, классная статья
                              0
                              спасибо. тема действительно сложная и хорошей литературы по ней маловато.
                                +1
                                Статья полезная и действительно в книгах много чего не пишут ))) А это живой опыт и практика!
                                Если будут продолжения с удовольствием буду читать :-)
                                  +1
                                  да, все будет. Меня очень интересует реакция на более сложные вещи, а это так сказать подготовка к ним — дабы можно было из последующих статей ссылаться сюда в виде «как получить тип возврата функции — см. сюда» :)
                                  0
                                  > … не очень хорошо раскрыты в популярных книгах по си.
                                  Не совсем понимаю причем тут си. Опечатка?
                                    0
                                    не очень хотелось писать длинное «си плюс плюс» или второй раз «c++». Переделал.
                                    0
                                    Спасибо автору. Я сейчас сам начал сильно углубляться в C++. Вот сижу пишу игрушку. :)
                                    P.S: если кому-то интересна игрушка «точки», то можете посмотреть: https://sourceforge.net/projects/gdots
                                    P.S.S: есть только сорцы, и те без вменяемых тулз для конфигурации. Если кто-то хочет, то попробуйте собрать так, как есть. У многих собралось.
                                      +3
                                      Спасибо, пошли действительно полезные статьи по c++
                                        0
                                        minifix: «параметры — это то, что после template в фигурных скобках» там не фигурные а угловые скобки
                                          0
                                          спасибо, исправил
                                          0
                                          Раз автор задел тему шаблонов, советую написать вам еще тему о том почему специализации шаблонов функций не перегружаются и как это протеворечит интуитивным ожиданиям со стороны разработчика.)
                                            0
                                            Уточните, пожалуйста. Там 20 страниц стандарта, я в детали особо не вдавался — специализация функций применяется достаточно редко :(. Вот такой код в VS2005 компилируется и работает корректно:

                                            #include <windows.h>
                                            #include // основной шаблон
                                            templatevoid foo( T ) {}

                                            // специализированый шаблон
                                            template< typename S, typename U > void foo( std::map< S, U > ) {}

                                            // перегрузка основного шаблона
                                            templatevoid foo( T, int ) {}

                                            // перегрузка специализированного шаблона
                                            template< typename S, typename U > void foo( std::map< S, U >, int ) {}

                                            void main( void )
                                            {
                                            foo( 10 );
                                            std::map< int, char > oMap;
                                            foo( oMap );
                                            foo( 10, 11 );
                                            foo( oMap, 11 );
                                            }

                                            Вы какой случай имели в виду?
                                            +1
                                            У Саттера есть пример Димова-Абрамса:

                                            template
                                            void f(T);

                                            template
                                            void f(T*);

                                            template
                                            void f(int*)



                                            int *p;
                                            f(p); // Что вызывается??

                                            А теперь фокус… меняем обьявления местами:

                                            template
                                            void f(T);

                                            template
                                            void f(int*)

                                            template
                                            void f(T*);

                                            int *p;
                                            f(p); // Что вызывается?? То же самое? Отнюдь!

                                            Вот про это я говорю, можно написать статейку)
                                              0
                                              Хабра поела синтаксис… сейчас исправлю!(не могу чаще 5 минут постить)
                                                0
                                                из того что я понял по съеденному синтаксису — валидное поведение, вы используете механизм «угадывания» аргументов шаблона по аргументам функции. Алгоритм угадывания зависит от того, что было написано перед определением. Если не использовать угадывание а вручную прописать от чего специализируем: fили f< int* > — то все работает однозначно.
                                                  0
                                                  У Саттера есть пример Димова-Абрамса:

                                                  template <class T>
                                                  void f(T);

                                                  template<class T>
                                                  void f(T*);

                                                  template<>
                                                  void f(int*)



                                                  int *p;
                                                  f(p); // Что вызывается??

                                                  А теперь фокус… меняем обьявления местами:

                                                  template<class T>
                                                  void f(T);

                                                  template<>
                                                  void f(int*)

                                                  template<class T>
                                                  void f(T*);

                                                  int *p;
                                                  f(p); // Что вызывается?? То же самое? Отнюдь!

                                                  Вот про это я говорю, можно написать статейку)

                                                  PS. Я бы не называл механизм выведения — угадыванием..)
                                                  ЗЫ. Текущая статья была названа «трюки» хотя самих трюков в ней не йоты, посему я решил что этот нюанс тоже можно было бы осветить… ваше право)
                                                    0
                                                    Усе правильно.
                                                    template<> foid f( int* ) 
                                                    автоматически угадывает аргумент шаблона по аргументу функции. Так как до ее объявления у нас уже была специализация
                                                    template<class T> void f(T*);
                                                    то в соответствии с логикой угадывания ( 15 страниц мелким шрифтм стандарта ^_^ ) компилятор угадал вот так:
                                                    template<> foid f< int >( int* ) 
                                                    . Во втором слуае во время угадывания получилось
                                                    template<> foid f< int* >( int* ) 
                                                    . Все честно.

                                                    А приоритеты вызова еще на 5 страницах стандарта, «как компилятор узнает какая из специализированных шаблонных функций более специализированная» :).

                                                    Я подумаю как это добавить в статью. Оно просто малопреминимо, не хотелось бы засорять избыточной информацией.
                                                      0
                                                      В стандарте нет мелкого шрифта) Только курсив, жирности и моноширный, для всего свой конекст…
                                                      Мало людей учит язык по стандарту, и далеко не много прибегают к нему при разрешение трудных ситуаций (хотя это самое правильное дело), потому что даже стандарт отмалчивается в многих спорных моментах. Да и чтение стандарта имеет малый КПД.

                                                      ЗЫЫ… Он выводит)) выводит))) to deduce != to guess )))
                                                      Слово угадывать асоциируется с недетерминированностью, компилятор же все выведения делает строго по правилам
                                                        0
                                                        Полностью вас поддерживаю — многие вещи в стандарте описаны достаточно сумбурно, что я пометил в топике и что послужило одной из причин его написания. Что же до слова «угадывать» — к сожалению, частая преподавательская деятельность заставила меня приучиться использовать упрощенные термины, дабы за терминологией не терять смысл. Мне сейчас «чисто конкретно по научному и в когнитивной синергии со специализированной терминологией, свойственной для майоритарной совокупности парадигм вышеназваной предметной области» достаточно тяжело писать :). Но я думаю что это ведь не главное? ^_^
                                                          0
                                                          Сугубосубъективное мое мнение: учить студентов \ школьников что компилятор угадывает) это не очень хорошо)
                                                          Прочтение книги Дракона дает полное понятие о том что и как «угадывает» компилятор)))
                                                            0
                                                            Согласен, не очень хорошо. Но когда есть два варианта — человек поверхностно и не совсем глубоко поймет как работает
                                                            template<> foo( int )
                                                            или корректно и в правильных терминах не поймет — приходится применять первый вариант. А если будет нужно или человеку самому будет интересно — можно потом расширить имеющиеся знания рассказав что угадывание на самом деле подчиняется сложным правилам. Но это все понятное дело не silver bullet — просто я так привык делать и переучиваться уже поздно :).
                                                              0
                                                              P.S. Есть еще третий вариант — человеку в правильных терминах и полностью корректно рассказали и он все понял. Но это либо человек шибко умный должен быть, либо очень хороший преподаватель. Я так не могу :(.
                                                0
                                                статья порадовала. благодарю автора!
                                                  +1
                                                  > Конечно, при высокоуровневом программировании использовать такие фокусы череповато — думаю, все помнят конструкцию для получения размера массива :)

                                                  Можно про «конструкцию для получения размера массива» подробнее?
                                                    0
                                                    Подробнее расписано, например, здесь
                                                      0
                                                      Если не ошибаюсь, то именно такая конструкция использована у Майкрософт — она называется _countof().
                                                    0
                                                    Картинки умерли (
                                                      0
                                                      Увы, это очень старая статья, тогда Хабр еще не умел перезаливать на gabrastorage. Я у себя не смог найти оригиналы :( Если кто покажет где сохранилась старая версия — без проблем перезалью

                                                    Only users with full accounts can post comments. Log in, please.