3

Consider this code:

#include <iostream>

struct S
{
    S(std::string s) : s_{s} { std::cout << "S( string ) c-tor\n"; }
    S(S const&) { std::cout << "S( S const& ) c-tor\n"; }
    S(S&& s) { std::cout << "S&& c-tor\n"; s_ = std::move(s.s_); }
    S& operator=(S const&) { std::cout << "operator S( const& ) c-tor\n";  return *this;}
    S& operator=(S&& s) { std::cout << "operator (S&&)\n"; s_ = std::move(s.s_); return *this; }
    ~S() { std::cout << "~S() d-tor\n"; }

    std::string s_;
};

S foo() { return S{"blaaaaa"}; }

struct A
{
    A(S s) : s_{s} {}

    S s_;
};

struct B : public A
{
    B(S s) : A(s) {}
};

int main()
{
    B b(foo());
    return 0;
}

When I compile it with g++ -std=c++1z -O3 test.cpp, I get the following output:

S( string ) c-tor
S( S const& ) c-tor
S( S const& ) c-tor
~S() d-tor
~S() d-tor
~S() d-tor

I'm wondering why there is no copy elision? I expect something more like this:

S( string ) c-tor
~S() d-tor

There is the same output when I compile it with -fno-elide-constructors

bladzio
  • 414
  • 3
  • 15
  • If I counted correctly, without elision there should be four copy-constructor calls, not the two you have. I suggest you run in a debugger, with a breakpoint on the copy-constructor, so you can see where it is called from. That might give you some hints about what is happening. – Some programmer dude Mar 08 '18 at 11:20

1 Answers1

5

Copy elision does happen for foo return value, as expected.

The other two copies happen in B and A constructors. Notice in the output that it calls S(S const&) twice, whereas one would expect to see at least one S(S&&) for B(foo()). This is because the compiler already eliminated those extra copies created with S(S&&). If you compile with -fno-elide-constructors you can see these 2 extra copies:

S::S(std::string)
S::S(S&&)
S::~S()
S::S(S&&)
S::S(const S&)
S::S(const S&)
S::~S()
S::~S()
S::~S()
S::~S()

Whereas without -fno-elide-constructors the output is:

S::S(std::string)
S::S(const S&)
S::S(const S&)
S::~S()
S::~S()
S::~S()

See copy initialization (the initialization used for function argument):

First, if T is a class type and the initializer is a prvalue expression whose cv-unqualified type is the same class as T, the initializer expression itself, rather than a temporary materialized from it, is used to initialize the destination object: see copy elision.

You can avoid the remaining two copies by accepting by reference:

struct A
{
    A(S&& s) : s_{std::move(s)} {}
    S s_;
};

struct B : public A
{
    B(S&& s) : A(std::move(s)) {}
};

Output:

S( string ) c-tor <--- foo
S&& c-tor         <--- A::s_
~S() d-tor
~S() d-tor
Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
  • may it also be the case that copy constructors have the side effect of changing the state of `std::cout`? – Arda Aytekin Mar 08 '18 at 11:23
  • @ArdaAytekin Huh? – Maxim Egorushkin Mar 08 '18 at 11:25
  • sorry, nevermind. I got confused. Indeed, accepting `S` by value in `B` and `std::move`ing to `A` would also decrement the number of copies. It is purely because of the constructor signatures, as you have stated. the user explicitly wants to have pass by value... sorry, again, for the confusion. – Arda Aytekin Mar 08 '18 at 11:27
  • Maxim you said that copy elision happened for foo, so why I get the same output when I compile it with -fno-elide-constructors ? Moreover I still do not understand why compiler do not omit these 2 copies ? – bladzio Mar 08 '18 at 11:39
  • @bladzio On x86-64 Linux platform the ABI requires that the caller allocates space for the return value (with caveats). In this case `-fno-elide-constructors` may have 0 effect. See table 7 in http://www.agner.org/optimize/calling_conventions.pdf – Maxim Egorushkin Mar 08 '18 at 11:44
  • @bladzio Updated the answer for you. – Maxim Egorushkin Mar 08 '18 at 12:22
  • 1
    To clarify: when compiling in C++17 mode `-fno-elide-constructors` doesn't make any difference, because the elision is required, and can't be turned off. In C++14 mode the elision is optional, and is done by default but can be turned off with `-fno-elide-constructors`. Eliding the extra two copies done by `A(s)` and `s_{s}` is not permitted (either in C++14 or C++17) and so the compiler doesn't do it. – Jonathan Wakely Mar 08 '18 at 12:49
  • Maxim pls correct me if I misunderstood. If I pass rvalue ref to function expecting a value then copy or move will be always performed (according to copy initialization) ? will not be any elision ? – bladzio Mar 08 '18 at 13:44
  • Jonathan could you elaborate why it is not permitted ? – bladzio Mar 08 '18 at 13:46
  • @bladzio Yes, it creates a temporary during copy initialization, and this temporary can be elided. – Maxim Egorushkin Mar 08 '18 at 13:46