- PVSM.RU - https://www.pvsm.ru -

Return by value и const variables в C++11

Во многих языках программирования существует возможность объявлять объекты и переменные константными. И, соответственно, существуют рекомендации делать так, если Вы не собираетесь менять их значения. С приходом нового стандарта, в С++ появилась рекомендация возвращать объекты из функций по значению, потому что даже без RVO [1] можно повысить производительность программы, за счет использования семантики перемещения. Что же будет, если использовать эти две рекомендации вместе: вернуть константный объект по значению? Попробуем разобраться далее.

Просматривая видеозаписи с недавно прошедшей конференции С++Now я наткнулся на один интересный момент (подводный камень). В конце одного из выступлений докладчик приводит следующий код:

struct s
{
    ...
};

s f()
{
    const s r;
    ...;
    return r;
}

и спрашивает, что произойдет с объектом r при возврате из функции?

Предположим, что у структуры s есть и конструктор копирования, и конструктор перемещения, а функция f содержит код, препятствующий применению RVO. Что же произойдет с объектом r при возврате из функции? Если объект создан без спецификатора const, то при возврате из функции он будет перемещен, а если создан с constскопирован. Подобное поведение замечено в GCC и в Clang (в Visual Studio не проверял). Что это: баг или ожидаемое поведение? Если объект сразу же будет уничтожен, почему не переместить его?

Со времен С++03 многие из нас привыкли к тому, что временные объекты и константы — это почти одно и тоже. И это логично, ведь поведение и тех, и других было схоже: они позволяли вызывать только функции, принимающие аргументы по значению или константной ссылке (или просто присваивать свои значения таким типам):

void f1(int a) { }
void f2(int& a) { }
void f3(const int& a) { }
int g() { return 0; }

int main()
{
    f1(g()); // OK
    f2(g()); // compile error
    f3(g()); // OK

    const int a = 0;
    f1(a); // OK
    f2(a); // compile error
    f3(a); // OK
}

В C++11 ситуация изменилась: теперь мы можем изменять временные объекты с помощью rvalue ссылок. Но не константы. Согласно стандарту, любое изменение объектов, объявленных со спецификатором const (будь то применение const_cast или извращений с указателями), приводит к неопределенному поведению (undefined behavior). Получается, что если бы компилятор сгенерировал код, использующий конструктор перемещения, то привел бы программу в состояние неопределенного поведения, что недопустимо. Иными словами, const-correctness стоит у компилятора в более высоком приоритете, чем подобная оптимизация. Все-таки С++ — это строго типизированный язык и любые неявные приведения ссылок и указателей от const типа к обычному — запрещены, например: const A a -> A&&.

Рассмотрим следующий код:

void f1(int& a) { }
void f2(const int& a) { }

int main()
{
    int a1 = 0;
    f1(a1); // OK

    int a2 = 0;
    f2(a2); // OK

    const int ca1 = 0;
    f1(ca1); // compile error

    const int ca2 = 0;
    f2(ca2); // OK
}

Стандарт С++ не позволяет неявно снимать квалификаторы с объектов. Если некоторый тип А можно привести как к A&, так и к const A&, то const A можно привести только к const A&. Теперь заменим lvalue ссылки на rvalue ссылки, добавив вызовы std::move при передачи параметров в функции… Именно! Мы не можем приводить const A a к A&&, но можем к const A&&. Если написать такой конструктор, то компилятор использует его для возврата константных переменных из функций, однако смысла в этом небольше, чем от обычного конструктора копирования. Поэтому такой тип ссылок обычно не используется.

Заключение

Как оказывается, некоторые рекомендуемые практики не сочетаются. В С++ постоянно приходится то тут, то там следить, как бы не запнуться об очередной подводный камень. Теперь нужно следить еще и за константами, чтобы они не усложнили Вам жизнь. Ведь никто не застрахован от такого кода (и даже если Вы рассчитываете на RVO и думаете, что данный случай Вам не важен, то можете потратить много времени на поиски причины, почему не компилируется этот код, если в структуре s нет копирующего конструктора):

struct s
{
    ...
};

s foo();

s bar()
{
    const auto r = foo();
    if (r.check_something())
        throw std::exception();
    return r;
}

И даже в этом коде есть копирование объекта (еще один подводный камень):

s bar()
{
    auto&& r = foo();
    ...;
    return r;
}

Предпочитайте такой вариант:

s bar()
{
    auto r = foo();
    ...;
    return r;
}

Или, только если Вы дорожите каждым конструктором перемещения:

s bar()
{
    auto&& r = foo();
    ...;
    return std::move(r);
}

Примечание

Не путайте предыдущий пример со следующим кодом, возвращающим rvalue ссылку:

s&& bar()
{
    auto&& r = foo();
    ...;
    return std::move(r);
}

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

И да пребудет с Вами Сила!

Автор: Thekondr

Источник [2]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/c-3/36623

Ссылки в тексте:

[1] RVO: http://en.wikipedia.org/wiki/Return_value_optimization

[2] Источник: http://habrahabr.ru/post/183454/