C ++ - это строго типизированный язык. Мы должны объявить каждую переменную, параметр функции и возвращаемое значение функции с использованием определенного типа. Если есть какая-либо несовместимость между ожидаемым типом и фактически используемым типом, он не будет компилироваться. Это дает преимущество, заключающееся в том, что сам язык обеспечивает «безопасность типов», чтобы разработчики не допускали ошибок при назначении неправильных объектов, параметров неправильным функциям, операторам. Кроме того, эта проверка выполняется статически во время компиляции, поэтому во время выполнения не требуется дополнительных затрат на производительность.

Однако этот строго типизированный аспект также делает C ++ менее гибким. Допустим, вы хотите предоставить функцию как библиотеку, и функция принимает два входных параметра, вычисляет сумму и возвращает ее. Поскольку вы не можете предсказать конкретный тип, который нужен пользователям библиотеки в их контексте, вы не можете реализовать функцию, совместимую с возможно необходимыми типами. Кроме того, даже если вы точно знаете, какие типы ваша библиотека должна поддерживать, вам необходимо подготовить несколько функций, которые принимают разные типы (int, void, string, custom class и т. Д.). В большинстве случаев функция имеет общее поведение, которое применяется ко всем типам. В результате повторов тоже будет много.

Чтобы решить эту проблему, язык программирования C ++ предлагает мощную функцию под названием Templates¹. Используя шаблоны, мы можем реализовать универсальную функцию или класс, который можно использовать для многих различных типов. Фактически, шаблоны очень популярны для разработки библиотек, и многие стандартные библиотеки C ++ используют шаблоны внутри.

В этом посте я расскажу о некоторых подводных камнях и сюрпризах, с которыми вы можете столкнуться при использовании шаблонов C ++.

Простой пример

Определение и использование функции простого шаблона выглядит так, как показано ниже,

template <class T>
void function(T arg) {
// do something with arg
}
int main() {
  int a = 1;
  function(a);  
}
# # # Instantiated function # # #
# void function(int arg) {
#  // do something with arg
# }

Когда function (a) читается компилятором, компилятор определяет тип T из a , который является переменной, переданной в функцию. В этом случае T выводится как int. В результате Создается экземпляр функции void (int arg).

Результат удержания совсем не удивителен. Теперь давайте посмотрим на следующий случай ниже.

template <class T>
void function(T arg) {
// do something with arg
}
int main() {
  int a = 1; 
  const int & ref_to_const_int = a;
  function(ref_to_const_int);
}
# # # Instantiated function # # #
# void function(int arg) {
#  // do something with arg
# }

В этом случае переменная ref_to_const_int была передана в функцию . ref_to_const_int - это ссылка на const int (то есть const int &). Учитывая результат первого примера, естественно предположить, что T будет выведено как const int &, что совпадает с ref_to_const_int. Однако T будет выводиться как «int», не «const int &». Таким образом, созданная функция выглядит точно так же, как и раньше: void function (int arg).

Автоматический тип «распад»

Этот сюрприз произошел из-за того, что компилятор выполнил определенное преобразование типа, который был передан при создании экземпляра функции. Когда мы передали параметр const int & в function, при выводе типа T компилятор удалил «const» и «&» из исходного параметра и выводит T как «int». Это автоматическое преобразование типов компилятором называется «распадом». Это правило применяется только тогда, когда вы объявляете параметры функции как передаваемые по значению. В нашем случае объявление нашей функции принимает аргумент, который передается по значению, как показано ниже, поэтому правило распада было применено компилятором.

template <class T>
void function(T arg) {  // pass-by-value         
// do something with arg
}

Характер распада и его условия:

при объявлении параметров функции как передаваемых по значению,

  1. Удаляются все ссылки верхнего уровня: int & - ›int.
  2. типы массивов преобразуются в типы указателей: char [10] - ›char *.
  3. функции преобразуются в типы указателей на функции: int (int) - ›int (*) (int).
  4. (Если 2, 3 неприменимы) Все квалификаторы CV верхнего уровня² (const, volatile) удаляются: const int - › int, volatile int - ›int.

В приведенном выше случае применялись правила 1 и 4. Следовательно, const int & был преобразован в int компилятором во время создания экземпляров шаблонов.

Давайте посмотрим на другой пример схемы распада. На этот раз мы сосредоточимся на схеме распада «тип массива в тип указателя»,

template <class T>
void function(T arg) {  // pass-by-value
// do something with arg
}
int main() {
  const char const_array[7] = "Hello!"; 
  function(str);
}
# # # Instantiated function # # #
# void function(const char* arg) {  // Not const char[7]
#  // do something with arg
# }

В этом случае const_array, имеющий тип const char [7], был передан в function. Поскольку не был включен эталонный квалификатор, образец распада 1 не применялся. Затем, поскольку это тип массива, был применен шаблон распада 2, который преобразовал const char [7] в const char *. И поскольку это не функция, шаблон распада 3 был пропущен. Наконец, поскольку была применима схема распада 2, модель распада 4 также была пропущена. В результате компилятор определил тип T как const char *. Таким образом, экземпляр функции был void function (const char * arg).

Предотвратить автоматический распад типа

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

template <class T>
void function(T& arg) {  // pass-by-reference
// do something with arg
}
int main() {
  int a = 1; 
  const int & ref_to_const_int = a;
  function(ref_to_const_int);
}
# # # Instantiated function # # #
# void function(const int& arg) {
#  // do something with arg
# }

Поскольку параметр функции был объявлен как T & (передача по ссылке), шаблоны распада не будут применяться (помните, что распад происходит только тогда, когда функция объявляет свои параметры как передаваемые по значению). Выведенный тип T - const int. Следовательно, компилятор создает экземпляр функции void function (const int & arg).

Это также работает для предотвращения других паттернов распада. Вы можете увидеть ниже пример типа массива.

template <class T>
void function(T& arg) {  // pass-by-reference
// do something with arg
}
int main() {
  const char const_char_array[7] = "Hello!"; 
  function(const_char_array);
}
# # # Instantiated function # # #
# void function(const char(&)[7] arg) {
#  // do something with arg
# }

Выведенный тип T - const char [7], не const char *. Следовательно, компилятор создает экземпляр функции void function (const char (&) [7] arg).

Хотя этот тип распада кажется негативным, которого следует избегать, это может быть желаемое поведение, которое также зависит от ситуации. Например, предположим, что вы хотите реализовать функцию, которая принимает 2 входа одного и того же типа, упаковывает их как std :: pair ‹T, T› и возвращает ее копию. Эти шаблоны выглядят так, как показано ниже. Как видите, для этого требуется, чтобы 2 входа имели одинаковые типы T.

template <class T>
std::pair<T, T> make_pair_function(T arg1, T arg2) {
  return std::pair<T, T>(arg1, arg2);
}

Допустим, вам нравится передавать два массива в make_pair_function. Если вы объявляете параметры функции как передаваемые по значению, даже если вы передаете два массива разной длины (следовательно, они разных типов) , благодаря автоматическому распаду компилятором оба типа выводятся как const char *. Вот почему это верно и компилируется без ошибок.

template <class T>
std::pair<T, T> make_pair_function(T arg1, T arg2) {
  return std::pair<T, T>(arg1, arg2);
}
int main() {
  const char const_array1[7] = "Hello!";
  const char const_array2[8] = "World!!";
make_pair_function(const_array1, const_array2);
}
# # # Instantiated function # # #
#  std::pair<const char*, const char*>
#   function(const char* arg1, const char* arg2) {
#  return std::pair<const char*, const char*>(a, b);
# }

Однако, если вы объявите параметры функции как передаваемые по ссылке и передадите два массива в make_pair_function, он не будет компилироваться. Это связано с тем, что компилятор выводит типы arg1 и arg2 как разные типы (const char [7], const char [8]), а шаблона make_pair_function, который может принимать два разных типа в качестве входных параметров, или нет не-шаблона make_pair_function, который может принимать ( const char [7], const char [8]).

Поэтому автоматическое распад типа может быть полезной функцией в зависимости от ситуации. На самом деле, в стандартной библиотеке определена служебная функция под названием std :: decay³, которая намеренно заставляет компилятор изменять поведение типа. Важно знать, что происходит под выводом типа шаблона, и при необходимости адаптироваться.

Резюме

В этом посте я объяснил подводные камни вывода типов шаблонов в C ++. Я объяснил концепцию автоматического преобразования распада типов компилятором и способы предотвращения этого.

[1]: шаблоны, https://en.cppreference.com/w/cpp/language/templates

[2]: cv-qualifiers, https://en.cppreference.com/w/cpp/language/cv

[3]: std :: decay, https://en.cppreference.com/w/cpp/types/decay