Как определить шаблон в заголовочном файле c

Обновлено: 18.05.2024

(Пояснение: заголовочные файлы не являются только портативное решение. Но они являются наиболее удобным портативным решением.)

Решение

это не Необходимо поместить реализацию в заголовочный файл, см. альтернативное решение в конце этого ответа.

В любом случае, причина вашего кода заключается в том, что при создании экземпляра шаблона компилятор создает новый класс с заданным аргументом шаблона. Например:

При чтении этой строки компилятор создаст новый класс (назовем его FooInt ), что эквивалентно следующему:

Следовательно, компилятор должен иметь доступ к реализации методов, чтобы создавать их экземпляры с помощью аргумента шаблона (в этом случае int ). Если бы эти реализации не были в заголовке, они не были бы доступны, и поэтому компилятор не смог бы создать экземпляр шаблона.

Распространенным решением этой проблемы является запись объявления шаблона в файл заголовка, затем реализация класса в файле реализации (например, .tpp) и включение этого файла реализации в конец заголовка.

Таким образом, реализация по-прежнему отделена от объявления, но доступна для компилятора.

Другое решение состоит в том, чтобы отделить реализацию и явно создать экземпляр всех необходимых вам шаблонов:

Если мое объяснение недостаточно ясно, вы можете посмотреть на C ++ Super-FAQ по этому вопросу .

Другие решения

Если вы, в нижней части файла cpp реализации, сделаете явное создание экземпляров всех типов, с которыми будет использоваться шаблон, компоновщик сможет найти их как обычно.

Редактировать: Добавление примера явной реализации шаблона. Используется после определения шаблона и определения всех функций-членов.

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

Приведенный выше пример довольно бесполезен, поскольку вектор полностью определен в заголовках, за исключением случаев, когда используется общий включаемый файл (предварительно скомпилированный заголовок?) extern template class vector<int> чтобы не создавать его во всех Другой (1000?) Файлы, которые используют вектор.

Это связано с требованием отдельной компиляции и тем, что шаблоны являются полиморфизмом в стиле экземпляров.

Давайте немного приблизимся к конкретному для объяснения. Скажем, у меня есть следующие файлы:

  • foo.h
    • объявляет интерфейс class MyClass<T>
    • определяет реализацию class MyClass<T>
    • использования MyClass<int>

    Отдельная компиляция означает, что я должен быть в состоянии собрать foo.cpp независимо от bar.cpp. Компилятор выполняет всю тяжелую работу по анализу, оптимизации и генерации кода на каждом модуле компиляции полностью независимо; нам не нужно делать анализ всей программы. Только компоновщик должен обрабатывать всю программу одновременно, и работа компоновщика существенно проще.

    bar.cpp даже не нужно существовать, когда я компилирую foo.cpp, но я все еще должен быть в состоянии связать foo.o Я уже имел вместе с bar.o Я только что произвел, без необходимости перекомпилировать foo.cpp. foo.cpp может даже быть скомпилирован в динамическую библиотеку, распространенную в другом месте без foo.cpp, и связаны с кодом, который они пишут спустя годы после того, как я написал foo.cpp.

    «Полиморфизм стиля реализации» означает, что шаблон MyClass<T> на самом деле не является универсальным классом, который может быть скомпилирован в код, который может работать для любого значения T , Это добавило бы дополнительные издержки, такие как бокс, необходимость передавать указатели на функции распределителям и конструкторам и т. Д. Цель шаблонов C ++ состоит в том, чтобы избежать необходимости писать почти идентичные class MyClass_int , class MyClass_float и т. д., но мы все равно можем получить скомпилированный код, который выглядит так, как если бы мы имел написана каждая версия отдельно. Таким образом, шаблон в прямом смысле шаблон; шаблон класса не класс, это рецепт для создания нового класса для каждого T мы сталкиваемся. Шаблон не может быть скомпилирован в код, может быть скомпилирован только результат создания шаблона.

    Так когда foo.cpp компилируется, компилятор не видит bar.cpp знать, что MyClass<int> нужно. Это может видеть шаблон MyClass<T> , но он не может генерировать код для этого (это шаблон, а не класс). И когда bar.cpp компилируется, компилятор может видеть, что ему нужно создать MyClass<int> , но он не видит шаблон MyClass<T> (только его интерфейс в foo.h) поэтому он не может создать это.

    Если foo.cpp сам использует MyClass<int> код для этого будет сгенерирован во время компиляции foo.cpp, так когда bar.o связан с foo.o они могут быть подключены и будут работать. Мы можем использовать этот факт, чтобы разрешить реализацию конечного набора шаблонов в файле .cpp, написав один шаблон. Но нет никакого способа для bar.cpp использовать шаблон как шаблон и создавать его на любых типах, которые ему нравятся; он может использовать только существующие версии шаблонного класса, который автор foo.cpp думал предоставить.

    Вы можете подумать, что при компиляции шаблона компилятор должен «генерировать все версии», а те, которые никогда не используются, отфильтровываются во время компоновки. Помимо огромных накладных расходов и чрезвычайных трудностей, с которыми столкнулся бы такой подход, потому что функции «модификатора типа», такие как указатели и массивы, позволяют даже только встроенным типам создавать бесконечное число типов, что происходит, когда я теперь расширяю свою программу добавляя:

    • baz.cpp
      • объявляет и реализует class BazPrivate и использует MyClass<BazPrivate>

      Нет никакого способа, которым это могло бы работать, если мы или

      1. Придется перекомпилировать foo.cpp каждый раз, когда мы меняемся любой другой файл в программе, в случае, если он добавил новый роман создания MyClass<T>
      2. Требуют, чтобы baz.cpp содержит (возможно, через заголовок включает) полный шаблон MyClass<T> , так что компилятор может генерировать MyClass<BazPrivate> во время компиляции baz.cpp.

      Никто не любит (1), потому что системы компиляции анализа всей программы занимают навсегда компилировать, и потому что это делает невозможным распространение скомпилированных библиотек без исходного кода. Таким образом, мы имеем (2) вместо этого.

      Шаблоны должны быть инстанцирован компилятором, прежде чем фактически скомпилировать их в объектный код. Эта реализация может быть достигнута только в том случае, если известны аргументы шаблона. Теперь представьте сценарий, в котором функция шаблона объявлена ​​в a.h , определенный в a.cpp и используется в b.cpp , когда a.cpp компилируется, не обязательно известно, что предстоящая компиляция b.cpp потребует экземпляр шаблона, не говоря уже о том, какой конкретный экземпляр будет. Для большего количества заголовочных и исходных файлов ситуация может быстро усложниться.

      Можно утверждать, что компиляторы могут быть умнее, чтобы «смотреть в будущее» для всех применений шаблона, но я уверен, что не будет трудно создавать рекурсивные или иные сложные сценарии. AFAIK, компиляторы не делают такой взгляд вперед. Как указал Антон, некоторые компиляторы поддерживают явные объявления экспорта экземпляров шаблона, но не все компиляторы поддерживают его (пока?).

      На самом деле, до C ++ 11 стандарт определял export Ключевое слово, которое было бы сделать возможным объявить шаблоны в заголовочном файле и внедрить их в другом месте.

      В результате комитет по стандарту ISO C ++ решил удалить export особенность шаблонов с C ++ 11.

      Хотя в стандарте C ++ такого требования нет, некоторые компиляторы требуют, чтобы все шаблоны функций и классов были доступны в каждом используемом модуле перевода. По сути, для этих компиляторов тела шаблонных функций должны быть доступны в заголовочном файле. Повторим: это означает, что эти компиляторы не позволят им быть определены в файлах без заголовка, таких как файлы .cpp

      Есть экспорт ключевое слово, которое должно смягчить эту проблему, но оно далеко не портативное.

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

      Была особенность с export Ключевое слово, которое должно было использоваться для отдельной компиляции.
      export функция устарела в C++11 и, AFAIK, только один компилятор реализовал это. Вы не должны использовать export , Отдельная компиляция невозможна в C++ или же C++11 но возможно в C++17 , если концепции делают это, у нас мог бы быть некоторый способ отдельной компиляции.

      Для достижения отдельной компиляции должна быть возможна отдельная проверка тела шаблона. Кажется, что решение возможно с концепциями. Взгляните на это бумага недавно представлен на
      заседание комитета по стандартам. Я думаю, что это не единственное требование, так как вам все еще нужно создать экземпляр кода для шаблона в пользовательском коде.

      Отдельная проблема компиляции для шаблонов, я думаю, это также проблема, возникающая при переходе на модули, которая в настоящее время работает.

      Это означает, что наиболее переносимым способом определения реализаций методов шаблонных классов является определение их внутри определения класса шаблона.

      На предыдущих уроках мы узнали, как с помощью шаблонов функций сделать одну версию функции, которая будет работать с разными типами данных. Хотя это значительный шаг на пути к обобщенному программированию, это не решает всех наших проблем. Рассмотрим пример такой проблемы и то, как шаблоны могут нам помочь в её решении.

      Шаблоны и контейнерные классы

      На уроке о контейнерных классах мы узнали то, как, используя композицию, реализовать классы, содержащие несколько объектов определенного типа данных. В качестве примера мы использовали класс ArrayInt:

      // Присваиваем значение nullptr для m_data, чтобы на выходе не получить висячий указатель!

      Хотя этот класс обеспечивает простой способ создания массива целочисленных значений, но что, если нам нужно будет работать со значениями типа double? Используя традиционные методы программирования мы создали бы новый класс ArrayDouble для работы со значениями типа double:

      // Присваиваем значение nullptr для m_data, чтобы на выходе не получить висячий указатель!

      Хотя кода много, но классы почти идентичны, меняется только тип данных! Как вы уже могли бы догадаться, это идеальный случай для использования шаблонов. Создание шаблона класса аналогично созданию шаблона функции. Например, создадим шаблон класса Array:

      template < class T > // это шаблон класса с T вместо фактического (передаваемого) типа данных // Присваиваем значение nullptr для m_data, чтобы на выходе не получить висячий указатель! // Длина массива всегда является целочисленным значением, она не зависит от типа элементов массива int getLength ( ) ; // определяем метод и шаблон метода getLength() ниже template < typename T > // метод, определенный вне тела класса, нуждается в собственном определении шаблона метода int Array < T > :: getLength ( ) < return m_length ; >// обратите внимание, имя класса - Array<T>, а не просто Array

      Как вы можете видеть, эта версия почти идентична версии ArrayInt, за исключением того, что мы добавили объявление параметра шаблона класса и изменили тип данных c int на T .

      Обратите внимание, мы определили функцию getLength() вне тела класса. Это необязательно, но новички обычно спотыкаются на этом из-за синтаксиса. Каждый метод шаблона класса, объявленный вне тела класса, нуждается в собственном объявлении шаблона. Также обратите внимание, что имя шаблона класса — Array<T> , а не Array (Array будет указывать на не шаблонную версию класса Array).

      Вот пример использования шаблона класса Array:

      for ( int count = 0 ; count < intArray . getLength ( ) ; ++ count ) for ( int count = intArray . getLength ( ) - 1 ; count >= 0 ; -- count ) std :: cout << intArray [ count ] << "\t" << doubleArray [ count ] << '\n' ;

      9 9.5
      8 8.5
      7 7.5
      6 6.5
      5 5.5
      4 4.5
      3 3.5
      2 2.5
      1 1.5
      0 0.5

      Шаблоны классов работают точно так же, как и шаблоны функций: компилятор копирует шаблон класса, заменяя типы параметров шаблона класса на фактические (передаваемые) типы данных, а затем компилирует эту копию. Если у вас есть шаблон класса, но вы его не используете, то компилятор не будет его даже компилировать.

      Шаблоны классов в Стандартной библиотеке С++

      Теперь вы уже поняли, чем на самом деле является std::vector<int>? Правильно, std::vector — это шаблон класса, а int — это всего лишь передаваемый тип данных! Стандартная библиотека С++ полна предопределенных шаблонов классов, доступных для вашего использования.

      Шаблоны классов и Заголовочные файлы

      Работая с обычными классами мы помещаем определение класса в заголовочный файл, а определения методов этого класса в отдельный файл .cpp с аналогичным именем. Таким образом, фактическое определение класса компилируется как отдельный файл внутри проекта. Однако с шаблонами всё происходит несколько иначе (о том, почему следует помещать определение методов класса в отдельный файл, читайте в материалах урока №122). Рассмотрим следующее:

      // Присваиваем значение nullptr для m_data, чтобы на выходе не получить висячий указатель! // Длина массива всегда является целочисленным значением, она не зависит от типа элементов массива for ( int count = 0 ; count < intArray . getLength ( ) ; ++ count ) for ( int count = intArray . getLength ( ) - 1 ; count >= 0 ; -- count ) std :: cout << intArray [ count ] << "\t" << doubleArray [ count ] << '\n' ;

      Вышеприведенная программа скомпилируется, но вызовет следующую ошибку линкера:

      unresolved external symbol "public: int __thiscall Array::getLength(void)" (?GetLength@?$Array@H@@QAEHXZ)

      Почему так? Сейчас разберемся.

      Для использования шаблона компилятор должен видеть как определение шаблона (а не только объявление), так и тип шаблона, применяемый для создания экземпляра шаблона. Помним, что язык C++ компилирует файлы по отдельности. Когда заголовочный файл Array.h подключается в main.cpp, то определение шаблона класса копируется в этот файл. В main.cpp компилятор видит, что нам нужны два экземпляра шаблона класса: Array<int> и Array<double> , он создаст их, а затем скомпилирует весь этот код как часть файла main.cpp. Однако, когда дело дойдет до компиляции Array.cpp (отдельным файлом), компилятор забудет, что мы использовали Array<int> и Array<double> в main.cpp и не создаст экземпляр шаблона функции getLength(), который нам нужен для выполнения программы. Мы получим ошибку линкера, так как компилятор не сможет найти определение Array<int>::getLength() или Array<double>::getLength() .

      Эту проблему можно решить несколькими способами.

      Определение шаблона класса хранится в заголовочном файле.

      Определения методов шаблона класса хранятся в отдельном файле .cpp.

      Затем добавляем третий файл, который содержит все необходимые нам экземпляры шаблона класса.

      Давайте представим себе, что нам нужно написать набор функций, которые отличаются друг от друга лишь парой ключевых слов (и, как правило, одно из них — название типа). Ну, вот, например, взгляните на функции, рассчитывающие суммы элементов массивов для разных типов (упрощения ради, проверки указателей на неравенство нулю опущены /*упрощения ради также не рассматривается возможность переполнения для int — прим. пер.*/)


      Ну согласитесь же, насколько бы было лучше описать тело функции один раз, указав названия принимаемого (и возвращаемого) типа в виде «параметра», а потом определить экземпляры функции для конкретных типов? И эти функции еще относительно просты, а представьте себе, если бы они были длиннее, а их набор — больше.

      Вот именно для этого случая в C++ существует ключевое слово template. Но увы, не в чистом С.

      Сейчас вы увидите, что иногда это ключевое слово можно эмулировать средствами старого доброго препроцессора языка С.

      Шаблоны в С.

      Нам понадобятся некоторые ингридиенты.

      1: Заготовки

      Для начала, определим пару макросов. Они будут располагаться в отдельном заголовочном файле, и этот файл нам еще понадобится. Для ясности, назовем этот отдельный заголовочный файл «templates.h»

      Макрос Template нам понадобится в дальнейшем чтобы объединять макроопределения X и Y в виде X_Y, таким образом, чтобы написав TEMPLATE(function,type) мы получили бы в этом месте function_type.

      2: Готовим
      3. Сервируем.

      Не помню, сколько строк мы до этого написали, но для резюме вы смело можете умножить их число на 3. Или 4?

      Ну, и Хииииииииидер!


      Теперь должно быть понятно, почему мы не защищали sum_as_template.h от множественных включений: мы включаем его по разу на каждый задействованный тип.

      4. Подаем

      Ну, собственно, и все. Можно вызывать:

      Пытливый читатель спросит переводчика, а что если мне нужен тип «unsigned long long»? Ведь у нас получится функция «void sum_unsigned long long()»? К счастью для переводчика, автор предусмотрел и это. Используйте typedef:

      (Это довольно-таки вольный перевод. Свою статью писать уже лень, раз гугл знает ответ на вопрос function template in plain c, и к той статье мне решительно нечего добавить, но раз на хабре и вообще в русскоязычном секторе ответ гуглом не находится, чтобы добру не пропадать, опубликую пост-мортем)

      — 8< — [здесь закончился оригинальный пост] —

      UPD: Хочу сказать огромное спасибо хабраюзеру GRAFIN99 за как минимум 4 выявленные ошибки в исходниках, причем три из них — на глаз, просто в ходе прочтения статьи.

      Шаблоны служат основанием для универсального программирования на C++. В качестве строго типизированного языка C++ требует, чтобы все переменные имели конкретный тип, либо явно объявленный программистом, либо выведенный компилятором. Однако многие структуры данных и алгоритмы выглядят одинаково независимо от типа, на котором они работают. Шаблоны позволяют определить операции класса или функции и предоставить пользователю указание конкретных типов, с которыми должны работать эти операции.

      Определение и использование шаблонов

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

      Приведенный выше код описывает шаблон для универсальной функции с одним параметром типа T, возвращаемое значение и параметры вызова (LHS и RHS) всех этих типов. Вы можете присвоить параметру типа любое имя, но, по правилам, наиболее часто используются буквы в одном верхнем регистре. T является параметром шаблона; typename ключевое слово говорит о том, что этот параметр является заполнителем для типа. При вызове функции компилятор заменит каждый экземпляр T с конкретным аргументом типа, который либо задается пользователем, либо выведенным компилятором. Процесс, в котором компилятор создает класс или функцию из шаблона, называется созданием экземпляра шаблона. minimum<int> — это экземпляр шаблона minimum<T> .

      В других случаях пользователь может объявить экземпляр шаблона, специализированный для типа int. Предположим, что get_a () и get_b () — это функции, возвращающие int:

      Однако, поскольку это шаблон функции, и компилятор может вывести тип T из аргументов a и b, можно вызвать его так же, как обычная функция:

      Когда компилятор встречает последнюю инструкцию, он создает новую функцию, в которой каждое вхождение T в шаблоне заменяется на int :

      Правила, определяющие, как компилятор выполняет выведение типов в шаблонах функций, основаны на правилах для обычных функций. Дополнительные сведения см. в разделе разрешение перегрузки вызовов шаблона функции.

      Параметры типа

      В minimum приведенном выше шаблоне Обратите внимание на то, что параметр типа T не определен каким-либо образом, пока он не будет использован в параметрах вызова функции, где добавляются квалификаторы Const и Reference.

      Практически не существует ограничения на количество параметров типа. Несколько параметров разделяются запятыми:

      Ключевое слово class эквивалентно typename в данном контексте. Предыдущий пример можно выразить следующим образом:

      Оператор с многоточием (. ) можно использовать для определения шаблона, принимающего произвольное число из нуля или более параметров типа:

      В качестве аргумента типа можно использовать любой встроенный или определяемый пользователем тип. Например, можно использовать std:: Vector в стандартной библиотеке для хранения переменных типа int , double , std:: String, MyClass , const MyClass *, MyClass& и т. д. Основным ограничением при использовании шаблонов является то, что аргумент типа должен поддерживать любые операции, применяемые к параметрам типа. Например, если мы вызываем метод minimum using MyClass , как в следующем примере:

      Будет создана ошибка компилятора, так как не MyClass предоставляет перегрузку для < оператора.

      Нет необходимости в том, что аргументы типа для любого конкретного шаблона принадлежат к одной и той же иерархии объектов, хотя можно определить шаблон, обеспечивающий такое ограничение. Объектно-ориентированные методики можно сочетать с шаблонами. Например, можно сохранить производный * в векторе <Base*> . Обратите внимание, что аргументы должны быть указателями

      Основные требования, которые std::vector и другие контейнеры стандартной библиотеки, накладываются на элементы T , являются T копируемыми и конструируемым.

      Параметры, не являющиеся типами

      Обратите внимание на синтаксис в объявлении шаблона. size_t Значение передается в качестве аргумента шаблона во время компиляции и должно быть const или constexpr выражением. Используйте его следующим образом:

      Другие виды значений, включая указатели и ссылки, могут передаваться как параметры, не являющиеся типами. Например, можно передать указатель на функцию или объект функции для настройки некоторой операции внутри кода шаблона.

      Выведение типа для параметров шаблона, не являющихся типами

      в Visual Studio 2017 и более поздних версиях, в режиме /std: c++ 17 , компилятор выводит тип аргумента шаблона, не являющегося типом, который объявлен с помощью auto :

      Шаблоны в качестве параметров шаблона

      Шаблон может быть параметром шаблона. В этом примере MyClass2 имеет два параметра шаблона: параметр typeName T и параметр шаблона arr:

      Поскольку сам параметр arr не имеет тела, его имена параметров не требуются. На самом деле, является ошибкой ссылаться на имена TypeName или параметров класса arr в теле MyClass2 . По этой причине имена параметров типа arr можно опустить, как показано в следующем примере:

      Аргументы шаблона по умолчанию

      Шаблоны классов и функций могут иметь аргументы по умолчанию. Если шаблон имеет аргумент по умолчанию, его можно оставить неопределенным при его использовании. Например, шаблон std:: Vector имеет аргумент по умолчанию для распределителя:

      В большинстве случаев класс по умолчанию std:: распределитель приемлем, поэтому вы используете такой же вектор:

      Но при необходимости можно указать пользовательский распределитель следующим образом:

      При наличии нескольких аргументов шаблона все аргументы после первого аргумента по умолчанию должны иметь аргументы по умолчанию.

      При использовании шаблона, параметры которого заданы по умолчанию, используйте пустые угловые скобки:

      Специализация шаблонов

      В некоторых случаях невозможно или нежелательно, чтобы шаблон определял точно такой же код для любого типа. Например, может потребоваться определить путь кода, который будет выполняться, только если аргумент типа является указателем или std:: wstring или типом, производным от конкретного базового класса. В таких случаях можно определить специализацию шаблона для этого конкретного типа. Когда пользователь создает экземпляр шаблона с этим типом, компилятор использует специализацию для создания класса, а для всех остальных типов компилятор выбирает более общий шаблон. Специализации, в которых все параметры являются специализированными, являются полными специализациями. Если только некоторые из параметров являются специализированными, это называется частичной специализацией.

      Шаблон может иметь любое количество специализаций, если каждый специализированный параметр типа уникален. Только шаблоны классов могут быть частично специализированными. Все полные и частичные специализации шаблона должны быть объявлены в том же пространстве имен, что и исходный шаблон.

      Читайте также: