1. Зачем нужны временные объекты
Временный объект (temporary) — это объект без имени, который создаётся «по пути», пока вычисляется выражение. И вот здесь начинается магия: выражение вы написали на одну строчку, а компилятор внутри может создать 1–3 объекта, часть из которых исчезнет раньше, чем вы успеете сказать «да я же просто склеил строки».
Интуитивно хочется думать так: «если я вижу значение, значит оно где-то лежит и живёт». В C++ это неверно. Значение может быть получено из временного объекта, который уничтожится сразу после окончания выражения (чаще всего — после ;). В итоге вы получаете ссылку/указатель на память, где раньше был объект. А что там теперь — лотерея.
Давайте договоримся о рабочей модели на сегодня: мы не будем читать стандарт как роман на ночь, а будем мыслить так, чтобы писать корректный код.
Как распознать временный объект
Когда вы только начинаете, временные объекты проще всего узнавать по двум признакам: у объекта нет имени в коде, и он создаётся прямо в выражении. Самые типовые источники временных: возврат по значению из функции, конструирование прямо в месте вызова, результаты операторов вроде + для строк, когда создаётся новая строка-результат.
Посмотрим на мини-примеры. Они короткие, но очень показательные.
#include <string>
std::string make_name() {
return "Alice"; // возвращаем по значению -> у вызывающего появится временный/результат
}
#include <string>
int main() {
std::string s = std::string("hi"); // "hi" -> временный std::string
}
#include <string>
int main() {
std::string a = "hi";
std::string b = " there";
std::string c = a + b; // результат a+b -> временная строка (потом копируется/перемещается в c)
}
Пока что всё звучит «и что?». А «что» начинается тогда, когда мы пытаемся привязать ссылку к тому, что живёт слишком мало.
2. Полное выражение и срок жизни временных
Сейчас будет ключевая идея, без которой всё остальное превращается в гадание по компилятору.
Упрощённое правило: временные объекты живут до конца полного выражения. В большинстве случаев «полное выражение» — это то, что заканчивается точкой с запятой ;. То есть временный объект в строке:
auto x = something();
обычно живёт до ;, и потом уничтожается.
Это правило удобно представлять как «линию времени» одной инструкции:
flowchart TD
A[начали вычислять выражение] --> B[создали временный объект]
B --> C[использовали его в вычислениях]
C --> D[дошли до конца полного выражения ';']
D --> E[временный уничтожен]
Выглядит просто. Опасность в том, что ссылка/указатель могут пережить этот момент — а объект уже нет.
Скрытые временные, которые вы не ожидаете
С временными объектами есть подлость: они часто «спрятаны» в привычных конструкциях. В коде вы не видите std::string(...), но временный всё равно создаётся.
Конкатенация строк: a + b + c
Под капотом обычно создаются промежуточные результаты.
#include <string>
int main() {
std::string a = "A";
std::string b = "B";
std::string c = "C";
std::string result = a + b + c; // минимум один временный объект где-то внутри
}
Пока вы сохраняете итог по значению (std::string result = ...;), всё хорошо. Но если вы попытаетесь взять ссылку на результат выражения — будет опасно.
#include <string>
int main() {
std::string a = "A";
std::string b = "B";
const std::string& r = a + b; // это ОК: r продлевает жизнь временного результата a+b
}
А вот если вы сделаете так, чтобы ссылка пережила выражение через функцию — снова ловушка (см. пример с identity дальше).
Тернарный оператор ?: тоже может порождать временные
Это не «обязательно так будет», но на уровне модели стоит помнить: выражения могут создавать временные результаты.
#include <string>
std::string choose(bool first) {
return first ? std::string("yes") : std::string("no");
}
И снова: если возвращаете по значению, вы в безопасности.
4. Продление времени жизни через const T&
Сейчас будет кусочек «хорошей магии». Иногда C++ действительно помогает: если вы напрямую инициализируете переменную типа const T& временным объектом, то время жизни временного продлевается до конца области видимости этой ссылки.
Важно прочитать это медленно: продлевается время жизни объекта, а не «ссылка становится безопасной по дефолту во всех случаях». Работает это только в конкретном сценарии.
Пример, который работает безопасно:
#include <iostream>
#include <string>
int main() {
const std::string& r = std::string("hi");
std::cout << r << '\n'; // hi
} // здесь r умирает, и вместе с ним уничтожается та временная строка
Почему это безопасно? Потому что временная std::string("hi") привязана к переменной-ссылке r напрямую, и компилятор продлевает жизнь временного объекта до конца scope r.
Это похоже на ситуацию «вы взяли кота на руки (ссылка): пока вы держите кота, кот не исчезает». Но только если вы держите его сразу, а не «через друга».
5. Когда продление времени жизни не работает
Вот здесь новичков обычно и ловит C++. Кажется логичным: «если const& может продлевать жизнь временного, значит можно возвращать const& и всё будет круто». Нет, не будет.
Нельзя вернуть const& на временный из return
Сценарий «функция возвращает ссылку на временный объект» — это почти всегда ошибка. Время жизни временного не «перескакивает» через return.
#include <string>
const std::string& bad_ref() {
return std::string("hi"); // dangling: временный уничтожится при выходе из функции
}
Почему так? Потому что продление времени жизни работает при инициализации переменной-ссылки, а return — это не «инициализация переменной вызывающего кода», это механизм передачи результата. Внутри функции временный объект умрёт, и наружу улетит ссылка на «уже бывшую строку».
Ловушка: возвращаем ссылку на параметр
Теперь более хитрый пример. Он компилируется, выглядит прилично и даже иногда «работает».
#include <string>
const std::string& identity(const std::string& s) {
return s;
}
Если вызвать так — нормально:
#include <iostream>
#include <string>
const std::string& identity(const std::string& s) {
return s;
}
int main() {
std::string name = "Alice";
const std::string& ok = identity(name);
std::cout << ok << '\n'; // Alice
}
Но если вызвать с временным объектом — получаем dangling сразу после ;:
#include <iostream>
#include <string>
const std::string& identity(const std::string& s) {
return s;
}
int main() {
const std::string& bad = identity(std::string("Bob"));
std::cout << bad << '\n'; // UB: после ';' временная "Bob" уничтожена
}
Что произошло? Временная std::string("Bob") живёт до конца полного выражения вызова функции, то есть до ;. А ссылка bad живёт дальше. Итог: bad указывает на объект, который уничтожен.
Это один из самых неприятных багов, потому что «вроде бы мы везде использовали const&, всё же должно быть безопасно». Нет: const& — это контракт «я не копирую и не изменяю», но не гарантия времени жизни.
6. Мини-программа: «генератор сообщений» и правильный контракт
Чтобы примеры не висели в вакууме, давайте продолжим одну и ту же мини-идею, которая полезна в реальных приложениях: формируем сообщения для пользователя. В таких задачах начинающие часто хотят «оптимизировать» и вернуть const std::string&, чтобы «не копировать строку». И именно здесь рождаются dangling-ссылки.
Сделаем маленький модуль: функция формирует приветствие.
Плохой вариант
#include <string>
const std::string& make_greeting_bad(const std::string& name) {
return std::string("Hello, ") + name; // dangling: возвращаем ссылку на временный результат
}
Почему это плохо? Потому что std::string("Hello, ") + name — временная строка-результат выражения. Она умрёт сразу после return. А ссылка наружу останется.
Хороший вариант: возвращаем по значению
Стоп. string_view у нас будет в следующих лекциях, не подглядываем. Сделаем честно на текущих знаниях: параметр const std::string&, а возврат — std::string.
#include <string>
std::string make_greeting(const std::string& name) {
return std::string("Hello, ") + name; // OK: возвращаем значение, не ссылку
}
И проверим в main:
#include <iostream>
#include <string>
std::string make_greeting(const std::string& name) {
return std::string("Hello, ") + name;
}
int main() {
std::string user = "Alice";
std::cout << make_greeting(user) << '\n'; // Hello, Alice
}
Здесь временные объекты есть (промежуточные строки), но они не «утекают наружу» через ссылку. Итоговый объект — самостоятельное значение.
7. Практическая памятка по времени жизни
Сейчас полезно собрать «полевую памятку». Это не полный стандарт, но очень рабочий набор правил, чтобы не падать в UB каждые два часа.
| Сценарий | Что делаем | Что происходит со временем жизни |
|---|---|---|
|
Прямая инициализация const& временным | Время жизни временного обычно продлевается до конца scope r |
|
Возвращаем по значению | ОК: наружу уходит значение, не ссылка |
|
Возвращаем ссылку на временный | Почти всегда dangling сразу после return |
|
Возвращаем ссылку на параметр, передали временный | Временный живёт только до ;, после — dangling |
|
«Запоминаем ссылку на потом» | Если источник окажется временным — структура хранит dangling |
Если вы запомните хотя бы два пункта — «return ссылки на временное нельзя» и «временный живёт до ;» — вы уже будете на голову выше многих начинающих.
Как думать об этом без паники: «где живёт объект?»
Чтобы не превращать разработку в паранойю, полезна простая дисциплина мышления.
Когда вы видите const T& или T*, задайте себе один вопрос: «А кто владелец объекта, на который я сейчас смотрю, и сколько он будет жить?»
Если владелец — локальная переменная в main, обычно всё нормально. Если владелец — временный объект в выражении, считайте, что он живёт до ближайшего ;, если только вы не сделали прямую инициализацию const& переменной (и вы уверены, что именно она продлевает жизнь).
И ещё один практичный критерий: если вы хотите «вернуть наружу» результат вычислений — по умолчанию возвращайте значение. Современный C++ умеет делать это эффективно, а ваш мозг скажет спасибо.
8. Ловушка: сохраняем ссылку в поле структуры
Сейчас мы не уходим в ООП-джунгли, но даже со struct легко сделать проблему. Представим, что мы хотим хранить «текст сообщения» без копии, и делаем поле-ссылку.
#include <string>
struct MessageRef {
const std::string& text;
};
Теперь можно написать так:
#include <iostream>
#include <string>
struct MessageRef {
const std::string& text;
};
int main() {
MessageRef m{std::string("Hi!")}; // dangling: временная строка умрёт после ';'
std::cout << m.text << '\n'; // UB
}
Это выглядит почти как «продление жизни через const&», но нет: продлевается жизнь временного при инициализации переменной-ссылки, а здесь ссылка — это подобъект внутри MessageRef. В итоге временная строка живёт до конца полного выражения, а m.text остаётся ссылкой на мёртвое.
Как сделать правильно на текущем уровне? Хранить владение: то есть хранить std::string.
#include <string>
struct Message {
std::string text;
};
И строить так:
#include <iostream>
#include <string>
struct Message {
std::string text;
};
int main() {
Message m{std::string("Hi!")}; // OK: m владеет строкой
std::cout << m.text << '\n'; // Hi!
}
Да, здесь есть копирование/перемещение. И да, это почти всегда правильная цена за корректность.
9. Типичные ошибки
Ошибка №1: возвращать const T& “ради производительности”, когда внутри создаётся новый объект.
Это очень частая причина UB: внутри функции вы собираете строку, вектор, структуру — и возвращаете ссылку. Компилятор не обязан вас спасать. Если объект создан внутри функции, он умрёт на выходе из неё, а ссылка останется. В таких случаях правильный контракт — вернуть T по значению.
Ошибка №2: верить, что “параметр const& делает всё безопасным”.
const& в параметрах — это про отсутствие копии и про «я не изменяю». Но если вы вернёте ссылку на параметр наружу или сохраните её где-то, вызывающий код может передать временный объект, и после ; ссылка станет висячей. Самый коварный момент тут в том, что код отлично компилируется и даже может работать «на вашем компьютере».
Ошибка №3: путать “время жизни ссылки” и “время жизни объекта”.
Ссылка — это просто другое имя для объекта. Она не является владельцем. Ссылка может существовать дольше, чем объект, и тогда она превращается в dangling reference. Это не «редкий случай», а ежедневная реальность, если вы начинаете возвращать ссылки из функций без строгого контракта.
Ошибка №4: хранить ссылки в полях структур/классов без железных гарантий времени жизни источника.
Ссылочное поле выглядит как «крутая оптимизация», но на практике очень быстро превращается в «мины замедленного действия», особенно когда в конструктор прилетает временный объект. Если объект должен жить вместе со структурой — структура должна им владеть (например, хранить std::string, а не const std::string&).
Ошибка №5: считать, что временный объект живёт “пока его не перестанут использовать”.
В C++ жизнь временного обычно заканчивается в конце полного выражения, а не в конце «последнего чтения» из него. Это кажется придиркой, пока не получите UB в строке, которая выглядит абсолютно логично. Привыкайте мысленно ставить границу жизни временного на ;, если не доказано обратное.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ