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

в 8:32, , рубрики: c++, c++0x, c++11, метки: ,

Бегло просматривая новый стандарт 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 результат этого выражения никому больше не нужен, даже если выражение является именованным объектом, объект можно смело перемещать.

Важно заострить внимание на словах «больше не нужен». Разве можно гарантировать, что после 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()
{
    std::string s = "Hi!";
    Finalizer fin(s);
    return s;
}

Компилятор 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.

Автор: vScherba


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js