Мне известен тот факт, что присвоение rvalue для ссылки на const lvalue продлевает время жизни временных файлов до конца области действия. Однако мне не ясно, когда использовать это и когда полагаться на оптимизацию возвращаемого значения.
LargeObject lofactory( ... ) {
// construct a LargeObject in a way that is OK for RVO/NRVO
}
int main() {
const LargeObject& mylo1 = lofactory( ... ); // using const&
LargeObject mylo2 = lofactory( ... ); // same as above because of RVO/NRVO ?
}
Согласно Scot Meyers ‘More Effective C ++ (пункт 20), компилятор может оптимизировать второй метод для создания объекта на месте (который будет идеальным и именно того, чего вы пытаетесь достичь с помощью const&
в первом методе).
const&
Временам и когда положиться на RVO / NRVO?const&
метод хуже, чем не использовать его? (Я думаю, например, о C ++ 11 переместить семантику, если LargeObject
это реализовано …)Давайте рассмотрим самый простой случай:
lofactory( ... ).some_method();
В этом случае один экземпляр из lofactory к контексту вызывающего абонента возможно — но это может быть оптимизировано РВО / NRVO.
LargeObject mylo2 ( lofactory( ... ) );
В этом случае возможны копии:
const LargeObject& mylo1 = lofactory( ... );
В этом случае есть еще одна копия:
Ссылка будет привязана к этому временному.
Так,
Существуют ли общепринятые правила или лучшие практики, когда следует использовать const?& Временам и когда положиться на RVO / NRVO?
Как я уже говорил выше, даже в случае с const&
возможна ненужная копия, и она может быть оптимизирована РВО / NRVO.
Если ваш компилятор применяется РВО / NVRO в некоторых случаях, скорее всего, он будет выполнять копирование на этапе 2 (выше). Потому что в этом случае copy-elision намного проще, чем NRVO.
Но в худшем случае у вас будет одна копия для const&
регистр и две копии при инициализации значения.
Может ли быть ситуация, в которой с помощью const& метод хуже, чем не использовать его?
Я не думаю, что есть такие случаи. По крайней мере, если ваш компилятор не использует странные правила, которые различают const&
, (Для примера похожей ситуации я заметил, что MSVC не выполняет NVRO для агрегированной инициализации.)
(Я думаю, например, о семантике перемещения в C ++ 11, если в LargeObject реализована такая …)
В C ++ 11, если LargeObject
имеет семантику ходов, то в худшем случае у вас будет один ход для const&
случай, и два шага, когда вы начинаете значение. Так, const&
все еще немного лучше.
Так что хорошим правилом было бы всегда связывать временные& если возможно, так как это может помешать копированию, если компилятор по какой-то причине не выполнит копирование?
Без знания реального контекста приложения это кажется хорошим правилом.
В C ++ 11 можно привязать временную ссылку к rvalue — LargeObject&&, Таким образом, такой временный может быть изменен.
Кстати, семантическая эмуляция хода доступна в C ++ 98/03 с помощью различных приемов. Например:
Бьярне Страуструп описывает другой трюк, используя маленький изменяемый флаг внутри класса. Пример кода, который он упомянул Вот.
Однако даже при наличии семантики перемещения — есть объекты, которые нельзя недорого перенести. Например, матричный класс 4×4 с двойными данными [4] [4] внутри. Итак, Copy-elision RVO / NRVO по-прежнему очень важны, даже в C ++ 11. И, кстати, когда происходит Copy-elision / RVO / NRVO — это быстрее, чем двигаться.
П.С., в реальных случаях есть некоторые дополнительные вещи, которые следует учитывать:
Например, если у вас есть функция, которая возвращает вектор, даже если будет применяться Move / RVO / NRVO / Copy-Elision — она все равно может оказаться неэффективной на 100%. Например, рассмотрим следующий случай:
while(/*...*/)
{
vector<some> v = produce_next(/* ... */); // Move/RVO/NRVO are applied
// ...
}
Будет эффективнее изменить код на:
vector<some> v;
while(/*...*/)
{
v.clear();
produce_next( v ); // fill v
// or something like:
produce_next( back_inserter(v) );
// ...
}
Потому что в этом случае уже выделенная память внутри вектора может быть повторно использована, когда v.capacity () достаточно, без необходимости делать новые выделения внутри yield_next на каждой итерации.
Если вы напишите свой lofactory
класс как это:
LargeObject lofactory( ... ) {
// figure out constructor arguments to build a large object
return { arg1, arg2, arg3 } // return statement with a braced-init-list
}
В этом случае нет RVO / NRVO, это прямая конструкция. Раздел 6.6.3 стандарта гласит: «А return
заявление с приготовился-INIT-лист инициализирует объект или ссылку, которые будут возвращены из функции путем копирования-инициализации списка (8.5.4) из указанного списка инициализатора ».
Затем, если вы захватите свой объект с
LargeObject&& mylo = lofactory( ... );
копирование не будет, время жизни будет таким, как вы ожидаете, и вы можете изменить mylo.
И все без копирования нигде, гарантировано.