8.x – Резюме к главе 8 и небольшой тест

Добавлено 27 июня 2021 в 17:45

Вы сделали это! Темы этой главы (особенно псевдонимы типов, перегруженные функции и шаблоны функций) встречаются в стандартной библиотеке 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;
}

Теги

autoC++ / CppLearnCppВывод типа / Type inferenceДля начинающихНеявное преобразование типаОбучениеПрограммированиеПсевдоним типа / Type aliasРасширяющее преобразование типаСужающее преобразование типаШаблон / TemplateШаблон функцииЯвное преобразование типа

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.