Я пытаюсь понять, почему достаточно хороший компилятор C ++ 11 (clang) не оптимизирует этот код, и мне интересно, есть ли у кого-нибудь здесь мнения.
#include <iostream>
#define SLOW
struct A {
A() {}
~A() { std::cout << "A d'tor\n"; }
A(const A&) { std::cout << "A copy\n"; }
A(A&&) { std::cout << "A move\n"; }
A &operator =(A) { std::cout << "A copy assignment\n"; return *this; }
};
struct B {
// Using move on a sink.
// Nice talk at Going Native 2013 by Sean Parent.
B(A foo) : a_(std::move(foo)) {}
A a_;
};
A MakeA() {
return A();
}
B MakeB() {
// The key bits are in here
#ifdef SLOW
A a(MakeA());
return B(a);
#else
return B(MakeA());
#endif
}
int main() {
std::cout << "Hello World!\n";
B obj = MakeB();
std::cout << &obj << "\n";
return 0;
}
Если я запускаю это с #define SLOW
закомментировано и оптимизировано с -s
я получил
Hello World!
A move
A d'tor
0x7fff5fbff9f0
A d'tor
что ожидается.
Если я запускаю это с #define SLOW
включен и оптимизирован с -s
Я получил:
Hello World!
A copy
A move
A d'tor
A d'tor
0x7fff5fbff9e8
A d'tor
Что, очевидно, не так приятно. Итак, вопрос:
Почему я не вижу оптимизации NRVO, примененной в случае «МЕДЛЕННО»? Я знаю, что компилятору не требуется применять NRVO, но это может показаться довольно распространенным простым случаем.
В общем, я стараюсь поощрять код в стиле «SLOW», потому что мне гораздо легче отлаживать.
Ответ прост: потому что в этом случае не разрешается применять elision. Компилятору разрешено применять только в очень немногих и особых случаях применять разрешение на копирование. Цитата из стандарта 12.8 [class.copy] параграф 31:
… Такое исключение операций копирования / перемещения, называемое разрешением копирования, допускается при следующих обстоятельствах (которые могут быть объединены для удаления нескольких копий):
- в операторе return в функции с типом возврата класса, когда выражение является именем энергонезависимого автоматического объекта (кроме параметра функции или catch-clause) с тем же типом cv unqualified, что и тип возврата функции, Операция копирования / перемещения может быть опущена путем создания автоматического объекта непосредственно в возвращаемое значение функции
- […]
Очевидно, тип B(a)
не является A
то есть копирование не разрешено. Другие пули в том же пункте относятся к таким вещам, как throw
выражения, дубликаты из временной и декларации исключений. Ни один из них не применяется.
Копия, которую вы видите на медленном пути, вызвана не отсутствием RVO, а тем, что
в B (MakeA ()) «MakeA ()» является r-значением, но в B (a) «a» является l-значением.
Чтобы сделать это понятным, давайте изменим медленный путь, чтобы указать, где завершается MakeA ():
#ifdef SLOW
A a(MakeA());
std::cout << "---- after call \n";
return B(a);
#else
Выход:
Hello World!
---- after call
A copy
A move
A d'tor
A d'tor
0x7fff5a831b28
A d'tor
Что показывает, что в
A a(MakeA());
Таким образом, RVO действительно произошло.
Исправление, которое удаляет все копии:
return B(std::move(a));