среда, Январь 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.

26 комментария(ев):

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)