Pull to refresh

Замечание по move semantics при операторе return в C++11

Reading time3 min
Views16K
Бегло просматривая новый стандарт C++11, решил углубить свое понимание в теме rvalue references. Все, в принципе, замечательно, но есть подводные камни, а именно: некоторая потеря обратной совместимости с С++03.

Стандарт позволяет компилятору (но не обязывает его) рассматривать выражение, передаваемое оператору return как rvalue reference и реализовывать move semantics, даже если оно не является временным объектом. Например:
std::string f()
{
    std::string s = "Hi!";
    return s;
}

Здесь, до return s была lvalue, а return воспринимает ее уже как rvalue reference. И это замечательно, т.к. это дает хороший прирост производительности при возврате тяжелых объектов, происходит не копирование, а перемещение внутреннего состояния строки. Попробуем разобраться, почему стандарт позволяет оператору return рассматривать любое выражение, результат которого живет на области стека функции, как rvalue reference? Ответ очевиден: да потому что после return результат этого выражения никому больше не нужен, даже если выражение является именованным объектом, такой объект является xvalue (xPiring value), можно смело перемещать.

Важно заострить внимание на словах «больше не нужен». Разве можно гарантировать, что после return и до возврата из функции никакой код не будет вызван? Для C можно (SEH и __try...__finally не является стандартом), для C++ нельзя. После выполнения return и до возврата из функции будут вызваны деструкторы автоматических объектов, начиная с текущей области видимости, заканчивая областью видимости функции.

// Класс инициализируется ссылкой на строку, деструктор выводит ее в консоль.
struct Finalizer
{
    Finalizer(std::string const& str)
        :_str(str)
    {
    }
 
    ~Finalizer()
    {
        std::cout << _str << std::endl;
    }
private:
    std::string const& _str;
};
 
std::string f()
{
    // ломаем NRVO (проверял только на MSVC 10.0)
    if (false)
        return std::string("Заведомо невыполнимый код!");
    //-------------------------------------------
 
    std::string s = "Hi!";
    Finalizer fin(s);
    return s;
}

Первые 2 строчки фиктивного кода нужны для невозможности применить оптимизацию NRVO (для MSVC, других компиляторов под рукой нет для проверки, уверен обойти NRVO в вашем любимом компиляторе тоже будет несложно). Компилятор C++03 соберет код, в котором функция f выведет строку «Hi!», а С++11 (как уже все догадались) выведет пустую строку, потому что return утащил содержимое строки s.

Я проводил тесты на MSVC 9.0 (c++03) и 10.0 (частичный c++11). Ваш компилятор С++11 теоретически может и выводить строку «Hi!» в моем примере. Дело в том, что в большинстве компиляторов используется оптимизация хранения строк небольшой длины. Для этого класс std::string хранит внутри себя небольшой фиксированный буфер, и, если строка помещается в него, то куча не используется, а значит и перемещать нечего, и строку-донор теоретически можно не менять, причем это будет даже немного производительнее. В таком случае, сделайте строку приветствия длиннее. Такое поведение станет еще более неожиданным во время выполнения, когда короткие строки выводятся, а длинные нет. Это теоретически. Я с ходу не заметил в стандарте требований по реализации move semantics для std::string, обязана ли строка-донор выполнять постусловие s.empty() == true? Если есть такое — поправьте меня.


Пример надуманный, но простой для рассмотрения. На практике я не сталкивался еще ни разу с такой проблемой переноса кода на новый стандарт. Могу лишь предположить, что группа риска — это код, использующий BOOST_SCOPE_EXIT (удобная штука, приходилось использовать ее несколько раз), который, как раз, и предназначен для выполнения финального кода через вызов деструктора класса, который (класс) инициализирован ссылками на локальные переменные. Да и рукописные финализаторы я тоже имел честь видеть.

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

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

Всем приятного перехода на C++11.

UPD: включил в пример фиктивный код, который позволяет обойти NRVO для MSVC.
Tags:
Hubs:
Total votes 41: ↑40 and ↓1+39
Comments25

Articles