2

I am trying to get copy elision to work for fields of the object that is to be returned.

Example code:

#include <iostream>

struct A {
    bool x;
    A(bool x) : x(x) {
        std::cout << "A constructed" << std::endl;
    }
    A(const A &other) : x(other.x) {
        std::cout << "A copied" << std::endl;
    }
    A(A &&other) : x(other.x) {
        std::cout << "A moved" << std::endl;
    }
    A &operator=(const A &other) {
        std::cout << "A reassigned" << std::endl;
        if (this != &other) {
            x = other.x;
        }
        return *this;
    }
};

struct B {
    A a;
    B(const A &a) : a(a) {
        std::cout << "B constructed" << std::endl;
    }
    B(const B &other) : a(other.a) {
        std::cout << "B copied" << std::endl;
    }
    B(B &&other) : a(other.a) {
        std::cout << "B moved" << std::endl;
    }
    B &operator=(const B &other) {
        std::cout << "B reassigned" << std::endl;
        if (this != &other) {
            a = other.a;
        }
        return *this;
    }
};

B foo() {
    return B{A{true}};
}


int main() {
    B b = foo();
    std::cout << b.a.x << std::endl;
}

I compile with: g++ -std=c++17 test.cpp -o test.exe

output:

A constructed
A copied
B constructed
1

B is constructed in-place. Why is A not? I would at least expect it to be move-constructed, but it is copied instead.

Is there a way to also construct A in-place, inside the B to be returned? How?

PEC
  • 593
  • 1
  • 5
  • 16
  • 1
    Try enabling your compilers optimizer if you want it to be clever and produce fast code (like `g++ -std=c++17 -O3 ...`). Debug builds (the default) are unoptimized and focus on providing a good debug experience, *not* on making the code run fast. – Jesper Juhl Jan 15 '20 at 17:27

3 Answers3

5

Constructing a B from an A involves copying the A - it says so in your code. That has nothing to do with copy elision in function returns, all of this happens in the (eventual) construction of B. Nothing in the standard allows eliding (as in "breaking the as-if rule for") the copy construction in member initialization lists. See [class.copy.elision] for the handful of circumstances where the as-if rule may be broken.

Put another way: You get the exact same output when creating B b{A{true}};. The function return is exactly as good, but not better.

If you want A to be moved instead of copied, you need a constructor B(A&&) (which then move-constructs the a member).

Max Langhof
  • 23,383
  • 5
  • 39
  • 72
4

You will not succeed at eliding that temporary in its current form.

While the language does try to limit the instantiation ("materialisation") of temporaries (in a way that is standard-mandated and doesn't affect the as-if rule), there are still times when your temporary must be materialized, and they include:

[class.temporary]/2.1: - when binding a reference to a prvalue

You're doing that here, in the constructor argument.

In fact, if you look at the example program in that paragraph of the standard, it's pretty much the same as yours and it describes how the temporary needn't be created in main then copied to a new temporary that goes into your function argument… but the temporary is created for that function argument. There's no way around that.

The copy to member then takes place in the usual manner. Now the as-if rule kicks in, and there's simply no exception to that rule that allows B's constructor's semantics (which include presenting "copied" output) to be altered in the way you were hoping.

You can check the assembly output for this, but I'd guess without the output there will be no need to actually perform any copy operations and the compiler can elide your temporary without violating the as-if rule (i.e. in the normal course of its activities when creating a computer program, from your C++, which is just an abstract description of a program). But then that's always been the case, and I guess you know that already.

Of course, if you add a B(A&& a) : a(std::move(a)) {} then you move the object into the member instead, but I guess you know that already too.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
1

I have figured how to do what I wanted.

The intent was to return multiple values from a function with the minimal amount of "work".

I try to avoid passing return values as writable references (to avoid value mutation and assignment operators), so I wanted to do this by wrapping the objects to be returned in a struct.

I have succeeded at this before, so I was surprised that the code above didn't work.

This does work:

#include <iostream>

struct A {
    bool x;
    explicit A(bool x) : x(x) {
        std::cout << "A constructed" << std::endl;
    }
    A(const A &other) : x(other.x) {
        std::cout << "A copied" << std::endl;
    }
    A(A &&other) : x(other.x) {
        std::cout << "A moved" << std::endl;
    }
    A &operator=(const A &other) {
        std::cout << "A reassigned" << std::endl;
        if (this != &other) {
            x = other.x;
        }
        return *this;
    }
};

struct B {
    A a;
};

B foo() {
    return B{A{true}};
}


int main() {
    B b = foo();
    std::cout << b.a.x << std::endl;
}

output:

A constructed
1

The key was to remove all the constructors of B. This enabled aggregate initialization, which seems to construct the field in-place. As a result, copying A is avoided. I am not sure if this is considered copy elision, technically.

PEC
  • 593
  • 1
  • 5
  • 16
  • See my answer. You removed the temporary materialisation, because you were no longer binding it to a reference (ctor arg). Basically, there _is no temporary_ any more. That's not an optimisation: it's baked into the actual meaning of your program. This is a fine solution (as long as `B` doesn't need more stuff) – Lightness Races in Orbit Jan 15 '20 at 17:58