- PVSM.RU - https://www.pvsm.ru -
Во многих языках программирования существует возможность объявлять объекты и переменные константными. И, соответственно, существуют рекомендации делать так, если Вы не собираетесь менять их значения. С приходом нового стандарта, в С++ появилась рекомендация возвращать объекты из функций по значению, потому что даже без 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/
Нажмите здесь для печати.