Почему параметры по значению исключены из NRVO? - PullRequest
38 голосов
/ 15 мая 2011

Представьте себе:

S f(S a) {
  return a;
}

Почему нельзя использовать псевдоним a и слот возвращаемого значения?

S s = f(t);
S s = t; // can't generally transform it to this :(

Спецификация не разрешает это преобразование, если конструктор копирования S имеет побочные эффекты. Вместо этого требуется как минимум две копии (одна от t до a, одна от a до возвращаемого значения и другая от возвращаемого значения до s, и только последняя из них может быть исключена. Обратите внимание, что я написал = t выше, чтобы представить факт копирования t в f's a, единственную копию, которая все еще была бы обязательной при наличии побочных эффектов конструктора перемещения / копирования).

Почему это?

Ответы [ 6 ]

20 голосов
/ 07 марта 2012

Вот почему копирование не имеет смысла для параметров. Это действительно о реализации концепции на уровне компилятора.

Копировать elision работает, по сути, создавая возвращаемое значение на месте. Значение не копируется; он создан прямо в месте назначения. Вызывающий объект обеспечивает пространство для предполагаемого вывода, и, таким образом, в конечном счете вызывающий абонент обеспечивает возможность выбора.

Все, что внутренняя функция должна сделать для удаления копии, - это создать вывод в месте, предоставленном вызывающей стороной. Если функция может сделать это, вы получаете право на копирование. Если функция не может, тогда она будет использовать одну или несколько временных переменных для хранения промежуточных результатов, а затем скопировать / переместить это в место, предоставленное вызывающей стороной. Он все еще создается на месте, но создание вывода происходит с помощью копирования.

Таким образом, мир за пределами определенной функции не должен знать или заботиться о том, выполняет ли функция выбор. В частности, вызывающий функции не должен знать о том, как реализована функция. Это не делает ничего другого; сама функция решает, возможно ли исключение.

Хранение для параметров значения также предоставляется вызывающей стороной. Когда вы вызываете f(t), вызывающая сторона создает копию t и передает ее f. Аналогично, если S неявно конструируется из int, то f(5) создаст S из 5 и передаст его f.

Все это делает вызывающий . Вызываемый не знает и не заботится о том, что это переменная или временная переменная; ему просто дают место в памяти стека (или регистры или что-то еще).

Теперь запомните: copy elision работает, потому что вызываемая функция создает переменную непосредственно в выходном местоположении. Поэтому, если вы пытаетесь исключить возврат из параметра значения, тогда хранилище для параметра значения также должно быть самим хранилищем вывода . Но помните: это вызывающий , который обеспечивает эту память как для параметра, так и для вывода. И поэтому, чтобы исключить выходную копию, вызывающая сторона должна встроить параметр непосредственно в output .

Чтобы сделать это, теперь вызывающий должен знать, что вызываемая функция исключит возвращаемое значение, потому что он может только вставить параметр непосредственно в вывод, если параметр будет возвращен. Обычно это не возможно на уровне компилятора, потому что вызывающая сторона не обязательно имеет реализацию функции. Если функция встроенная, то, возможно, она может работать. Но в противном случае нет.

Поэтому комитет по С ++ не стал допускать такую ​​возможность.

3 голосов
/ 16 мая 2011

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

X foo();
X bar( X a ) 
{ 
   return a;
}
int main() {
   X x = bar( foo() );
}

Теоретически весь набор копий будет содержать оператор return в foo ($tmp1), аргумент a из bar, оператор return bar ($tmp2) и x in main. Компиляторы могут исключить два из четырех объектов, создав $tmp1 в расположении a и $tmp2 в расположении x. Когда компилятор обрабатывает main, он может заметить, что возвращаемое значение foo является аргументом для bar и может привести их к совпадению, в этот момент он не может знать (без встраивания), что аргумент и возврат bar - это один и тот же объект, и он должен соответствовать соглашению о вызовах, поэтому он поместит $tmp1 в позицию аргумента в bar.

В то же время он знает, что целью $tmp2 является создание только x, поэтому он может разместить оба по одному и тому же адресу. Внутри bar мало что можно сделать: аргумент a находится вместо первого аргумента в соответствии с соглашением о вызовах, а $tmp2 должен находиться в соответствии с соглашением о вызовах (в в общем случае в другом месте, подумайте, что пример может быть расширен до bar, который принимает больше аргументов, только один из которых используется в качестве оператора возврата.

Теперь, если компилятор выполняет встраивание, он может обнаружить, что дополнительная копия, которая потребуется, если функция не была встроена, действительно не нужна, и у него будет шанс ее исключить. Если стандарт позволит исключить эту конкретную копию, то один и тот же код будет иметь различное поведение в зависимости от того, встроена функция или нет.

1 голос
/ 17 мая 2011

Дэвид Родригес - dribeas ответ на мой вопрос 'Как разрешить копирование elision для классов C ++' дал мне следующую идею. Хитрость заключается в том, чтобы использовать лямбда-выражения для задержки оценки внутри тела функции:

#include <iostream>

struct S
{
  S() {}
  S(const S&) { std::cout << "Copy" << std::endl; }
  S(S&&) { std::cout << "Move" << std::endl; }
};

S f1(S a) {
  return a;
}

S f2(const S& a) {
  return a;
}

#define DELAY(x) [&]{ return x; }

template <class F>
S f3(const F& a) {
  return a();
}

int main()
{
  S t;
  std::cout << "Without delay:" << std::endl;
  S s1 = f1(t);
  std::cout << "With delay:" << std::endl;
  S s2 = f3(DELAY(t));
  std::cout << "Without delay pass by ref:" << std::endl;
  S s3 = f2(t);
  std::cout << "Without delay pass by ref (temporary) (should have 0 copies, will get 1):" << std::endl;
  S s4 = f2(S());
  std::cout << "With delay (temporary) (no copies, best):" << std::endl;
  S s5 = f3(DELAY(S()));
}

Это выводит на ideone GCC 4.5.1:

Без задержки:
Копировать
Копирование
С задержкой:
Копия

Теперь это хорошо, но можно предположить, что версия DELAY похожа на передачу по константной ссылке, как показано ниже:

Без задержки передать по реф.
Копия

Но если мы передадим временную ссылку на const, мы все равно получим копию:

Без задержки передать ref (временный) (должен иметь 0 копий, получит 1):
Копия

Если отложенная версия скрывает копию:

С задержкой (временная) (без копий, лучше):

Как видите, это исключает все копии во временном кейсе.

Отложенная версия производит одну копию во временном случае, и никаких копий в случае временного. Я не знаю другого способа добиться этого, кроме лямбды, но мне было бы интересно, если таковые будут.

1 голос
/ 15 мая 2011

От т до а нецелесообразно элидировать копию.Параметр объявлен изменяемым, поэтому копирование выполняется, поскольку ожидается, что он будет изменен в функции.

От значения a до возвращаемого значения я не вижу причин для копирования.Возможно, это какой-то недосмотр?Параметры по значению ощущаются как локальные внутри тела функции ... я не вижу никакой разницы.

0 голосов
/ 10 марта 2012

Я чувствую, потому что альтернатива всегда доступна для оптимизации:

S& f(S& a) { return a; }  // pass & return by reference
^^^  ^^^

Если f() закодировано, как указано в вашем примере, то вполне нормально предположить, что копия предназначена или ожидаются побочные эффекты; иначе почему бы не выбрать пропуск / возврат по ссылке?

Предположим, что если применяется NRVO (как вы просите), то между S f(S) и S& f(S&)!

нет никакой разницы

NRVO пинает в ситуациях, подобных operator +() ( пример ), потому что нет достойной альтернативы.

Один вспомогательный аспект, все функции ниже имеют различные способы копирования:

S& f(S& a) { return a; }  // 0 copy
S f(S& a) { return a; } // 1 copy
S f(S a) { A a1; return (...)? a : a1; }  // 2 copies

В 3-м фрагменте, если во время компиляции известно, что (...) равно false, то компилятор генерирует только 1 копию.
Это означает, что компилятор целенаправленно не выполняет оптимизацию, когда доступна тривиальная альтернатива.

0 голосов
/ 06 марта 2012

Я думаю, проблема в том, что если конструктор копирования что-то делает, компилятор должен делать это предсказуемое количество раз. Если у вас есть класс, который увеличивает счетчик каждый раз, когда он копируется, например, и есть способ получить доступ к этому счетчику, то совместимый со стандартами компилятор должен выполнить эту операцию определенное количество раз (в противном случае, как записать юнит тесты?)

Теперь, вероятно, плохая идея - написать такой класс, но задача компилятора не в том, чтобы это выяснить, а только для того, чтобы убедиться, что вывод правильный и непротиворечивый.

...