Функции, области видимости и выражения
Параметры, результат и вызов функции, локальные имена и простые выражения. Лямбды, ODR и категории значений — следующий уровень.
Создание и вызов функций
Функция объединяет действия, которым можно передать данные и от которых
можно получить результат. Сначала опиши, какие параметры ей нужны и что
она возвращает, затем запиши тело. Определение не запускает функцию:
действия выполняются при вызове, например add(2, 3).
Обычная функция в глобальной области
Общая форма определения:
тип_результата имя(параметры) { тело }
В записи
int add(int left, int right) функция называется
add, принимает два целых и возвращает целое. Каждый
параметр получает своё имя и тип. Вызов передаёт конкретные аргументы: в
add(2, 3) параметр left получает 2, а
right — 3.
#include <iostream>
int add(int left, int right) {
return left + right;
}
void print_result(int value) {
std::cout << "result: " << value << '\n';
}
int main() {
int total = add(2, 3);
print_result(total);
return 0;
}| Часть | Назначение | В примере |
|---|---|---|
Тип результата |
Какое значение получает вызывающий код.
void означает отсутствие возвращаемого
значения.
| int у add, void у
print_result.
|
Имя | Используется для обращения к функции. | add, print_result. |
Список параметров |
Какие данные нужны функции; параметры разделяются
запятыми.
| (int left, int right). |
Тело | Инструкции внутри фигурных скобок. | { return left + right; }. |
return |
Завершает текущий вызов и, если нужно, передаёт
результат вызывающему коду.
| return left + right;. |
Вызов | Запускает выполнение тела с указанными аргументами. | int total = add(2, 3);. |
add и print_result определены вне
main, то есть в глобальной области. В C++ нельзя определить
обычную именованную функцию прямо внутри тела другой функции. Перед
вызовом её объявление должно быть видно компилятору: можно поместить
определение выше вызова или заранее написать объявление
int add(int, int);, а тело разместить ниже.
Для void-функции можно завершить выполнение через
return; без значения или просто дойти до конца тела. В
функции, возвращающей int, каждый достигаемый путь
завершения должен вернуть подходящее значение; особое правило для конца
main здесь не действует.
Функция и процедура: в чём разница
Процедура (procedure) —
Учебное название функции с типом результата void. Она
выполняет действие, но не возвращает значение вызывающему коду. В
C++ отдельной конструкции procedure нет: процедура
записывается как обычная функция.
Функция int square(int value) может вернуть вычисленное
число: int result = square(4); сохраняет 16. Процедура
void printResult(int value) может вывести это число:
printResult(result);, но записать
int other = printResult(result); нельзя — возвращаемого
значения нет. Обе конструкции в C++ являются функциями; слово
«процедура» подчёркивает отсутствие возвращаемого значения, а не
отсутствие полезного действия.
Анонимная функция: лямбда-выражение
Лямбда-выражение (lambda expression) —
Выражение, создающее вызываемый объект с описанным поведением. У
самой лямбды нет обычного имени функции, но её можно сохранить в
переменную и вызывать через неё:
auto twice = [](int value) { return value * 2; };.
Базовая форма:
[захваты](параметры) -> тип_результата { тело }
Лямбду удобно создавать внутри main или другой функции,
когда небольшое действие нужно рядом с местом использования. Для
переменной обычно пишут auto: компилятор сам выводит
тип созданного вызываемого объекта.
#include <iostream>
int main() {
auto twice = [](int value) -> int {
return value * 2;
};
int result = twice(6);
std::cout << result << '\n';
return 0;
}| Часть | Что означает |
|---|---|
[] |
Список захватов. Пустые скобки означают, что лямбда
не захватывает локальные переменные окружающей
функции.
|
(int value) |
Параметры, которые передаются при каждом вызове.
|
-> int |
Явно заданный тип результата. В простом примере его
можно опустить: компилятор выведет
int из return.
|
{ return value * 2; } | Тело с обычными инструкциями C++. |
; после } |
Завершает объявление переменной twice.
После определения обычной функции такая точка с
запятой не требуется.
|
twice(6) | Вызывает сохранённую лямбду с аргументом 6. |
В других языках похожие анонимные функции могут называться
стрелочными. В C++ используется термин «лямбда-выражение» и запись
со скобками []; синтаксиса => здесь
нет. Стрелка -> указывает тип возвращаемого значения
и в простых случаях необязательна.
Параметры и захваты — разные источники данных
Параметр получает значение при каждом вызове, а захват позволяет
сохранить нужную локальную переменную при создании лямбды. Например,
[bonus] захватывает текущее значение
bonus по значению (создаёт внутри вызываемого объекта
свою копию). Последующее изменение исходной переменной не меняет эту
копию.
#include <iostream>
int main() {
int bonus = 5;
auto add_bonus = [bonus](int value) {
return value + bonus;
};
std::cout << add_bonus(10) << '\n';
bonus = 20;
std::cout << add_bonus(10) << '\n';
std::cout << "original bonus: " << bonus << '\n';
return 0;
}
Запись [&bonus] захватывала бы
bonus по ссылке, и вызов видел бы текущее значение
исходного объекта. При таком захвате нужно следить за временем
жизни: нельзя использовать лямбду после уничтожения захваченного
локального объекта. На первом этапе выбирай явные захваты вроде
[bonus], чтобы было видно, откуда берутся данные.
Перед этим практикумом пройдите время жизни и конечную рекурсию. Для обычных функций этот блок пока не требуется.
Замыкание и функция, создающая функцию
Замыкание (closure) — Вызываемый объект лямбды вместе с её захваченным окружением. Захват по значению сохраняет собственную копию настройки, которую лямбда использует при последующих вызовах.
Обычная функция может создать и вернуть лямбду. Например,
auto makeOffset(int shift) { return [shift](int x) { return x +
shift; }; }
возвращает готовое вычисление с запомненным сдвигом.
auto addFive = makeOffset(5); создаёт его, а
addFive(3) уже вычисляет ответ 8. Это два разных
вызова: сначала создание функции, затем её применение.
Каждый вызов генератора создаёт отдельный объект со своими
захватами. auto addTen = makeOffset(10); не изменит
поведение addFive. Тип результата
auto выводится из возвращаемой лямбды; такой вывод типа
результата функции доступен с C++14. Возвращайте захваты по
значению: ссылка на локальный параметр shift стала бы
висячей после завершения makeOffset.
И обычная функция, и лямбда задают вызываемое поведение. Различие в форме объявления и возможностях: обычная глобальная функция имеет имя и может вызываться из разных частей программы, а лямбда удобна для небольшого действия рядом с использованием и умеет захватывать локальные данные. Теперь можно перейти к вопросу, как компилятор выбирает между несколькими обычными функциями с одинаковым именем.
Иногда нужно многократно выполнять одно вычисление с постоянной настройкой. Например, к каждому аргументу прибавлять выбранный сдвиг. Обычная функция могла бы принимать и x, и shift при каждом вызове. Генератор makeOffset устроен иначе: он получает shift один раз и возвращает объект, который можно вызывать. Квадратные скобки [shift] сохраняют копию настройки внутри этого объекта. Вызов makeOffset(5) ещё не складывает пять с каким-либо x: аргумент x появится позже. Когда выполняется addFive(3), сохранённая настройка участвует в вычислении и получается восемь. Второй вызов генератора создаёт независимый объект. Изменение исходной переменной настройки не переписывает уже сохранённую копию. Auto позволяет компилятору назвать фактический тип результата за нас. Здесь не нужны ни std::function, ни указатели на функции, ни собственные классы. Захватывать ссылку на локальную настройку возвращаемой лямбды нельзя: после выхода из генератора локальная переменная прекращает существование.
#include <iostream>
auto makeOffset(int shift) {
return [shift](int x) { return x + shift; };
}
int main() {
int setting = 5;
auto addFive = makeOffset(setting);
auto addTen = makeOffset(10);
setting = 20;
std::cout << addFive(3) << ' ' << addTen(3) << '\n';
std::cout << setting << '\n';
}Области видимости и конфликты имён
Читая имя переменной, недостаточно найти где-нибудь в файле такое же слово. Нужно понять, из какой области к нему обращаются. Фигурные скобки создают вложенные блоки, а поиск имени учитывает ближайшие подходящие объявления. При скрытии появляется другая переменная с тем же написанием имени (старый объект не переименовывается и не превращается в новый).
Scope определяет, в какой части программы доступно имя. Внутренние области могут скрывать имена внешних областей (name hiding / shadowing).
Если снаружи объявлено int value = 10;, а внутри блока — int value = 20;, увеличение внутреннего value не меняет внешнее число. После выхода из вложенного блока снова доступно внешнее имя. Полезная привычка при разборе примера — связывать каждое использование имени с конкретным объявлением. Совпадение букв ещё не означает, что речь об одном и том же объекте.
int x = 5;
int y = 1;
int x = 2;
x → 2, ::x → 5
Поиск имени зависит от места обращения к нему. Внутреннее объявление может скрыть внешнее, а квалификатор позволяет указать нужную область явно. Проследите значения в визуализации и сравните два способа разделить одинаковые имена.
Внутренний x2Вложенный блок объявил собственный x. Простое x находит ближайшее объявление.
#include <iostream>
int value = 10;
int main() {
int value = 20;
{
int value = 30;
std::cout << value << ' ' << ::value << '\n';
}
std::cout << value << '\n';
}#include <iostream>
namespace first {
int size() { return 3; }
}
namespace second {
int size() { return 8; }
}
int main() {
std::cout << first::size() << ' ' << second::size() << '\n';
}#include <iostream>
int x = 5;
int main() {
std::cout << x << '\n'; // 5
{
int x = 2;
std::cout << x << '\n'; // 2
std::cout << ::x << '\n'; // 5
}
std::cout << x << '\n'; // 5
}Сокрытие имени (name hiding / shadowing) —
Во внутренней области объявлено имя x; обычный поиск
выбирает внутреннее x, хотя внешнее продолжает
существовать. При выходе из блока внутреннее перестаёт быть видимым.
Неоднозначность — ошибка компиляции. Если два пространства
имён привнесены директивами using namespace и обе
содержат x, обращение к x может стать
неоднозначным.
namespace A { int x = 6; }
namespace B { int x = 7; }
using namespace A;
using namespace B;
// int y = x; // ошибка: неоднозначное имя
int y = A::x; // корректно
Операция ::x означает обращение к глобальному
x. Она не предоставляет способ «перескочить» к произвольной
скрытой локальной переменной.
int x = x; имя справа уже относится
к объявляемому x, а не к внешнему одноимённому
имени. Чтение неинициализированного значения приводит к
некорректному поведению. Подробнее — правило point of
declaration / locus of declaration.
Объявления и определения
Объявление можно воспринимать как сообщение о том, что сущность существует и как ею пользоваться. Например, по int square(int); компилятор узнаёт тип аргумента и тип результата. Определение функции добавляет её тело — конкретные действия. Такое разделение позволяет одной части программы вызывать функцию, реализация которой находится в другом исходном файле.
Объявление (declaration) вводит имя и сообщает сведения о сущности. Определение (definition) — особое объявление, дающее полное определение, необходимое для использования сущности. Любое определение — объявление, но не любое объявление — определение.
int f(int);int f(int x) { return x+1; }extern int counter;int counter = 0;Для переменных правила выглядят иначе, чем для функции без тела. Запись int count; уже определяет переменную; отсутствие явного начального значения не превращает её в «только объявление». В то же время extern int count; обычно сообщает о переменной, определённой в другом месте. Здесь важно не угадывать смысл по наличию знака =, а различать назначение конкретной конструкции.
extern int count; // только объявление
int f(int); // объявление функции
int count = 0; // определение переменной
int f(int x) { return x+1; } // определение функцииИнициализация (initialization) — Получение объектом начального состояния при создании. int x = 5; определяет и инициализирует переменную. int x; тоже определяет переменную, но локальный int остаётся без заданного значения.
void f(int); — объявление функции без тела. int x; — уже определение переменной, даже если начальное значение не указано. Для объявления внешней переменной без определения используется extern int x;.
Выражения и инструкции
Выражение отвечает на вопрос, что вычислить: например, сумму, сравнение или результат вызова функции. Инструкция описывает законченный шаг выполнения программы. Выражение total + price само по себе задаёт вычисление суммы; запись total += price; становится отдельной инструкцией и дополнительно изменяет состояние программы. Различие полезно, когда вы пытаетесь понять, где допустим результат вычисления, а где требуется законченная конструкция.
Грамматическая конструкция, например переменная, литерал, операция или вызов функции. Выражение имеет тип, категорию значения и может иметь побочные эффекты. x + 5 и std::cout << x — выражения.
x и 5x + 5int, значениеИнструкция (statement) — Самостоятельная грамматическая конструкция: объявление, выражение-инструкция, блок {...}, управление if / for / while и др.
| Исходный код | Что перед нами |
|---|---|
5 + x | Выражение |
5 + x; | Выражение-инструкция: завершается ; |
int y = x + 5; | Объявление переменной; внутри него содержится выражение |
if (x) { ... } | Инструкция выбора, условие — выражение |
for (...) { ... } | Инструкция цикла |
; завершает многие инструкции; сама по себе точка с запятой в таком контексте не является арифметическим оператором. Запятая в списке аргументов функции — разделитель, а не оператор запятой.
Не каждое вычисленное значение программа сохраняет. Инструкция x + 1; для обычного целого x вычисляет сумму, но не записывает её обратно в x. Чтобы переменная изменилась, нужно явное действие, например x = x + 1;. Поэтому при чтении строки проверяйте два вопроса отдельно: какое значение получилось и изменилось ли что-нибудь в программе (это изменение называют побочным эффектом).
int x = 5; // объявление
int y = x + 2; // x + 2 — выражение
x += 1; // выражение-инструкция
if (x > 0) { // инструкция выбора
y = y * 2;
}Операторы и их вычисление
Оператор задаёт действие над операндами — значениями или выражениями, к которым он применяется. Например, у сложения два операнда, а у логического отрицания ! один. При этом знакомый символ не гарантирует одинакового смысла во всех местах: * может обозначать умножение или разыменование указателя. Значение записи определяется её окружением и типами, а не только внешним видом символа.
Для встроенных типов C++ предоставляет арифметические, сравнительные, побитовые, логические операторы и операторы присваивания. Один и тот же символ может иметь разные значения в зависимости от контекста и типов операндов.
| Группа | Операторы | Что делают |
|---|---|---|
Арифметика | + - * / % | Вычисления над числами, остаток % для целочисленных операндов. |
Сравнение | == != < > <= >= | Возвращают значение типа bool. |
Побитовые | & | ^ << >> | Обрабатывают биты целых чисел. У потоков <</>> перегружены. |
Логические | && || ! | Логическое И, ИЛИ, НЕ. Результат — bool. |
Присваивание | = += -= *= /= %= | Меняют левый операнд при допустимом присваивании. |
Ленивое вычисление логических операций
A && B вычисляет B только если A истинно. A || B вычисляет B только если A ложно. Это позволяет безопасно охранять потенциально недопустимый доступ.
Короткое замыкание позволяет сначала проверить условие, необходимое для следующего действия. В записи denominator != 0 && numerator / denominator > 2 деление выполняется только после успешной проверки знаменателя. Если поменять операнды местами, защита исчезнет: опасное выражение окажется раньше проверки. Читайте такую цепочку слева направо как последовательность разрешений: «сначала убедись, затем используй».
У встроенных && и || правый операнд вычисляется только тогда, когда нужен для результата. Это позволяет поставить проверку перед потенциально опасной операцией. Второй пример делает пропуск вычисления заметным через счётчик вызовов.
Результат false · правая часть не выполнялась
#include <iostream>
int main() {
int numerator = 10;
int denominator = 0;
if (denominator != 0 && numerator / denominator > 2) {
std::cout << "Large quotient\n";
} else {
std::cout << "Condition is false\n";
}
}#include <iostream>
bool probe(int& calls) {
++calls;
return true;
}
int main() {
int calls = 0;
bool first = false && probe(calls);
bool second = true || probe(calls);
bool third = true && probe(calls);
std::cout << std::boolalpha << first << ' ' << second << ' '
<< third << "; calls=" << calls << '\n';
}#include <vector>
std::vector<int> a = {1, 2, 3};
// a[4] не читается, если size() <= 4
if (a.size() > 4 && a[4] == 10) {
// безопасно по отношению к границе
}Не путайте && и &: побитовое & не обладает таким коротким замыканием. Проверку указателя или границ пишите через &&.
Пространства имён и using
В небольшой программе имён немного, но в большом проекте разные части легко захотят назвать функцию одинаково: например, print или open. Пространства имён позволяют сохранить понятные короткие имена внутри каждой части. Запись network::open уточняет, к какой группе относится функция (как путь к файлу уточняет, в какой папке его искать).
Пространство имён (namespace) — область, которая группирует имена. Имя n::x явно указывает: ищем x в n. Это помогает разделять код больших проектов.
main, nint x = 7;n::xusing n::x; — делаем имя доступным.using namespace n; — участвуют имена из n при поиске.using VI = vector<int>; — новое название типа.Директива using namespace не переносит функции в ваш код и не создаёт их копии: она влияет на поиск имён. Поэтому после подключения нескольких библиотек короткое имя может стать неоднозначным. Для чтения учебного примера полезно сначала замечать полные имена с ::. А using std::cout; вводит в текущую область конкретное имя и обычно делает намерение понятнее, чем подключение сразу всех имён пространства.
#include <iostream>
namespace N { int x = 7; }
int main() {
std::cout << N::x << '\n'; // явно: N::x
using N::x; // одно имя
std::cout << x << '\n'; // 7
}В заголовочных файлах и глобальной области лучше не писать using namespace std;: в поиск имён попадёт множество сущностей, что может привести к конфликтам и неожиданному разрешению перегрузки.
Псевдоним типа (type alias) — Конструкция using Int = long long; задаёт ещё одно имя того же типа, не создавая нового отдельного типа. typedef позволяет делать подобное, но хуже читается для сложных типов и не поддерживает alias templates.
#include <vector>
using VI = std::vector<int>;
typedef long long ll; // старый вариант
VI a = {1, 2, 3};
ll sum = 6;Терминология
Запись x — неквалифицированное имя (unqualified name); n::x — квалифицированное (qualified name).
Перегрузка функций
Перегрузка позволяет назвать похожие действия одинаково, сохранив разные варианты для разных типов данных. Но выбор делает компилятор, а не функция во время исполнения. Он рассматривает типы переданных аргументов и допустимые преобразования. Поэтому числа 3 и 3.0 могут вызвать разные версии функции, хотя с математической точки зрения обозначают одно значение.
Function overloading — несколько функций с одним именем, которые различаются списками типов параметров. При вызове компилятор выбирает подходящую перегрузку по правилам разрешения перегрузок.
При чтении вызова сначала выпишите тип каждого аргумента, затем сравните его с типами параметров кандидатов. Если одна функция принимает int, а другая double, литерал 3 точно совпадает с первым вариантом. Когда два варианта оказываются одинаково подходящими по правилам языка, компилятор сообщает о неоднозначности (не выбирает произвольную функцию по порядку её объявления).
#include <iostream>
void f(int) { std::cout << "int\n"; }
void f(double) { std::cout << "double\n"; }
int main() {
f(3); // int
f(3.0); // double
f(3.0f); // double: promotion
}При перегрузке компилятор сопоставляет аргументы с параметрами доступных функций. Точное совпадение обычно предпочтительнее преобразования. Чтобы увидеть разницу, меняйте тип аргумента, сохраняя его числовое значение.
int
Точное совпадение с f(int) — предпочтительно.
#include <iostream>
void show(int) { std::cout << "int\n"; }
void show(double) { std::cout << "double\n"; }
int main() {
show(3);
show(3.0);
show(static_cast<double>(3));
}#include <iostream>
#include <string>
void show(const char*) { std::cout << "const char*\n"; }
void show(const std::string&) { std::cout << "std::string\n"; }
int main() {
show("hello");
show(std::string{"hello"});
}| Преобразование | Обычно предпочтение | Пример |
|---|---|---|
Точное совпадение | Самое сильное | f(int) от 5 |
Повышение (promotion) | Лучше обычной conversion | float → double |
Обычное преобразование | Ниже promotion | double → int |
Нельзя перегружать лишь по возвращаемому типу. int f(int); и double f(int); не образуют две допустимые перегрузки. Если одинаково хорошие кандидаты — ошибка компиляции, а не ошибка во время выполнения.
void f(int);
void f(double);
// f(3L); // неоднозначно, когда long не совпадает с int
// int f(int); / double f(int); — так перегружать нельзяПрисваивание и категории значений
Присваивание изменяет уже существующий объект. Его левый операнд должен подходить для записи, а правый задаёт новое значение. Встроенный оператор присваивания также имеет собственный результат: он снова обозначает левый объект. Именно это позволяет использовать присваивание внутри более крупного выражения, хотя для первых программ отдельные простые инструкции обычно понятнее.
Для встроенного присваивания x = y результат — lvalue, обозначающее x. Поэтому возможны цепочки присваиваний и присваивание результату присваивания. Операция x + y обычно даёт временный результат, которому нельзя присвоить новое значение.
int x = 1;
(x = 5) = 8; // OKРезультат присваивания — снова x.
int x=1, y=2;
// (x + y) = 8; // ошибкаСумма встроенных чисел не обозначает изменяемую переменную.
Категорию значения удобно понимать как ответ на вопрос, что выражение даёт программе: доступ к определённому объекту или вычисленное значение. Например, имя изменяемой переменной x позволяет обратиться к самому объекту, а x + 1 даёт результат сложения. Слово lvalue не означает автоматического разрешения на изменение: имя константного объекта тоже может быть lvalue, но записывать в него нельзя.
int x = 1, y = 2, z = 3;
x = y = z; // справа налево: x=3, y=3
(x = 5) = 8; // допустимо: x=8
// (x + y) = 8; // ошибка компиляции
x += 2; // x=10Категории значений (lvalue · prvalue · xvalue) — Упрощение «lvalue — можно писать слева от =» удобно на старте, но неточно: существуют const-lvalue, которым присваивать нельзя. Категории описывают свойства выражений, а не просто место относительно знака равно.
x = (y = z). Для встроенных типов операция y = z изменяет y и возвращает lvalue, обозначающее y; затем это значение присваивается x.
Не путайте int x = 5; (инициализация при определении переменной) и x = 5; (присваивание существующему объекту).
Правило одного определения
Проект часто состоит из нескольких исходных файлов, которые собираются отдельно, а затем соединяются. Объявления помогают этим частям договориться о доступных функциях и переменных. Но для обычной используемой функции должна существовать согласованная реализация. Если разные части предлагают несовместимые определения одной сущности, у программы больше нет единого корректного описания её поведения.
В упрощённом виде: используемая функция или переменная с внешним связыванием должна иметь допустимое определение в программе, а повторные определения одной сущности ограничены правилами языка. Повторное определение обычной функции в одном translation unit — ошибка.
Практический учебный шаблон таков: объявление обычной функции размещают в заголовке, а её определение — в одном .cpp-файле. Защита заголовка от повторного включения решает другую задачу: не даёт вставить его содержимое дважды в одну единицу трансляции. Она не превращает обычную функцию из заголовка в автоматически допустимые определения во всех исходных файлах. Для inline и шаблонов существуют отдельные правила.
void f(); // можно объявить многократно
void f(); // повторное объявление корректно
void f() {} // одно определение
// void f() {} // ошибка: второе определениеПроверяет синтаксис, типы, доступность имён и многие повторы в отдельной единице трансляции.
Соединяет объектные файлы; при отсутствии или лишних внешних определениях часто сообщает ошибку.
Дополнительно
Point of declaration: необычный пример
Имя локальной переменной становится доступным уже в собственном инициализаторе. Поэтому int x = x; не читает внешний x.
int x = 5;
int main() {
// int x = x; // Не выполнять: справа уже новое неинициализированное x.
int local = ::x; // Корректно читаем глобальный объект.
(void)local;
}Компилироваться — не значит быть корректным при выполнении: чтение неинициализированного int не даёт определённого полезного результата.
Когда перегрузка становится сложной
Реальные правила учитывают несколько аргументов, стандартные и пользовательские преобразования, шаблоны и другие критерии. Эта лекция знакомит с самой идеей выбора лучшего кандидата.
void f(float, double);
void f(double, float);
// f(1.0f, 1.0f); // неоднозначно:
// первое лучше по первому аргументу,
// второе лучше по второмуКлючевые слова и препроцессор
int, namespace, class, using — ключевые слова с заданным грамматикой значением. Препроцессорная директива #define — отдельный механизм текстовой подстановки, который не создаёт новый язык и легко ухудшает читаемость.
Термины
Стандарт не гарантирует никакого результата: программа может работать как угодно.
Примеры кода
Параметры и результат функции
#include <iostream>
int maximum(int left, int right) {
if (left > right) return left;
return right;
}
int main() {
int a = 0, b = 0;
if (!(std::cin >> a >> b)) {
std::cerr << "Invalid numbers\n";
return 1;
}
std::cout << maximum(a, b) << '\n';
}Генератор создаёт независимые вызываемые объекты с копиями настроек.
#include <iostream>
auto makeOffset(int shift) {
return [shift](int x) { return x + shift; };
}
int main() {
int setting = 5;
auto addFive = makeOffset(setting);
auto addTen = makeOffset(10);
setting = 20;
std::cout << addFive(3) << ' ' << addTen(3) << '\n';
std::cout << setting << '\n';
}Обсудить лекцию с ИИ
Скопируйте промпт в свой ИИ-чат. В него уже включены материал этой лекции и примеры кода — можно сразу задавать вопросы и тренироваться.