8.x – Резюме к главе 8 и небольшой тест
Вы сделали это! Темы этой главы (особенно псевдонимы типов, перегруженные функции и шаблоны функций) встречаются в стандартной библиотеке C++ повсюду. У нас есть еще одна глава (введение в составные типы), а затем мы будем готовы углубиться в некоторые из наиболее полезных частей стандартной библиотеки!
Краткое резюме
Процесс преобразования значения из одного типа данных в другой тип данных называется преобразованием типа.
Неявное преобразование типа (также называемое автоматическим преобразованием типа или принуждением) выполняется всякий раз, когда ожидается один тип данных, но предоставляется другой тип данных. Если компилятор может выяснить, как выполнить преобразование между двумя этими типами, он это сделает. Если он не знает, как это сделать, он завершится ошибкой компиляции.
Язык C++ определяет ряд встроенных преобразований между его базовыми типами (а также несколько преобразований для более сложных типов), называемых стандартными преобразованиями. К ним относятся числовые продвижения (расширяющие преобразования), числовые преобразования и арифметические преобразования.
Числовое продвижение – это преобразование меньших числовых типов в более крупные числовые типы (обычно int
или double
), чтобы CPU мог работать с данными, которые соответствуют естественному размеру данных для процессора. Числовые продвижения включают в себя как целочисленные продвижения, так и продвижения с плавающей запятой. Числовые продвижения сохраняют значения, то есть при их выполнении нет потерь значения или точности.
Числовое преобразование – это преобразование типов между базовыми типами, которое не является числовым продвижением. Сужающее преобразование – это числовое преобразование, которое может привести к потере значения или точности.
В C++ некоторые бинарные операторы требуют, чтобы их операнды были одного типа. Если предоставлены операнды разных типов, один или оба операнда будут неявно преобразованы в соответствующие типы с использованием набора правил, называемых обычными арифметическими преобразованиями.
Явное преобразование типа выполняется, когда программист явно запрашивает преобразование через приведение типов. Приведение (cast) представляет собой запрос программиста на явное преобразование типа. C++ поддерживает 5 типов приведения типов: приведения в стиле C, статические приведения, константные приведения, динамические приведения и реинтерпретирующие приведения. Как правило, вам следует избегать приведений в стиле C, константных и реинтерпретирующих приведений. static_cast
используется для преобразования значения одного типа в значение другого типа и на сегодняшний день является наиболее часто используемым преобразованием в C++.
Определения typedef
и псевдонимы типов позволяют программисту создать псевдоним для типа данных. Эти псевдонимы не являются новыми типами и действуют идентично типам, на которые они ссылаются. Определения typedef
и псевдонимы типов не обеспечивают какой-либо безопасности типов, и необходимо соблюдать осторожность, чтобы не предполагать, что псевдоним отличается от типа, которому он присваивает псевдоним.
Ключевое слово auto
имеет несколько применений. Во-первых, auto
можно использовать для вывода типа, который будет выводить тип переменной из ее инициализатора. При выводе типов удаляются константность и ссылки, поэтому не забудьте добавить их обратно, если они вам нужны.
auto
также может использоваться как тип возвращаемого значения функции, чтобы компилятор определил тип возвращаемого значения функции из инструкций return
, хотя этого следует избегать для обычных функций. auto
также используется как часть синтаксиса, завершающегося типом возвращаемого значения.
Перегрузка функций позволяет нам создавать несколько функций с одним и тем же именем, при условии, что каждая функция с повторяющимся именем имеет отличающийся набор типов параметров (или функции могут различаться иным образом). Такая функция называется перегруженной функцией (или для краткости перегрузкой). Типы возвращаемых данных не учитываются для различения функций.
При разрешении перегруженных функций, если точное совпадение не найдено, компилятор будет отдавать предпочтение перегруженным функциям, которые могут быть сопоставлены с помощью числовых продвижений, а не тем, которые требуют числовых преобразований. Когда выполняется вызов функции для функции, которая была перегружена, компилятор попытается сопоставить вызов функции с соответствующей перегрузкой на основе аргументов, используемых в вызове функции. Это называется разрешением перегрузки.
Неоднозначное совпадение происходит, когда компилятор находит две или более функции, которые могут соответствовать вызову функции для перегруженной функции, и не может определить, какая из них лучше.
Аргумент по умолчанию – это значение по умолчанию, предоставленное для параметра функции. Параметры с аргументами по умолчанию всегда должны быть крайними правыми параметрами, и они не используются для различения функций при разрешении перегруженных функций.
Шаблоны функций позволяют нам создавать определение, подобное функции, которое служит шаблоном для создания связанных функций. В шаблоне функции мы используем шаблонные типы в качестве заполнителей для любых типов, которые мы хотим указать позже. Синтаксис, который сообщает компилятору, что мы определяем шаблон, и объявляет шаблонные типы, называется объявлением параметров шаблона.
Процесс создания функций (с определенными типами) из шаблонов функций (с шаблонными типами) называется созданием экземпляра шаблона функции (или, для краткости, созданием экземпляра). Когда этот процесс происходит из-за вызова функции, он называется неявным созданием экземпляра. Созданная функция называется экземпляром функции (или, для краткости, экземпляром или иногда шаблонной функцией).
Вывод аргументов шаблона позволяет компилятору вывести из аргументов вызова функции фактический тип, который должен использоваться для создания экземпляра функции. Вывод аргументов шаблона не выполняет преобразование типов.
Шаблонные типы иногда называют обобщенными (универсальными) типами, а программирование с использованием шаблонов иногда называют обобщенным программированием.
Когда ключевое слово auto
используется в качестве типа параметра в обычной функции, компилятор автоматически преобразует эту функцию в шаблон функции, при этом каждый параметр auto
становится независимым параметром шаблонного типа. Этот метод создания шаблона функции называется сокращенным шаблоном функции.
Небольшой тест
Вопрос 1
Какой тип преобразования происходит в каждом из следующих случаев? Допустимые ответы: преобразование не требуется, числовое продвижение, числовое преобразование, не будет компилироваться из-за сужающего преобразования. Предположим, что int
и long
равны 4 байтам.
int main()
{
int a{ 5 }; // 1a
int b{ 'a' }; // 1b
int c{ 5.4 }; // 1c
int d{ true }; // 1d
int e{ static_cast<int>(5.4) }; // 1e
double f { 5.0f }; // 1f
double g { 5 }; // 1g
// Немного посложнее
long h{ 5 }; // 1h
float i { f }; // 1i
float j { 5.0 }; // 1j
}
1a) Ответ
Преобразование не требуется
1b) Ответ
Целочисленное продвижение
char 'a'
вint
1c) Ответ
Не компилируется из-за сужающего преобразования
1d) Ответ
Числовое продвижение
bool true
вint
1e) Ответ
Явное числовое преобразование
double 5.4
вint
1f) Ответ
Числовое продвижение числа
float
вdouble
1g) Ответ
Числовое преобразование
int
вdouble
1h) Ответ
Числовое преобразование
int
вlong
(это преобразование тривиально, но это всё же преобразование)
1i) Ответ
Не компилируется из-за сужающего преобразования из
double
воfloat
1j) Ответ
Числовое преобразование из
double
воfloat
(оно разрешено, поскольку 5.0 являетсяconstexpr
и подходит для диапазона значенийfloat
)
Вопрос 2
2a) Обновите следующую программу, используя псевдонимы типов:
#include <iostream>
namespace Constants
{
inline constexpr double pi { 3.14159 };
}
double convertToRadians(double degrees)
{
return degrees * Constants::pi / 180;
}
int main()
{
std::cout << "Enter a number of degrees: ";
double degrees{};
std::cin >> degrees;
double radians { convertToRadians(degrees) };
std::cout << degrees << " degrees is " << radians << " radians.\n";
return 0;
}
Ответ
#include <iostream> namespace Constants { inline constexpr double pi{ 3.14159 }; } using degrees_t = double; using radians_t = double; radians_t convertToRadians(degrees_t degrees) { return degrees * Constants::pi / 180; } int main() { std::cout << "Enter a number of degrees: "; degrees_t degrees{}; std::cin >> degrees; radians_t radians{ convertToRadians(degrees) }; std::cout << degrees << " degrees is " << radians << " radians.\n"; return 0; }
2b) Основываясь на вопросе 2a, объясните, почему следующая инструкция будет или не будет компилироваться:
radians = degrees;
Ответ
Она будет компилироваться. И радианы, и градусы являются псевдонимами типа для
double
, так что это просто присвоение значенияdouble
переменной типаdouble
.
Вопрос 3
3a) Что выдает эта программа и почему?
void print(int x)
{
std::cout << "int " << x << '\n';
}
void print(double x)
{
std::cout << "double " << x << '\n';
}
int main()
{
short s { 5 };
print(s);
return 0;
}
Ответ
Результатом будет int 5. Преобразование
short
вint
– это числовое продвижение, тогда как преобразованиеshort
вdouble
– это числовое преобразование. Компилятор предпочтет вариант с числовым продвижением вместо варианта с числовым преобразованием.
3b) Почему следующий код не компилируется?
void print()
{
std::cout << "void\n";
}
void print(int x=0)
{
std::cout << "int " << x << '\n';
}
void print(double x)
{
std::cout << "double " << x << '\n';
}
int main()
{
print(5.0f);
print();
return 0;
}
Ответ
Поскольку параметры с аргументами по умолчанию не учитываются при разрешении перегруженных функций, компилятор не может определить, должен ли вызов
print()
разрешаться вprint()
илиprint(int x = 0)
.
3c) Почему следующий код не компилируется?
void print(long x)
{
std::cout << "long " << x << '\n';
}
void print(double x)
{
std::cout << "double " << x << '\n';
}
int main()
{
print(5);
return 0;
}
Ответ
Значение 5 – это число
int
. Преобразованиеint
вlong
илиdouble
– это числовое преобразование, и компилятор не сможет определить, какая функция подходит лучше.
Вопрос 4
Что выдает эта программа и почему?
#include <iostream>
template <typename T>
int count(T x)
{
static int c { 0 };
return ++c;
}
int main()
{
std::cout << count(1);
std::cout << count(1);
std::cout << count(2.3);
std::cout << count<double>(1);
return 0;
}
Ответ
1212
- Когда вызывается
count(1)
, компилятор создает экземпляр функцииcount<int>(int)
и вызывает ее. Это вернет 1.- Когда
count(1)
вызывается снова, компилятор увидит, чтоcount<int>(int)
уже существует, и вызовет ее снова. Это вернет 2.- Когда вызывается
count(2.3)
, компилятор создает экземпляр функции с прототипомcount<double>(double)
и вызывает ее. Это новая функция со своей собственной статической переменнойc
, поэтому она вернет 1.- Когда вызывается
count(1)
, компилятор увидит, что мы явно запрашиваем версиюdouble
дляcount()
. Эта функция уже существует из-за предыдущей инструкции, поэтому будет вызыватьсяcount<double>(double)
, и аргумент будет неявно преобразован доdouble
. Эта функция вернет 2.
Вопрос 5
5a) Напишите шаблон функции с именем add
, который позволяет пользователям складывать 2 значения одного типа. Следующая программа должна запуститься:
#include <iostream>
// напишите здесь свой шаблон функции add
int main()
{
std::cout << add(2, 3) << '\n';
std::cout << add(1.2, 3.4) << '\n';
return 0;
}
и выдать следующий результат:
5
4.6
Ответ
#include <iostream> template <typename T> T add(T x, T y) { return x + y; } int main() { std::cout << add(2, 3) << '\n'; std::cout << add(1.2, 3.4) << '\n'; return 0; }
5b) Напишите шаблон функции с именем mult
, который позволяет пользователю умножать одно значение любого типа на число int
. Следующая программа должна запуститься:
#include <iostream>
// напишите здесь свой шаблон функции mult
int main()
{
std::cout << mult(2, 3) << '\n';
std::cout << mult(1.2, 3) << '\n';
return 0;
}
и выдать следующий результат:
6
3.6
Ответ
#include <iostream> template <typename T> T mult(T x, int y) { return x * y; } int main() { std::cout << mult(2, 3) << '\n'; std::cout << mult(1.2, 3) << '\n'; return 0; }
5c) Напишите шаблон функции с именем sub
, который позволяет пользователю вычитать два значения разных типов. Следующая программа должна запуститься:
#include <iostream>
// напишите здесь свой шаблон функции sub
int main()
{
std::cout << sub(3, 2) << '\n';
std::cout << sub(3.5, 2) << '\n';
std::cout << sub(4, 1.5) << '\n';
return 0;
}
и выдать следующий результат:
1
1.5
2.5
Ответ
#include <iostream> template <typename T, typename U> auto sub(T x, U y) { return x - y; } int main() { std::cout << sub(3, 2) << '\n'; std::cout << sub(3.5, 2) << '\n'; std::cout << sub(4, 1.5) << '\n'; return 0; }