Во многих языках программирования существует возможность объявлять объекты и переменные константными. И, соответственно, существуют рекомендации делать так, если Вы не собираетесь менять их значения. С приходом нового стандарта, в С++ появилась рекомендация возвращать объекты из функций по значению, потому что даже без RVO можно повысить производительность программы, за счет использования семантики перемещения. Что же будет, если использовать эти две рекомендации вместе: вернуть константный объект по значению? Попробуем разобраться далее.
Просматривая видеозаписи с недавно прошедшей конференции С++Now я наткнулся на один интересный момент (подводный камень). В конце одного из выступлений докладчик приводит следующий код:
и спрашивает, что произойдет с объектом r при возврате из функции?
Предположим, что у структуры s есть и конструктор копирования, и конструктор перемещения, а функция f содержит код, препятствующий применению RVO. Что же произойдет с объектом r при возврате из функции? Если объект создан без спецификатора const, то при возврате из функции он будет перемещен, а если создан с const — скопирован. Подобное поведение замечено в GCC и в Clang (в Visual Studio не проверял). Что это: баг или ожидаемое поведение? Если объект сразу же будет уничтожен, почему не переместить его?
Со времен С++03 многие из нас привыкли к тому, что временные объекты и константы — это почти одно и тоже. И это логично, ведь поведение и тех, и других было схоже: они позволяли вызывать только функции, принимающие аргументы по значению или константной ссылке (или просто присваивать свои значения таким типам):
В C++11 ситуация изменилась: теперь мы можем изменять временные объекты с помощью rvalue ссылок. Но не константы. Согласно стандарту, любое изменение объектов, объявленных со спецификатором const (будь то применение const_cast или извращений с указателями), приводит к неопределенному поведению (undefined behavior). Получается, что если бы компилятор сгенерировал код, использующий конструктор перемещения, то привел бы программу в состояние неопределенного поведения, что недопустимо. Иными словами, const-correctness стоит у компилятора в более высоком приоритете, чем подобная оптимизация. Все-таки С++ — это строго типизированный язык и любые неявные приведения ссылок и указателей от const типа к обычному — запрещены, например: const A a -> A&&.
Рассмотрим следующий код:
Стандарт С++ не позволяет неявно снимать квалификаторы с объектов. Если некоторый тип А можно привести как к A&, так и к const A&, то const A можно привести только к const A&. Теперь заменим lvalue ссылки на rvalue ссылки, добавив вызовы std::move при передачи параметров в функции… Именно! Мы не можем приводить const A a к A&&, но можем к const A&&. Если написать такой конструктор, то компилятор использует его для возврата константных переменных из функций, однако смысла в этом не больше, чем от обычного конструктора копирования. Поэтому такой тип ссылок обычно не используется.
Как оказывается, некоторые рекомендуемые практики не сочетаются. В С++ постоянно приходится то тут, то там следить, как бы не запнуться об очередной подводный камень. Теперь нужно следить еще и за константами, чтобы они не усложнили Вам жизнь. Ведь никто не застрахован от такого кода (и даже если Вы рассчитываете на RVO и думаете, что данный случай Вам не важен, то можете потратить много времени на поиски причины, почему не компилируется этот код, если в структуре s нет копирующего конструктора):
И даже в этом коде есть копирование объекта (еще один подводный камень):
Предпочитайте такой вариант:
Или, только если Вы дорожите каждым конструктором перемещения:
Не путайте предыдущий пример со следующим кодом, возвращающим rvalue ссылку:
Никогда так не делайте, здесь возвращается ссылка на уже уничтоженный объект.
И да пребудет с Вами Сила!
Просматривая видеозаписи с недавно прошедшей конференции С++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);
}
Никогда так не делайте, здесь возвращается ссылка на уже уничтоженный объект.
И да пребудет с Вами Сила!