Код с занятия 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';  // 11
  • static int x живёт всю программу и обнуляется один раз (см. Область действия). Поэтому вернуть на неё ссылку можно — второй пункт списка из F.
  • ++x — префиксный инкремент, его результат — lvalue, сам x. Поэтому return ++x; в функции с типом int& возвращает ссылку на x, а не на временное значение.
  • G() = 10; вызывает G (теперь x == 4), а затем через возвращённую ссылку записывает в x 10. Следующий вызов — 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 1

Swap(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=0
  • PrintValues(rx, 0.0): rx — ссылка, но по правилу 2 ссылочность теряется, T = int; U = double.
  • PrintValues<float>(1, 0.0): T задан явно, 1 преобразуется во float; U выводится из 0.0 как double. Печатается x=1 — float 1.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.

Связанные термины

По тексту разбора: Ссылка, Функция, Передача параметров.