воскресенье, 20 июня 2010 г.

Кое-что про ссылки


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

Разбирая библиотечный код на работе, наткнулся на явно ошибочный код, который на удивление нормально работал. Во втором прочтении было решено, что это отклонение компилятора от стандарта. А на самом деле оказалось вполне легальной особенностью языка. 

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

Obj func();
const Obj &o = func();

Большинство плюсистов со стажем сразу увидят здесь проблему: объект, возвращаемый функций func(), должен быть разрушен, как только выражение полностью вычислено. Однако, ссылка на этот объект продолжает жить на протяжении всей области видимости, потенциально позволяя коду работать с разрушенным объектом. Вы поймете мое удивление, когда увидите, что код выше компилируется и исполняется корректно msvc, gcc и online comeau.

Рассмотрим подробный пример:

#include <iostream>

class A
{
public:
    A(const std::string &name)
    : m_name(name)
    {
        std::cout << "created " << m_name << std::endl;
    }

    ~A()
    {
        std::cout << "deleted " << m_name << std::endl;
    }

    void say_hello() const
    {
        std::cout << "hello from " << m_name << std::endl;
    }

private:
    std::string m_name;
};

int main()
{
    A("nameless").say_hello();

    A a = A("a");
    a.say_hello();

    const A &b = A("b");
    b.say_hello();

    return 0;
}

При исполнении он выведет на консоль следующее:

created nameless
hello from nameless
deleted nameless
created a
hello from a
created b
hello from b
deleted b
deleted a

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

Далее, если попробовать в первичном коде убрать квалификатор const, то код вообще перестанет компилироваться!

Obj func();
Obj &o = func();

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

void incr(int &i) 
    ++i; 

void f() 
    unsigned x = 3; 
    incr(x); 

При разборе этого примера под микроскопом, видно, что при вызове функции incr(), для x неявно создастся временный объект типа unsigned int, который и будет передан в функцию. Неудивительно, что после вызова значение x не изменится.

Вот тебе, бабушка, и C++.

7 комментариев:

  1. Последний пример по-моему очень странный, в С++ же ссылки - это, прошу прощения за тавтологию, официальный способ передачи параметров по ссылке?

    ОтветитьУдалить
  2. @alex: да, последний пример немного не в тему, просто это единственный осмысленный пример опасности присваивания не-конст ссылкам, который мне удалось найти.

    поясню еще раз, в чем опасность: на деле вызов incr(x) разворачивается в

    unsigned y = x;
    incr(y)

    в итоге, после incr(x) значение x не меняется, что выглядит очень странно, учитывая, что incr(x) суть ++x.

    ОтветитьУдалить
  3. Ну, первое я знал. А вот с передачей временного параметра -- это засада!

    ОтветитьУдалить
  4. @Alex: RE: первое я знал
    надеюсь, ты в продакщене такое не используешь? :)

    ОтветитьУдалить
  5. Те в случае, если бы в последнем примере функция была бы определена как void incr(const int &i), то копирования бы не было?

    ОтветитьУдалить
  6. @alexk: не-не-не :) копирования бы не было, если бы типы совпадали полностью:

    void incr(int &) и int x

    или void incr(unsigned &) и unsigned x

    повторюсь, этот пример просто показывает опасность потенциального использования неконстантных ссылок на временные объекты.

    ОтветитьУдалить
  7. > надеюсь, ты в продакщене такое не используешь? :)

    не, вроде таким извратом не страдал :)

    ОтветитьУдалить