Код с занятия L05. Исходник лежит в базе как
30-Код/C++/L05 - main.cpp и компилируется как есть. На сайт он не
выкладывается, поэтому все нужные фрагменты приведены здесь.
Что демонстрирует
Темы, которых нет в лонгриде: возврат ссылки из функции, статические переменные, Перегрузка функций и шаблоны функций. Теория записана прямо в коде многострочными комментариями.
Это код с занятия, а не эталон
g++ -std=c++20 -Wall -Wextraсобирает файл с двумя предупреждениями. Два комментария в нём неточны: проint → longи про препроцессор. Ниже эти места отмечены. Исходник в базе не правлен.
Полный вывод программы (GCC 16.2):
1
2
3
11
Hello, world!
2 1 3
3 2 4
4 3 5
1 2
2 1
PrintValue(double x): value=0
PrintValue(int x): value=0
PrintValue(long x, long y): x=0 y=0
2 1
1 2
x=0 y=0
x=1 y=0Разбор
F: на что можно вернуть ссылку
int& F() {
// на что можно вернуть ссылку / указатель?
// - на объект в динамической памяти
// - на глобальную переменную (любой объект со статической областью действия (lifetime))
// - на переменную, которая была передана в функцию как параметр по ссылке
int x = 0;
return x;
}Комментарий перечисляет допустимые варианты, а сама функция показывает
недопустимый: x — локальная переменная, она умирает в конце вызова, и
вызывающий код получает висячую ссылку (см. Висячий указатель). GCC это
видит:
main.cpp:9:10: warning: reference to local variable 'x' returned [-Wreturn-local-addr]В main функция F не вызывается, так что UB в программе нет. Это
антипример на словах.
G: статическая переменная и возврат ссылки
int& G() {
static int x; // = 0
return ++x;
}
std::cout << G() << '\n'; // 1
std::cout << G() << '\n'; // 2
std::cout << G() << '\n'; // 3
G() = 10;
std::cout << G() << '\n'; // 11static int xживёт всю программу и обнуляется один раз (см. Область действия). Поэтому вернуть на неё ссылку можно — второй пункт списка изF.++x— префиксный инкремент, его результат — lvalue, самx. Поэтомуreturn ++x;в функции с типомint&возвращает ссылку наx, а не на временное значение.G() = 10;вызываетG(теперьx == 4), а затем через возвращённую ссылку записывает вx10. Следующий вызов — 11.
H: static во вложенном блоке
void H(int x) {
std::cout << ++x << ' ';
{
static int y;
int z = x;
std::cout << ++y << ' ' << ++z << '\n';
}
}
H(1); // 2 1 3
H(2); // 3 2 4
H(3); // 4 3 5Три переменные — три поведения:
x— параметр по значению, создаётся при каждом вызове из аргумента;z— автоматическая переменная блока, создаётся заново каждый раз;y—static: одна на все вызовы, поэтому 1, 2, 3.
Средняя колонка вывода растёт на 1 сама по себе, крайние зависят от аргумента.
Swap: перегрузка по типу и количеству
void Swap(int& lhs, int& rhs) { /* обмен int */ }
void Swap(float& lhs, float& rhs) { /* обмен float */ }
void Swap(float& x) {}
float z = 1, v = 2;
std::cout << z << ' ' << v << '\n'; // 1 2
Swap(z, v);
Swap(v);
Swap(z);
std::cout << z << ' ' << v << '\n'; // 2 1Swap(z, v) выбирает версию для float. Swap(v) и Swap(z) вызывают
однопараметрическую Swap(float&). Её тело пустое, поэтому значения после
обмена не меняются. Сделано это ради перегрузки по количеству аргументов,
отсюда и второе предупреждение компилятора:
main.cpp:49:18: warning: unused parameter 'x' [-Wunused-parameter]PrintValue: выбор перегрузки
void PrintValue(double x);
void PrintValue(int x);
void PrintValue(long x);
void PrintValue(long x, long y);
float y = 0;
PrintValue(y); // PrintValue(double x): value=0
PrintValue(x); // PrintValue(int x): value=0
PrintValue(y, y); // PrintValue(long x, long y): x=0 y=0Комментарий в коде:
/*
при вызове функции с наличием перегрузок выбирается наиболее подходящая по параметрам версия,
требующая наименьшего числа преобразований (conversion)
расширение (promotion) - не пребразование (int -> long, float -> double)
однако float -> long double, double -> long double - преобразования
при выборе перегрузки константность ПАРАМЕТРА игнорируется
const int* - констнатность на объекте, на который указываем - не игнорируется
int* const - константность на параметре - игрорируется
*/PrintValue(y):float → double— расширение, лучше, чем преобразования вint/long. Комментарий у вызова верно говорит, что без версии сdoubleвыбор был бы неоднозначным.PrintValue(x): точное совпадение сint.PrintValue(y, y): единственная версия с двумя параметрами, обаfloatпреобразуются вlong.
int -> long— не расширениеЦелочисленное расширение (integral promotion) — это только малые типы (
bool,char,short) вint.int → long— обычное преобразование. Проверка: с перегрузкамиP(long)иP(double)вызовP(i)дляint iне компилируется —call of overloaded 'P(int&)' is ambiguous. Если быint → longбыло расширением, выбрался быlong. См. Арифметические преобразования типов.
Про константность проверено отдельно: void H(int) и void H(const int) с
телами дают redefinition, а void G(int*) и void G(const int*)
уживаются как две перегрузки.
Шаблон Swap
template <typename T>
void Swap(T& lhs, T& rhs) {
T tmp = lhs;
lhs = rhs;
rhs = tmp;
}
double lhs = 2, rhs = 1;
Swap(lhs, rhs); // 1 2Для double обычной Swap нет, поэтому шаблон выводит T = double. А вот
Swap(z, v) выше с float пошёл в обычную функцию, хотя шаблон к тому
моменту тоже объявлен. При одинаково хорошем совпадении нешаблонная
функция побеждает (проверено: в тестовом файле с печатью из обеих версий
float идёт в обычную, double — в шаблон).
Шаблоны — не препроцессор
Комментарий «шаблоны на этапе препроцессинга превращаются в перегрузки функций» неточен по этапу. Препроцессор шаблон не трогает: в
g++ -E main.cppстрокаtemplate <typename T>остаётся. Конкретная функция из шаблона появляется при компиляции (инстанцирование). См. Компиляция и сборка.
PrintValues: вывод типов
/*
типы выводятся для аргументов независимо
выведение по значению:
1. отбрасывается const и volatile (что это?)
2. ссылочность аргумента теряется
3. массив низводится до указателя на первый элемент
4. функция низводится до указателя на функцию
*/
template <typename T, typename U>
void PrintValues(T x, U y) {
std::cout << "x=" << x << ' ' << "y=" << y << '\n';
}
int& rx = x;
PrintValues(rx, 0.0); // x=0 y=0
PrintValues<float>(1, 0.0); // x=1 y=0PrintValues(rx, 0.0):rx— ссылка, но по правилу 2 ссылочность теряется,T = int;U = double.PrintValues<float>(1, 0.0):Tзадан явно,1преобразуется воfloat;Uвыводится из0.0какdouble. Печатаетсяx=1—float1.0 выводится без дробной части.
Что будет, если сделать иначе
- Вызвать
F()и прочитать результат — UB: ссылка на уничтоженный объект. - Убрать
staticвG—xстанет локальной неинициализированной переменной:++xчитает мусор (UB), и вдобавок функция вернёт висячую ссылку, какF. - Убрать
staticвH—yперестанет копиться и станет неинициализированной: средняя колонка — UB вместо 1, 2, 3. - Удалить
PrintValue(double)—PrintValue(y)перестанет компилироваться из-за неоднозначностиint/long. - Удалить
PrintValue(int)—PrintValue(x)тоже станет неоднозначным:int → longиint → double— оба преобразования. - Вызвать
Swap(z, lhs)сfloatиdouble— шаблон не подойдёт (deduced conflicting types for parameter 'T'), а обычные версии не принимаютdouble&дляfloat&: ошибка компиляции.
Не из лекции
Все пункты, кроме первого, — следствия, а не материал занятия. Удаление
PrintValue(double)и смешанныйSwapпроверены на GCC 16.2.
Связанные термины
По тексту разбора: Ссылка, Функция, Передача параметров.