Может ли возврат заключенного в фигурные скобки инициализатора привести к копии на С++?

Пример:

struct s { int a; };

s func() { return {42}; }

int main() {
    s new_obj = func(); // line 6
    (void) new_obj;
    return 0;
}

Это работает. Что произойдет, если мы предположим, что наш компилятор не выполняет RVO?

  1. func возвращает структуру s, поэтому {42} нужно преобразовать в s, затем вернуть и, наконец, скопировать в new_obj в строке 6.
  2. func возвращает список инициализаторов, поэтому глубокая копия невозможна.

Что говорит язык? Можете ли вы привести доказательство?

Примечание. Я знаю, что в этом примере это не кажется полезным, но для возврата очень больших std::arrays постоянного размера я не хочу полагаться на RVO.


person Johannes    schedule 06.02.2014    source источник
comment
«это может быть полезно знать» — за исключением того, что ваш компилятор действительно выполняет RVO.   -  person Konrad Rudolph    schedule 06.02.2014
comment
@KonradRudolph Что делать, если по каким-то причинам RVO нельзя использовать в одном примере. И даже если - я думал, что язык C++ не гарантирует RVO.   -  person Johannes    schedule 06.02.2014
comment
@KonradRudolph Разве он не должен перемещать объект вместо применения (или, возможно, пропуска) RVO?   -  person BЈовић    schedule 06.02.2014
comment
@BЈовић: Он перемещен семантически, но на самом деле RVO может прийти и оптимизировать его! Обратите внимание, что RVO — это функция компилятора, а move — это функция языка... и язык позволяет компиляторам выполнять RVO везде, где это возможно.   -  person Nawaz    schedule 06.02.2014
comment
если возможно, то будет RVO, если нет, то переместит объект, если невозможно, то скопирует его, в таком порядке   -  person Drax    schedule 06.02.2014
comment
@Drax Нет. Реализация не требуется для исключения копирования (RVO). И что вы имеете в виду под этим будет перемещаться объект, если это невозможно...? Если конструктор перемещения является закрытым или удаленным, программа имеет неправильный формат.   -  person MWid    schedule 06.02.2014
comment
@BЈовић Он должен двигаться, только если он не может применить RVO (как сказал Наваз, семантическое действие - это движение, но его можно опустить). Пропустить ход, очевидно, было бы лучше.   -  person Konrad Rudolph    schedule 06.02.2014
comment
@Nawaz и язык позволяют компиляторам выполнять RVO везде, где это возможно. Нет! Любая оптимизация разрешена только по правилу «как если бы». RVO в форме того, что спецификация называет копированием, является исключением из этого правила.   -  person MWid    schedule 06.02.2014
comment
@MWid: Как будто везде, где это возможно, действует правило против «как если бы»!   -  person Nawaz    schedule 06.02.2014
comment
@MWid Да, RVO не требуется, поэтому я сказал, возможно ли это. И если конструктор перемещения не реализован (не удален явно и не приватный), он вызовет конструктор копирования:)   -  person Drax    schedule 06.02.2014


Ответы (1)


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

#include <iostream>

struct foo {
    foo(int) {}
    foo(const foo&) { std::cout << "copy\n"; }
    foo(foo&&)      { std::cout << "move\n"; }
};

foo f() {
    //return 42;
    return { 42 };
}

int main() {
    foo obj = f();
    (void) obj;
}

При компиляции с gcc 4.8.1 с -fno-elide-constructors для предотвращения RVO вывод

move

Если в f используется оператор return без фигурных скобок, то вывод будет

move
move

При отсутствии RVO происходит следующее. f должен создать временный объект типа foo, назовем его ret, который будет возвращен.

Если используется return { 42 };, то ret прямо инициализируется значением 42. Таким образом, до сих пор не вызывался конструктор копирования/перемещения.

Если используется return 42;, то другой временный, назовем его tmp, напрямую инициализируется из 42, а tmp перемещается для создания ret. Следовательно, до сих пор был вызван один конструктор перемещения. (Обратите внимание, что tmp является значением r, а foo имеет конструктор перемещения. Если бы не было конструктора перемещения, то был бы вызван конструктор копирования.)

Теперь ret является значением r и используется для инициализации obj. Следовательно, конструктор перемещения вызывается для перемещения от ret к obj. (Опять же, в некоторых случаях вместо этого может быть вызван конструктор копирования.) Следовательно, происходит одно (для return { 42 };) или два (для return 42;) перемещения.

Как я уже сказал в своем комментарии к вопросу ОП, этот пост очень актуален: помощник построения make_XYZ, позволяющий RVO и вывод типов, даже если XZY имеет ограничение на некопирование. Особенно отличный ответ от Р. Мартиньо Фернандес.

person Cassio Neri    schedule 06.02.2014
comment
В случае списков инициализации в фигурных скобках ret является инициализированным списком копий! Если конструктор преобразования из int в foo объявлен как explicit, вы получите ошибку. Аналогично в случае return 42;. Это всегда инициализация копирования. - person MWid; 06.02.2014
comment
@MWid Ты прав. Кроме того, если во время инициализации списка требуется сужающее преобразование (например, return { 42.0 };), код имеет неправильный формат. Я просто хотел сосредоточиться на главном вопросе ОП (если/когда будут сделаны копии/перемещения) и оставил эти другие вопросы в стороне. - person Cassio Neri; 06.02.2014