РЕДАКТИРОВАТЬ: Рассмотрим 2 следующих примера:
std::string x;
{
std::string y = "extremely long text ...";
...
x = y; // *** (1)
}
do_something_with(x);
struct Y
{
Y();
Y(const Y&);
Y(Y&&);
... // many "heavy" members
};
struct X
{
X(Y y) : y_(std::move(y)) { }
Y y_;
}
X foo()
{
Y y;
...
return y; // *** (2)
}
В обоих примерах y
на линиях (1) и (2) близок конец своего срока службы и вот-вот будет уничтожен. Кажется очевидным, что его можно рассматривать как значение и перемещать в обоих случаях. В (1) его содержимое может быть перемещено в x
и в (2) во временный экземпляр X().y_
,
Мои вопросы:
1) Будет ли оно перенесено в любой из приведенных выше примеров?
а) Если да, то в соответствии с каким стандартным положением.
(б) Если нет, то почему? Это стандартное упущение или есть еще одна причина, о которой я не думаю?
2) Если ответ выше — НЕТ. В первом примере я могу изменить (1) на x = std::move(y)
заставить компилятор выполнить перемещение. Что я могу сделать во втором примере, чтобы указать компилятору, что y
можно переместить? return std::move(y)
?
NB: я намеренно возвращаю экземпляр Y
и не X
в (2), чтобы избежать (N) RVO.
Для вашего первого примера ответ явно «нет». Стандарт дает разрешение компилятору на различные свободы в отношении копий (даже с побочными эффектами) при работе с возвращаемым значением из функции. Я полагаю, в конкретном случае std::string
компилятор может «знать», что ни копирование, ни перемещение не имеют побочных эффектов, поэтому он может заменить одно другим на основании правила «как будто». Если, однако, у нас было что-то вроде:
struct foo {
foo(foo const &f) { std::cout << "copy ctor\n"; }
foo(foo &&f) { std::cout << "move ctor\n"; }
};
foo outer;
{
foo inner;
// ...
outer = inner;
}
…правильно работающий компилятор должен генерировать код, который выводит «copy ctor», а не «move ctor». Для этого в действительности нет конкретной ссылки — скорее, есть цитаты, в которых говорится об исключениях для возвращаемых значений из функций, которые здесь не применяются, потому что мы не имеем дело с возвращаемым значением из функции.
Что касается того, почему никто не имеет дело с этим: я думаю, это просто потому, что никто не беспокоится. Возврат значений из функций происходит достаточно часто, поэтому стоит потратить немало усилий на его оптимизацию. Создание нефункционального блока и создание значения в блоке, который вы продолжаете копировать в значение вне блока, чтобы сохранить его видимость, случается достаточно редко, и кажется маловероятным, что кто-либо написал предложение.
Этот пример является по крайней мере, возвращая значение из функции — поэтому мы должны посмотреть на особенности исключений, которые позволяют перемещать вместо копий.
Здесь правило (N4659, § [class.copy.elision] / 3):
В следующих контекстах инициализации копирования вместо операции копирования может использоваться операция перемещения:
[…]
- Если выражение в операторе возврата (9.6.3) является (возможно, заключенным в скобки) идентификатором-идентификатором, который присваивает имя объекту с автоматической продолжительностью хранения, объявленной в теле или в параметре-объявлении-объявлении самой внутренней включающей функции или лямбда-выражения,
Разрешение перегрузки для выбора конструктора для копии сначала выполняется так, как если бы объект был обозначен значением r. Если первое разрешение перегрузки не удалось или не было выполнено, или если тип первого параметра выбранного конструктора не является rvalue-ссылкой на тип объекта (возможно, cv-квалифицированный), разрешение перегрузки выполняется снова, рассматривая объект как именующий.
Выражение (y
) в вашем операторе return есть id-выражение, которое именует объект с автоматической продолжительностью хранения, объявленной в теле самой внутренней включающей функции, поэтому компилятор должен выполнить двухэтапное разрешение перегрузки.
Тем не менее, в данный момент он ищет конструктор для создания X
из Y
, X
определяет один (и только один) такой конструктор — но этот конструктор получает его Y
по значению. Поскольку это единственный доступный конструктор, он «побеждает» в разрешении перегрузки. Так как он принимает свой аргумент по значению, тот факт, что мы впервые попробовали обработать разрешение перегрузки y
как значение не имеет никакого значения, потому что X
не имеет ctor правильного типа, чтобы получить его.
Сейчас, если мы определили X
что-то вроде этого:
struct X
{
X(Y &&y);
X(Y const &y);
Y y_;
}
…затем двухэтапное разрешение перегрузки имело бы реальный эффект — даже если y
обозначает lvalue, первый раунд разрешения перегрузки обрабатывает его как rvalue, поэтому X(Y &&y)
будет выбран и использован для создания временного X
который возвращается — то есть, мы получили бы ход вместо копии (хотя y
является lvalue, и у нас есть конструктор копирования, который принимает ссылку lvalue).
Почему бы просто не использовать outer
За все? Или, если работа с функцией передать в &outer
, Затем внутри функции работают с *inner
,