Чи продовжує член референтного класу const життя тимчасового?


171

Чому так:

#include <string>
#include <iostream>
using namespace std;

class Sandbox
{
public:
    Sandbox(const string& n) : member(n) {}
    const string& member;
};

int main()
{
    Sandbox sandbox(string("four"));
    cout << "The answer is: " << sandbox.member << endl;
    return 0;
}

Дайте вихід:

Відповідь:

Замість:

Відповідь: чотири


39
І просто для більшої розваги, якби ви написали cout << "The answer is: " << Sandbox(string("four")).member << endl;, то це б гарантовано працювало.

7
@RogerPate Чи можете ви пояснити, чому?
Паоло М

16
Для когось цікаво, наприклад, Роджер Пат розмістив твір, оскільки рядок ("чотири") тимчасовий, а тимчасовий знищується в кінці повної експресії , тому в його прикладі, коли SandBox::memberйого читають, тимчасова рядок все ще залишається живою .
PcAF

1
Питання: Оскільки писати такі класи небезпечно, чи існує попередження компілятора щодо передачі тимчасових категорій до таких класів , чи є керівництво щодо проектування (у Stroustroup?), Яке забороняє писати класи, які зберігають посилання? Більш кращим буде настанова щодо збереження покажчиків замість посилань.
Грим Фанданго

@PcAF: Чи можете ви поясніть, чому тимчасове string("four")знищується в кінці повного виразу, а не після закінчення Sandboxконструктора? Відповідь Potatoswatter говорить, що тимчасовий
Тейлор Ніколс

Відповіді:


166

Лише місцеві const довідки продовжують термін експлуатації.

Стандарт визначає таку поведінку в §8.5.3 / 5, [dcl.init.ref], розділі про ініціалізатори опорних декларацій. Посилання у вашому прикладі пов'язане з аргументом конструктора nі стає недійсним, коли об'єкт nповинен вийти за межі області.

Продовження терміну служби не є транзитивним через аргумент функції. §12.2 / 5 [клас.часова]:

Другий контекст - це коли посилання пов'язане з тимчасовим. Тимчасовий, до якого посилається посилання, або тимчасовий, що є повним об'єктом, до суб'єкта якого тимчасовий прив'язується, зберігається протягом життя посилання, крім випадків, зазначених нижче. Тимчасовий прив’язаний до опорного члена в ctor-ініціалізаторі конструктора (§12.6.2 [class.base.init]) зберігається, поки конструктор не вийде. Тимчасове прив’язання до опорного параметра у виклику функції (§5.2.2 [expr.call]) зберігається до завершення повного виразу, що містить виклик.


49
Ви також повинні побачити GotW # 88 для більш зрозумілих для людини пояснень: biljeutter.com/2008/01/01/…
Натан Ернст

1
Я думаю, що було б зрозуміліше, якби стандарт сказав "Другий контекст - це коли посилання пов'язане з первинним значенням". У коді OP ти міг би сказати , що memberце пов'язано з тимчасовим, тому що ініціалізація memberз nдопомогою прив'язки до memberодного і того ж об'єкту nзобов'язаний, і це насправді тимчасовий об'єкт в цьому випадку.
ММ

2
@MM Існують випадки, коли ініціалізатори lvalue або xvalue, що містять первіту, розширять першіlue. У моєму документі P0066 розглядається стан справ.
Potatoswatter

1
Станом на C ++ 11, посилання Rvalue також продовжує термін служби тимчасового, не вимагаючи constкваліфікатора.
GetFree

3
@KeNVinFavo так, завжди використовується мертвий предмет UB
Potatoswatter

30

Ось найпростіший спосіб пояснити, що сталося:

У main () ви створили рядок і передали його в конструктор. Цей рядковий екземпляр існував лише в конструкторі. Всередині конструктора ви призначили члена, щоб вказати безпосередньо на цей екземпляр. Коли область вийшла з конструктора, екземпляр рядка був знищений, а член потім вказав на об'єкт рядка, якого вже не існувало. Надання Sandbox.member вказівки на посилання поза його межі не буде містити ці зовнішні екземпляри в області застосування.

Якщо ви хочете виправити вашу програму, щоб відображати бажану поведінку, внесіть такі зміни:

int main()
{
    string temp = string("four");    
    Sandbox sandbox(temp);
    cout << sandbox.member << endl;
    return 0;
}

Тепер temp вийде за межі в кінці main (), а не в кінці конструктора. Однак це погана практика. Змінна вашого члена ніколи не повинна бути посиланням на змінну, яка існує поза екземпляра. На практиці ви ніколи не знаєте, коли ця змінна вийде за межі сфери.

Я рекомендую визначити Sandbox.member як a const string member;Це скопіює дані тимчасового параметра в змінну члена, а не призначити змінну члена як тимчасовий параметр.


Якщо я це роблю: const string & temp = string("four"); Sandbox sandbox(temp); cout << sandbox.member << endl;чи все одно це буде працювати?
Ів

@Thomas const string &temp = string("four");дає такий же результат, як const string temp("four"); , якщо ви не використовуєте decltype(temp)конкретно
MM

@MM Дякую зараз, я цілком розумію це питання.
Ів

However, this is bad practice.- чому? Якщо і темп, і об'єкт, що містить об'єкт, використовують автоматичне зберігання в однаковій області, чи не на 100% безпечно? І якщо ви цього не зробите, що б ви зробили, якщо рядок занадто великий і такий занадто дорогий для копіювання?
макс

2
@max, тому що клас не примушує передане тимчасово мати правильну область застосування. Це означає, що одного разу ви можете забути про цю вимогу, передати недійсне тимчасове значення, і компілятор не попередить вас.
Алекс Че

5

Технічно кажучи, цій програмі не потрібно насправді нічого виводити на стандартний вихід (з чого це спочатку буферний потік).

  • cout << "The answer is: "Біт буде випромінювати "The answer is: "в буфер на стандартний висновок.

  • Тоді << sandbox.memberбіт подасть звисаючу посилання operator << (ostream &, const std::string &), яка викликає невизначене поведінку .

Через це нічого не гарантується. Програма може спрацювати начебто добре, або може вийти з ладу, навіть не змиваючись, тобто текст "Відповідь:" не з’явиться на вашому екрані.


2
Коли є UB, поведінка всієї програми не визначено - вона не починається просто в певний момент виконання. Тож ми не можемо точно сказати, що "The answer is: "буде написано де завгодно.
Toby Speight

0

Оскільки ваша тимчасова рядок вийшла з рамки, як тільки конструктор Sandbox повернувся, а зайнятий нею стек був відновлений для деяких інших цілей.

Як правило, ви ніколи не повинні зберігати посилання довгостроково. Посилання хороші для аргументів або локальних змінних, а не членів класу.


7
"Ніколи" - жахливо сильне слово.
Фред Ларсон

17
ніколи членів класу, якщо вам не потрібно зберігати посилання на об'єкт. Є випадки, коли вам потрібно зберігати посилання на інші об'єкти, а не копії, оскільки для цих випадків посилання є чіткішим рішенням, ніж покажчики.
Девід Родрігес - дрибес

0

ви маєте на увазі те, що зникло. Далі буде працювати

#include <string>
#include <iostream>

class Sandbox
{

public:
    const string member = " "; //default to whatever is the requirement
    Sandbox(const string& n) : member(n) {}//a copy is made

};

int main()
{
    Sandbox sandbox(string("four"));
    std::cout << "The answer is: " << sandbox.member << std::endl;
    return 0;
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.