Время жизни временного объекта, связанного с константной ссылкой (цепочка методов)

Рассмотрим следующий фрагмент кода:

#include <iostream>

struct S {
    ~S() { std::cout << "dtor\n"; }
    const S& f(int i) const { std::cout << i << "\n"; return *this; }
};

int main() {
    const S& s = S(); 
    s.f(2);
}

Вывод:

2
dtor

Т.е. время жизни объекта продлевается по ссылке, что объясняется в Herb's статья.

Но если мы изменим всего одну строчку кода и напишем:

const S& s = S().f(1);

вызов f(2) на уже уничтоженном объекте:

Вывод:

1
dtor
2

Почему это случилось? Является ли возвращаемое значение f() неправильным типом «темпоральности»?


person αλεχολυτ    schedule 01.09.2015    source источник
comment
f возвращает lvalue, а не prvalue.   -  person Kerrek SB    schedule 01.09.2015
comment
И время жизни S() заканчивается в конце полного оператора.   -  person Jarod42    schedule 01.09.2015
comment
@KerrekSB почему lvalue? Мы не можем разместить f слева от =.   -  person αλεχολυτ    schedule 01.09.2015
comment
@alexolut: Это ни с чем не связано. f возвращает lvalue, потому что его объявленный тип возвращаемого значения T & для некоторого не ссылочного типа T (а именно T = const S).   -  person Kerrek SB    schedule 01.09.2015
comment
Голосование за повторное открытие, потому что причина в другом вопросе (нелокальная ссылка на константу) отличается от причины здесь (цепочка методов). Кроме того, разные компиляторы дают разные результаты с этим кодом: clang 5 продлевает время жизни, а VC ++ 2017 - нет. Я читаю вопросы по этой теме около часа и до сих пор не нашел окончательного ответа ни на этот, ни даже на другой вопрос, который ставит этот конкретный вопрос.   -  person Adrian McCarthy    schedule 16.11.2017


Ответы (2)


Когда вы пишете функцию таким образом ...

const S& f(int i) const { std::cout << i << "\n"; return *this; }

... вы инструктируете компилятор вернуть const S& и берете на себя ответственность за обеспечение того, чтобы указанный объект имел время жизни, подходящее для использования вызывающей стороной. («обеспечение» может означать документирование использования клиента, которое правильно работает с вашим дизайном.)

Часто - с типичным разделением кода на заголовки и файлы реализации - реализация f(int) const даже не будет видна вызывающему коду, и в таких случаях компилятор не понимает, какой S ссылка может быть возвращена, и является ли эта S временный или нет, поэтому у него нет оснований для принятия решения о том, нужно ли продлевать срок службы.

Помимо очевидных вариантов (например, доверие клиентам писать безопасный код, возврат по значению или интеллектуальный указатель), стоит знать о более неясном варианте ...

const S& f(int i) const & { ...; return *this; }
const S f(int i) const && { ...; return *this; }

& и && непосредственно перед телами функций перегружают f, так что используется версия &&, если *this является перемещаемым, в противном случае используется версия &. Таким образом, кто-то, привязывающий const & к f(...), вызванный к истекающему объекту, будет привязан к новой копии объекта и продлит время жизни в соответствии с локальной const ссылкой, в то время как, когда объект не истекает (пока), ссылка const будет к исходному объекту (который по-прежнему не гарантирован, пока действует ссылка - требуется некоторая осторожность).

person Tony Delroy    schedule 01.09.2015
comment
У вас есть рекомендации, как написать более безопасный код в случае использования функции, возвращающей ссылку? Может быть, всегда присваивать ему значение (а не другую ссылку)? В идеале эта ошибка возникает во время компиляции. - person αλεχολυτ; 01.09.2015
comment
@alexolut В настоящее время лучшая рекомендация - не использовать цепочку методов в C ++. Также обратите внимание, что порядок оценки не указан, поэтому в foo.f( bar() ).g( baz() ) вы можете увидеть baz(), вызванный перед bar(). - person Potatoswatter; 02.09.2015
comment
@alexolut: Я кое-что добавил к ответу, обсуждая некоторые варианты. - person Tony Delroy; 02.09.2015
comment
@Potatoswatter Как ваш совет not to use method chaining согласуется с operator<< для классов стандартного потока? - person αλεχολυτ; 02.09.2015
comment
@alexolut Это не так. Но я бы сказал иначе: потоки несовместимы с хорошим советом, поэтому невинно выглядящее использование может привести к висящим ссылкам. В моем предложении есть несколько примеров. Или должен, может я их еще не добавил. Примерно так: std::ostream & text = std::ostringstream{} << "hello " << 123; Предложение также содержит некоторый исторический контекст, если вам интересно: в самых ранних компиляторах C ++ такие вещи действительно работали. - person Potatoswatter; 02.09.2015

Почему это случилось? Является ли возвращаемое значение f() неправильным типом «темпоральности»?

Верно, это не так. В последнее время это несколько противоречивый вопрос: официальное определение «темпоральности» носит несколько открытый характер.

В последних компиляторах темпоральность расширяется. Сначала он применялся только к выражениям prvalue (не «ссылочные»), а доступ к членам («оператор точки») применялся к таким выражениям. Теперь это применимо также к выражениям приведения и доступу к массивам. Хотя вы можете записать операцию перемещения как static_cast< T && >( t ), что сохранит темпоральность, простая запись std::move( t ) - нет.

Я работаю над серией предложения по расширению C ++, чтобы ваш пример работал так, как вы ожидали. Вероятность того, что эта функция может появиться в C ++ 17, ненулевая.

person Potatoswatter    schedule 01.09.2015