среда, января 09, 2008

Возможно, самый важный const

Герб Саттер у себя в блоге рассказывает про интересный случай с использованием const.

Краткий пересказ для ленивых.


string f() { return "abc"; }

void g() {
const string& s = f();
cout << s << endl; // можно ли использовать временный объект?
}


Код несколько напрягает. Создается ссылка на временный объект. Но тем не менее, с кодом все в порядке. Почему так? Потому что в С++ явно специфицировано, что если привязать временный объект к ссылке на const в стеке, то жизнь временного объекта будет продлена. Теперь он будет жить столько, сколько живет константная ссылка на него. В приведенном примере все валидно, время жизни s заканчивается с закрывающей фигурной скобкой.

Это все относится только к объектам в стеке. На члены класса это не действует.

С++ это специфицирует, а как оно в реальности, работает? Герб проверил в нескольких компиляторах, нормально, практически во всех работает.

Легким движением руки убираем const...

string f() { return "abc"; }

void g() {
string& s = f(); // все еще нормально?
cout << s << endl;
}


И получаем невалидный код, наличие const'а тут важно. Правильный компилятор выдаст ошибку на этапе компиляции.

И есть еще момент с вызовом деструктора.

Derived factory(); // construct a Derived object

void g() {
const Base& b = factory(); // здесь вызов Derived::Derived
// … используем b …
} // здесь вызывается Derived::~Derived напрямую
//-- а не Base::~Base + virtual dispatch!


Ссылки по теме:
Использование const. Часть 1.
Использование const. Часть 2.

52 коммент.:

Roman Konovalov комментирует...

В примере без const время жизни s тоже ведь заканчивается с закрывающей фигурной скобкой?

Этот код работает в Visual Studio, никаких warnings нет (кроме getch()):

std::string f()
{
return "abc";
}

void g()
{
using namespace std;

const string& s1 = f();
cout << "Reading with const: " << s1 << endl;

string& s2 = f();
cout << "Reading without const: " << s2 << endl;

getch();
}

int _tmain(int argc, _TCHAR* argv[])
{
g();
}

blog комментирует...

Совершенно правильно, что ругают C++! Поделом. :-)

Yuriy Volkov комментирует...

совершенно верно все Алёна написала

В случае
string& s=string("abc")
создается ссылка на временную переменную, которая прекращает свое существование после выполнения выражения. Если бы так не было (т.е. время жизни временной переменной равнялось времени жизни ссылки на нее), то можно было бы написать

string& s=string("abc")
s="Hello world"

что однако не имеет большого смысла - модифицировать скоро исчезающую временную переменную

В случае
const string& s=string("abc")
временная переменная живет пока живет константная ссылка на нее, и так как ссылка константная, то модификация временной переменной запрещена.

Мне кажется именно из этих соображений исходили, когда определяли такой behaviour

Алёна комментирует...

2Roman Konovalov:
В примере без const время жизни s тоже ведь заканчивается с закрывающей фигурной скобкой?

Этот код работает в Visual Studio, никаких warnings нет


Я проверила в MSVC++6.0 и в MSVC++2003.
MSVC++6.0 - все компиляет на ура. При запуске никаких проблем.
MSVC++2003 - выдает warning, если ему поднять уровень тревожности до четвертого.
warning C4239: nonstandard extension used : 'initializing' : conversion from 'std::string' to 'std::string &'
A reference that is not to 'const' cannot be bound to a non-lvalue

При запуске опять же никаких проблем.

Саттер у себя в посте упомянул, что VC++ компиляет это дело с ворнингом. То, что в шестой версии ворнинга нет - это неправильно, потому что это не валидный ISO C++. Но шестой VC++ он вообще довольно глюкавый.

Итого: нет, без const время жизни s заканчивается до фигурной скобки, приведенный в примере без const код не валиден. VC++ при компиляции этого примера допускает некоторую самодеятельность, в принципе можно этой его особенностью пользоваться, если очень нужно, но помнить, что код при этом будет непортируем.

2blog:
Совершенно правильно, что ругают C++! Поделом. :-)

Злыдень :-)

Roman Konovalov комментирует...

Если временая переменая прекращает свое существование сразу после string& s=string("abc"), то что там будет в s к концу выполнения функции никто не знает. Тем не менее, Visual Studio такие вещи не отлавливает. Либо дело кончится крэшом, либо эта конкретная реализация компилятора не освобождает временную переменную сразу. Получается так?

Roman Konovalov комментирует...

2 Алёна - верно, с 4ым уровнем показывает варнинг. MSDN объясняет так же, как и в статье: http://msdn2.microsoft.com/en-us/library/aa233872(VS.60).aspx

Только пример на установленом на моей машине MSDN другой - :

// compile with: /W4 /c
struct C { C(){} };

void func(void) {
C & rC = C(); // C4239
const C & rC2 = C(); // OK
rC2;
}

Век живи, век учись, хорошая статья :-)

Yuriy Volkov комментирует...

gcc version 4.1.2 20061115 (prerelease) (Debian 4.1.1-21)
выдает ошибку времени компиляции на этом примере ;-).

Yuriy Volkov комментирует...

либо эта конкретная реализация компилятора не освобождает временную переменную сразу
Получается так, раз Алёна говорит, что при запуске никаких проблем ;-)

Алёна комментирует...

2Roman Konovalov:
Либо дело кончится крэшом, либо эта конкретная реализация компилятора не освобождает временную переменную сразу. Получается так?

Скорее второе - MSVC++ не удаляет временную переменную сразу в этом случае. Причем, судя по тому, что это задокументировано в msdn, это фича MSVC++, а не бага.

Yuriy Volkov комментирует...

по хорошему надо было бы добавить опцию, которая эту "фичу" позволяет отключать ;-). Подобные "фичи" очень мешают, когда нужно писать и отлаживать переносимый код :-(

discouraged_one комментирует...

Вы чё-та все miss the point
почитайте ка лучше оригинал. Если не поймете, то хотя бы не рассуждайте про C++ -- у этой фичи тож есть своя история.
Тут фишка в том как это работает. Заодно посмотрите в каком контесте Александерску сказал про "самый главный const"

Анонимный комментирует...

спасибо за информацию.

как раз из тех, что сам даже не подумаешь искать, будучи уверен, что так нельзя

hVostt комментирует...

По мне - информация интересная, но ничего нового, потому что временные переменные и их константность - азы С++. Если учесть, что по ссылкам переменные передаются только через модификатор const, и чувствуют себя при этом прекрасно - а параметры функции те же самые локальные переменные в контексте этой самой функции, то... :) Это ж логика, которая должна работать на уровне подсознания. Я, например, долго напрягался, пытаяся найти трик в примере, пока не прочитал пост. Зато полезна информация в отношении политики MSVC. Опять огорчения :)

К комментарию blog: это как правила вождения автомобиля - если знаешь простое правило, что направо поворачивать надо с правой полосы, то и на кольце не запутаешься, вовремя перестроившись. Будем ругаться, что в правилах отдельно не оговаривается поведение водителя на кольце? Так же и в С++, все достаточно грамотно и лаконично, я вообще проблем не испытываю и не удивляюсь в большинстве случаев, при том что стандарта не читал и не собираюсь читать. Удивляет порой поведение компиляторов, которые, чувствуешь, что гонят уже на подсознательном уровне. Это откровенно раздражает, такие не соответствия.

Алёна комментирует...

2hVostt:
по ссылкам переменные передаются только через модификатор const, и чувствуют себя при этом прекрасно - а параметры функции те же самые локальные переменные в контексте этой самой функции

Нет, это не так. В функцию в качестве параметра может быть передана неконстантная ссылка.

zorgg комментирует...

Не совсем понятно, на кой черт это нужно. На практике

string f() { return "abc"; }

...

string a = f();

будет работать в точности так же. Да?

hVostt комментирует...

2Алёна: кажется речь шла о временных переменных. Или с каких-то пор разрешено временные переменные передавать в функции по неконстантным сслыкам? :) У меня не получается. VC 7/8/9.

а еще, зачем так много кода? можно выразить и одной строчкой:

const int& i = 5;

hVostt комментирует...

[b]zorgg[/b] обычно константностью не злоупотребляют, обычно как раз наоборот - её игнорируют.

а работать ваш код будет так как и ожидается :) т.е. копирование по значению (если не обращать внимание на особенности реализации std::string).

zorgg комментирует...

Вы ошибаетесь.

Создайте класс с копирующим конструктором, добавьте в него (в к-конструктор, а не в класс) отладочный вывод, попробуйте.

Подавляющее большинство компиляторов будет генерировать код, работающий напрямую с lvalue.

Я сегодня обсуждал это со своим наставником, по его словам коммитет принял, после длительного обсуждения, решение, при котором компилятор может опускать копирующий конструктор, если ему так нужно. Вывод - избегать сайд-эффектов в копирующих конструкторах.

zorgg комментирует...

Что бы не быть голословным,

#include <stdio.h>

class Supergrass
{
int a;
public:
Supergrass(int a):a(a) {puts("ctor.");}
~Supergrass(){puts("dtor.");}
Supergrass(const Supergrass &a) {puts("copy.");}
};

Supergrass func()
{
return Supergrass(42);
}

int main()
{
puts("before decl.");
Supergrass x = func();
puts("after decl.");
return 0;
}

Если компилятор не дурак, то вывод будет следуюющим:

before decl.
ctor.
after decl.
dtor.

Что и требовалось.

Adept комментирует...

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

Алёна комментирует...

2zorgg:
компилятор может опускать копирующий конструктор, если ему так нужно

Угу, return value optimization. Там только не "если нужно", в Стандарте (12.8/15) есть жесткие ограничения когда комплиятор может такую оптимизацию делать.

будет работать в точности так же. Да?

Нет. Я помедитировала над Стандартом, поспрашивала Гугл и вот что получается.

В примере Саттера
const string& s = f();

продлевается время жизни временной переменной в стеке.

В твоем примере
string a = f();
временная переменная не создается вовсе. Компилятор видит, что возвращаемый функцией f локальный объект используется при инициализации и он создает этот возращаемый объект прямо в a. Поэтому и не вызывается конструктор копирования. Тут вообще никакого копирования не происходит, даже побитового.

2hVostt
кажется речь шла о временных переменных.

Я привела точную цитату из твоего комментария и отвечала на нее.


а еще, зачем так много кода? можно выразить и одной строчкой:

const int& i = 5;


Изначально вопрос был такой: есть пример кода, корректен ли он.

Алёна комментирует...

2Adept:
Продлевать жизнь объекта при бинде его на константную ссылку стандарт не обязывает

В 12.2/5 приводится пример, где говорится следующее

The temporary T3 bound to the reference cr is destroyed
at the end of cr’s lifetime, that is, at the end of the program.

так что обязывает, имхо.

zorgg комментирует...

2Алёна: Да я просто пытаюсь понять, зачем это вообще может быть нужно. Какое-либо вменяемое практическое применение.

Kodt комментирует...

2zorgg:

Польза в том, что тип ссылки контравариантен типу значения. Что позволяет играть с полиморфизмом (не упоминая фактический тип).

const base& x = make_derived();

Причём этот полиморфизм - времени компиляции, а не только времени исполнения. То есть, нет накладных расходов.

Этакое auto для бедных.

Где это используется: в основном, там, где make_... перегружено.

Например,

struct scoped_base {
operator bool() const { return false; }
};

template<class Mutex>
struct scoped_lock_t : scoped_base
{
Mutex& m_;
scoped_lock_t(Mutex& m) : m_(m) { take(m_); }
~scoped_lock_t() { give(m_); }
};
template<class Mutex>
scoped_lock_t<Mutex> scoped_lock(Mutex& m)
{ return scoped_lock_t<Mutex>(m); }

#define LOCKED(m) \
if(scoped_base& sb = scoped_lock(m)) {} else

.....
LOCKED(the_critical_section)
{
.....
}

LOCKED(the_semaphore)
{
.....
}

Алёна комментирует...

2zorgg:
Да я просто пытаюсь понять, зачем это вообще может быть нужно. Какое-либо вменяемое практическое применение.

Вот пример, который привел Саттер:
ScopeGuard, написанный Александреску.

Adept комментирует...

Алёна:
В 12.2/5 приводится пример, где говорится следующее

The temporary T3 bound to the reference cr is destroyed
at the end of cr’s lifetime, that is, at the end of the program.

На самом дле это означает лишь то, что некий временный объект будет существовать до конца времени жизни cr, но это может быть и копия другого временного объекта. Даже если компилятор в действительности не создаст копии, по стандарту мы всё равно обязаны обеспечить возможность вызова копирующего конструктора (The constructor that would be used to make the copy shall be callable whether or not the copy is actually done - 8.5.3/5)

R-Team Funs комментирует...

В свое время с удивлением обнаружил, что все классы исключений из stdexcept принимают в конструкторе const std::string&, а не "честный" std::string; То есть следует полагать, что объект класса исключения не копирует строку к себе.

Этот факт заставил меня писать что-то вроде этого:
if (badSituation) {
static string s = "Bad situation";
throw std::logic_error(s);
}

Исходя из этой статьи получается, что
if (badSituation) {
throw std::logic_error("Bad situation");
}
тоже будет вполне корректно....

А вот если нужно строку прежде сформировать (допустим, записать пару интов), то все-равно прийдется париться с обеспечением адекватного времени жизни =( .

Анонимный комментирует...

От чего const не сохраняет объект в этом случае?

std::ostringstream oss;
oss << "AAA";
const char* const& tmp = oss.str().c_str();

//далее tmp содержит невалидное значение

Kodt комментирует...

Отчего const не оставляет объект?
А отчего бы ему оставлять.

oss.str() возвращает временный объект, время жизни которого - до точки с запятой.

Далее, берём оттуда указатель на внутренние данные этого объекта.

Сам объект никак не продлеваем. Вот и наступает "фиаско".

А вот если бы написать

string const& s = oss.str();
char const* tmp = s.c_str();

то здесь первым пунктом мы продлеваем время жизни временного объекта - и можем пользоваться значением tmp столько, сколько живы внутренние данные.

Анонимный комментирует...

Видимо работает только для объектов, т.к. такой вариант не работает:

int main()
{
std::string str1 = "AAA";
std::string str2 = "BBB";
char const* const & cstr = (str1 + str2).c_str();
printf("\n%s\n", cstr);
return 0;
}

Kodt комментирует...

2Аноним
Видимо работает только для объектов, т.к. такой вариант не работает:

Это какая-то вера в магические заклинания "iddqd" и "const&"?

Вариант работает. Но не так, как вам хочется.

Поскольку (s1+s2) - это временный объект, то .c_str() возвращает указатель на временные данные.
Дальше - сам указатель остаётся жить, а данные помирают.

С равным успехом можно было взять итератор - (s1+s2).begin() - очень даже объект какого-то там класса.
Но этот объект не озадачивается глубоким копированием данных, связанных с ним.
И указатель не озадачивается.

Tubrik комментирует...

В следующем коде что не так?

#include <iostream>
#include <map>

typedef std::map<int, int> mapIntT;
mapIntT MapInt;

int main()
{
MapInt.insert(std::pair<int, int>(1, 2));
mapIntT::iterator const& cIt = --MapInt.end();
if (cIt->second == cIt->first)
std::cout << "equal\n";
return 0;
}

В данном случае итератор - это объект. Он должен сохраняться, поскольку ссылка создается константная, или я чего-то недопонимаю?

Kodt комментирует...

"Поскольку ссылка константная" не является причиной для продлевания жизни. Ещё раз: "const&" не IDDQD.

Давайте посмотрим, что происходит в коде --MapInt.end()

MapInt.end() возвращает rvalue - итератор на конец.

С++ позволяет вызывать неконстантные методы у rvalue; вот если бы автодекремент был внешней функцией, то здесь мы получили бы ошибку.

Итак, вызывается
iterator& iterator::operator--()
{.....; return *this; }
который возвращает неконстантную ссылку на самого себя.

Но компилятору-то уже пофиг, что это был временный объект. Сказано вернуть ссылку, нате.

Таким образом,
iterator const& clt = --MapInt.end();
неконстантная ссылка превращается в константную.

Дальше мы выходим за пределы полного выражения, временный объект погибает, ссылка инвалидируется.

Отчасти проблему могут решить ссылки-на-rvalue (из стандарта C++0x).
Для этого итератор должен иметь два оператора автодекремента
iterator& operator--(iterator&);
iterator&& operator--(iterator&&);
(Я ещё не вкурил в новый стандарт, как это добро сделать членами; а на внешних функциях - вот так).

Kodt комментирует...

AlenaC++, может быть, перевести дискуссию из гостевухи в форум? Уж больно неудобно обсуждать код в "комментариях", изначально неприспособленных для этого.

Предлагаю родную для меня площадку - RSDN. http://www.rsdn.ru, форум С++.

Алёна комментирует...

2Kodt:

AlenaC++, может быть, перевести дискуссию из гостевухи в форум? Уж больно неудобно обсуждать код в "комментариях", изначально неприспособленных для этого.

Предлагаю родную для меня площадку - RSDN. http://www.rsdn.ru, форум С++.


Эээ... Переносите :-)
А от меня какие действия требуются?

Kodt комментирует...

Собственно, никаких :)
Просто было бы невежливо с моей стороны сделать это в обход хозяина блога.
---
Приглашаю всех заинтересованных участников продолжить обсуждение на RSDN, с любезного согласия Алёны.
http://www.rsdn.ru/Forum/?mid=3191155

zmeysa комментирует...

а такой код валиден?

int& f() { int a = 1; return a; }

void g() {
const int& b = f();
cout << b << endl;
}

Алёна комментирует...

2 zmeysa:

а такой код валиден?

int& f() { int a = 1; return a; }


Меня напрягает уже вот эта строка, поскольку мы здесь пытаемся вернуть ссылку на локальную переменную.

Comeau на это выдает вполне ожидаемый warning.

warning: returning reference to local variable

Анонимный комментирует...

Алён, а вот интересно, ты сама как думаешь, что такое ссылка в стеке, если ссылка вообще не объект по стандарту? ;)
Кроме того, насколько я понял стандарт, дело не в способе размещения(стек тут не причем), а в константности ссылки. Вот из стандарта(2003) пример(12.5/5):
[Example:
class C {
// ...
public:
C();
C(int);
friend C operator+(const C&, const C&);
˜C();
};
C obj1;
const C& cr = C(16)+C(23);
C obj2;

the expression C(16)+C(23) creates three temporaries. A first temporary T1 to hold the result of the
expression C(16), a second temporary T2 to hold the result of the expression C(23), and a third tempo-
rary T3 to hold the result of the addition of these two expressions. The temporary T3 is then bound to the
reference cr. It is unspecified whether T1 or T2 is created first. On an implementation where T1 is cre-
ated before T2, it is guaranteed that T2 is destroyed before T1. The temporaries T1 and T2 are bound to
the reference parameters of operator+; these temporaries are destroyed at the end of the full expression
containing the call to operator+. The temporary T3 bound to the reference cr is destroyed at the end of
cr’s lifetime, that is, at the end of the program. In addition, the order in which T3 is destroyed takes into
account the destruction order of other objects with static storage duration. That is, because obj1 is con-
structed before T3, and T3 is constructed before obj2, it is guaranteed that obj2 is destroyed before T3,
and that T3 is destroyed before obj1. ]

В общем смысл статьи Саттера в том, что константная ссылка продливает жизнь временного объекта, а неконстантную ссылку на временный объект создавать нельзя(ибо он rvalue).
А на счет стековой ссылки это он погорячился...

Д.К.

Алёна комментирует...

2Анонимный:

Кроме того, насколько я понял стандарт, дело не в способе размещения(стек тут не причем), а в константности ссылки.

По-моему, стек там упоминается по отношению к объекту, жизнь которого продлевается, а не по отношению к ссылке.

elwood комментирует...

Недавно столкнулся с проблемой, которая может возникнуть, если в класс, который передается в вызывающую функцию по ссылке, добавить оператор преобразования вида operator T& (). Пусть он ничего не делает и возвращает *this. Если класс Test содержит такой оператор, то GCC компилирует вызов функции

Test someFunc()
{
return Test();
}

{
printf("Begin\r\n");
const Test& test = someFunc();
printf("End\r\n");
}

следующим образом (выводятся диагностические сообщения) :

Begin
Default constructor
operator Test&
Copy constructor
Destructor
operator Test&
Destructor
End

Visual C++ компилирует это иначе и программа выведет :

Begin
Default constructor
End
Destructor

Если отбросить лишние вызовы конструкторов копий / деструкторов (спишем это на отсутствие RVO в gcc), то остается главное отличие :
в Visual C++ переменная жива до выхода ссылки из области видимости, а GCC уничтожает её сразу.

Пытался поискать описание такой ситуации в Стандарте 98, но, к сожалению, не нашел.

Так какому же из компиляторов верить ?

Лично мне кажется, что GCC себя ведет здесь неверно, поскольку в данном случае user-defined оператор T& ничем не отличается от генерируемого по умолчанию, но семантика вызова после добавления этого оператора преобразования коренным образом меняется.

Kodt комментирует...

2 elwood

Во-первых, здесь мы видим, что gcc не смог выполнить Return Value Optimization, а VC смог.

Во-вторых, это действительно интересный вопрос: если в классе определён пользовательский оператор тривиального приведения типа - нужно ли пользоваться им (gcc), или забить (VC), или выдать ошибку?

Нужно будет на RSDN спросить. И в стандарте порыться.

Kodt комментирует...

Comeau, кстати, выдал варнинг о том, что тривиальный пользовательский оператор никогда не будет вызван.
Так что он здесь солидарен с VC.
Почему же gcc пошёл по другому пути - это загадка.

Kodt комментирует...

Во, кстати, на RSDN дискуссия трёхлетней давности про оператор приведения к самому себе:
http://rsdn.ru/forum/cpp/1865794.aspx

Прав комо, и причину он указывает в предупреждении:

"ComeauTest.c", line 4: warning: "self::operator self &()" will not be called for
implicit or explicit conversions
operator self & () { return (*this); }

Причина:

12.3.2/1
...
A conversion function is never used to convert a (possibly cv-qualified)
object to the (possibly cv-qualified) same object type (or a reference to it),
...

Vsevolod Shorin комментирует...

Обидно, что при этом

const B& b = foo().b();

всё же не эквивалентно

const A& a = foo();
const B& a = a.b();

al.zatv комментирует...

А правильно ли делать вот так:

const Obj &myFunc()
{
Obj x;
return x;
}

? То есть, это функция, возвращающая константную ссылку на временный объект.

Alena комментирует...

al.zatv

А правильно ли делать вот так:

const Obj &myFunc()
{
Obj x;
return x;
}

? То есть, это функция, возвращающая константную ссылку на временный объект.


Зависит от того что понимать под "правильно". Синтаксис валидный.
Но нет гарантий, что пользователь будет создавать ссылку на стеке, компилятор на такой код выдаст предупреждение. Во всяких разных книгах по С++ не раз говорится "никогда не возвращайте ссылку на временный объект".

Да и приведенный в статье путь трудно назвать "правильным". Это интересный с теоретической точки зрения хак, который я бы не стала использовать в промышленном коде.

Kodt комментирует...

Так неважно же, где пользователь сохранит ссылку. После выхода из функции ссылка на локальный объект инвалидируется. И в чём здесь хак? В том, чтобы получить ссылку на свежеосвобождённый участок стека?
Для стрельбы в ногу и низкоуровневой отладки, разве что.

Анонимный комментирует...

Поясните пожалуйста как обходить следующий момет?

class first
{
public:
int i;
public:
first &operator=(const first &in_This)
{
i = in_This.i;
return(*this);
}
};

class obj
{
public:
first mF;
public:
obj()
{
}
obj(obj &inThis)
{
mF = inThis.getM();
}
~obj()
{
}

first &getM(first &out_m)
{
out_m = mF;
return(out_m);
}
const first &getM(void)
{
return(mF);
}
obj &setM(const first &in_m)
{
mF = in_m;
return(*this);
}
};

int _tmain(int argc, _TCHAR* argv[])
{
setlocale( LC_ALL, ".1251" );

obj o;
obj k;
first f;
k.setM(o.getM());
k.setM(o.getM(f));

obj d(k);
}

Все работает, но если что логично изменим функцию так:
obj(const obj &inThis)
{
mF = inThis.getM();
}
Все ломается.

Спасибо...

Kodt комментирует...

Исправить это элементарно.
first const& obj::getM(void) const

что логично, поскольку getM ни меняет объект, ни отдаёт на сторону права на изменение.

Андрей Лихачев комментирует...

Прокоментируйте этот код
http://stackoverflow.com/questions/20476669/g-segmentation-fault-when-enabling-optimization
Почему здесь ломается?

Kodt комментирует...

На StackOverflow уже ответили.
Expression k в конструкторе захватывает ссылку на временный объект, который разрушается сразу после завершения конструктора.
Дальнейшее - неопределённое поведение, которое в условиях оптимизации может выглядеть вообще как угодно.