Ответ 1
В этом случае компилятор не может нарушить правило "как есть". Но вы можете использовать std::move
для достижения желаемого эффекта:
object3 a(x);
object2 b(std::move(a));
object1 c(std::move(b));
return c;
Иногда бывает разумно разделить сложные или длинные выражения на несколько шагов, например (вторая версия не более ясна, но это всего лишь пример):
return object1(object2(object3(x)));
может быть записана как:
object3 a(x);
object2 b(a);
object1 c(b);
return c;
Предполагая, что все 3 класса реализуют конструкторы, которые принимают значение rvalue в качестве параметра, первая версия может быть быстрее, поскольку временные объекты передаются и могут перемещаться. Я предполагаю, что во второй версии локальные переменные считаются lvalues. Но если переменные не используются позже, компиляторы С++ 11 оптимизируют код, поэтому переменные считаются значениями r, а обе версии работают одинаково? Меня больше всего интересует компилятор Visual Studio 2013 С++, но я также доволен тем, как компилятор GCC ведет себя в этом вопросе.
Спасибо, Михал
В этом случае компилятор не может нарушить правило "как есть". Но вы можете использовать std::move
для достижения желаемого эффекта:
object3 a(x);
object2 b(std::move(a));
object1 c(std::move(b));
return c;
Как сказал juanchopanza, компилятор не может (на уровне С++) нарушать правило "как-если"; то есть все преобразования должны генерировать семантически эквивалентный код.
Однако, за пределами уровня С++, когда код оптимизирован, могут возникнуть дополнительные возможности.
Таким образом, это действительно зависит от самих объектов: если у движений-конструкторов/деструкторов есть побочные эффекты, а (де) выделение памяти является побочным эффектом, то оптимизация не может произойти. Если вы используете только POD, с конструкторами-конструкторами/деструкторами по умолчанию, то он, вероятно, будет автоматически оптимизирован.
Но если переменные не используются позже, сделайте компиляторы С++ 11 оптимизированными код, поэтому переменные считаются значениями r и версии работают точно так же?
Возможно, но это сильно зависит от ваших типов. Рассмотрим следующий пример с типом POD point
:
#include <cstdio>
struct point {
int x;
int y;
};
static point translate(point p, int dx, int dy) {
return { p.x + dx, p.y + dy };
}
static point mirror(point p) {
return { -p.x, -p.y };
}
static point make_point(int x, int y) {
return { x, y };
}
int main() {
point a = make_point(1, 2);
point b = translate(a, 3, 3);
point c = mirror(b);
std::printf("(x,y) = (%d,%d)\n", c.x, c.y);
}
Я посмотрел на код сборки, вот в чем была скомпилирована вся программа (!) (поэтому приведенный ниже код является приближением С генерируемого кода сборки):
int main() {
std::printf("(x,y) = (-4,-5)\n");
}
Он не только избавился от всех локальных переменных, но и выполнил вычисления во время компиляции! Я пробовал как gcc, так и clang, но не msvc.
ОК, так что сделайте программу немного более сложной, чтобы она не могла выполнять вычисления:
int main(int argc, char* argv[]) {
int x = *argv[1]-'0';
int y = *argv[2]-'0';
point a = make_point(x,y);
point b = translate(a, 3, 3);
point c = mirror(b);
std::printf("(x,y) = (%d,%d)\n", c.x, c.y);
}
Чтобы запустить этот код, вы должны называть его как ./a.out 1 2
.
Вся эта программа сводится к этой (сборка, переписанная на C) после оптимизации:
int main(int argc, char* argv[]) {
int x = *argv[1]-'0';
int y = *argv[2]-'0';
std::printf("(x,y) = (%d,%d)\n", -(x+3), -(y+3));
}
Таким образом, он избавился от a, b, c
и всех функций make_point()
, translate()
и mirror()
и сделал как можно больше вычислений во время компиляции.
По причинам, указанным в Matthieu M. answer, не ожидайте такой хорошей оптимизации с более сложными типами (особенно не-POD).
По моему опыту, inlining имеет решающее значение. Трудно работать, чтобы ваши функции были легко встроены. Используйте оптимизацию времени ссылки.
Помните, что помимо семантики перемещения, которая может значительно ускорить ваш код, компилятор также выполняет (N) RVO - (Именованно) Оптимизацию возвращаемого значения, что может фактически повысить эффективность вашего кода. Я протестировал ваш пример и в g++ 4.8 кажется, что ваш второй пример может быть фактически более оптимальным:
object3 a(x);
object2 b(a);
object1 c(b);
return c;
Из моих экспериментов похоже, что он вызовет конструктор/деструктор 8 раз (1 ctr + 2 copy ctrs + 1 move ctr + 4 dtrs) по сравнению с другим методом, который вызывает его 10 раз (1 ctr + 4 move ctors + 5 dtors). Но по мере того как user2079303 прокомментировал, перемещение конструкторов должно по-прежнему превосходить конструкторы копирования, также в этом примере все вызовы будут встраиваться, поэтому служебные служебные вызовы функций не будут иметь места.
Копирование/перемещение elision на самом деле является исключением из правила "as-if", это означает, что иногда вы можете быть удивлены тем, что ваш конструктор/деструктор даже с побочными эффектами не вызван.
http://coliru.stacked-crooked.com/a/1ca7ebec0567e48f
(вы можете отключить (N) RVO с параметром -fno-elide-constructors)
#include <iostream>
#include <memory>
template<int S>
struct A {
A() { std::cout<<"A::A"<<std::endl; }
template<int S2>
A(const A<S2>&) { std::cout<<"A::A&"<<std::endl; }
template<int S2>
A(const A<S2>&&) { std::cout<<"A::A&&"<<std::endl; }
~A() { std::cout<<"~A::A"<<std::endl;}
};
A<0> foo () {
A<2> a; A<1> b(a); A<0> c(b); return c; // calls dtor/ctor 8 times
//return A<0>(A<1>(A<2>())); // calls dtor/ctor 10 times
}
int main()
{
A<0> a=foo();
return 0;
}