JavaRush /Курсы /C++ SELF /Время жизни временных объектов

Время жизни временных объектов

C++ SELF
46 уровень , 4 лекция
Открыта

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 T& r = T{...};
Прямая инициализация const& временным Время жизни временного обычно продлевается до конца scope r
return T{...};
Возвращаем по значению ОК: наружу уходит значение, не ссылка
return const T&
Возвращаем ссылку на временный Почти всегда dangling сразу после return
const T& r = f(T{...});
Возвращаем ссылку на параметр, передали временный Временный живёт только до ;, после — dangling
struct S { const T& ref; };
«Запоминаем ссылку на потом» Если источник окажется временным — структура хранит 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 в строке, которая выглядит абсолютно логично. Привыкайте мысленно ставить границу жизни временного на ;, если не доказано обратное.

1
Задача
C++ SELF, 46 уровень, 4 лекция
Недоступна
Бейдж одним вздохом
Бейдж одним вздохом
1
Задача
C++ SELF, 46 уровень, 4 лекция
Недоступна
Приветствие без дыр
Приветствие без дыр
1
Задача
C++ SELF, 46 уровень, 4 лекция
Недоступна
Эхо без сюрпризов
Эхо без сюрпризов
1
Задача
C++ SELF, 46 уровень, 4 лекция
Недоступна
Сообщение владельца
Сообщение владельца
1
Опрос
noexcept, 46 уровень, 4 лекция
Недоступен
noexcept
noexcept
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ