-5

Whover downvoted me, mind explaining why? I thought it was a legitimate question, and all the answers have been very helpful.

Theoretically, when I do MyClass a = b + c, it should first call const MyClass operator+, returning a const MyClass object, then call the assignment operator to create object a.

It seems like I would copy stuff twice when returning a object and calling the assignment operator. Is this optimized in the compiler? If yes, how? It seems more tricky if it involves casting.

Let's assume we are talking about g++, which is pretty much the golden standard of c++ compilers. [EDIT: ok, let's say most commonly used]

[EDIT:] Whoa, I didn't expect using const in return-by-value be criticized. I thought it was enouraged to use const when return-by-value for non-built-in types? I remember seeing it somewhere.

CuriousMind
  • 15,168
  • 20
  • 82
  • 120

4 Answers4

2

Copy-initialisation doesn't use the assignment operator, it uses the copy or move constructor. Since your operator foolishly returns a const object, moving is impossible, so it would use the copy constructor.

However, initialising an object from a temporary is one of the situations where copy elision is allowed, so any decent compiler should do that, initialising a directly as the return value instead of creating a temporary.

Mike Seymour
  • 249,747
  • 28
  • 448
  • 644
  • There was an argument once that for identical types of variable and initializer expressions it *has* to do copy elision, but I can't recall. Argh. – Cheers and hth. - Alf Apr 10 '15 at 13:44
  • Hi thanks for your comment. I have made some edits. I didn't expect using const in return-by-value be criticized. I thought it was enouraged to use const when return-by-value for non-built-in types? I remember seeing it somewhere – CuriousMind Apr 10 '15 at 13:52
  • @CodeNoob: Some people used to suggest it (so that weird code like `a+b=c` would fail to compile, instead of giving weird behaviour), but these days move semantics make it a bad idea, since you can't move from a constant object. – Mike Seymour Apr 10 '15 at 14:06
1

Most compilers will optimise this using copy-elision. The temporary created from calling MyClass::operator+ will be constructed directly into a rather than calling the copy constructor.

Also note that MyClass a = ... does not call the assignment operator, it calls the copy constructor. This is known as copy-initialization.

Have a look here for more information on copy elision.

Community
  • 1
  • 1
TartanLlama
  • 63,752
  • 13
  • 157
  • 193
0

There is an optimization called copy elision, described by the standard in §12.8/31:

This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

  • in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv- unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value

  • when a temporary class object that has not been bound to a reference (12.2) would be copied/moved to a class object with the same cv-unqualified type, the copy/move operation can be omitted by constructing the temporary object directly into the target of the omitted copy/move

Thus in the line

MyClass a = b + c;

The temporary object returned by operator+ is directly constructed into a, and no unnecessary copies/moves occur, not even in the return statement in operator+. A demonstration:

struct MyClass
{
    int i;

    MyClass operator+( MyClass const& m ) {
        MyClass r = m.i + i;
        return r;
    }

    MyClass(int i) : i(i) {std::cout << "Ctor!\n";}
    // Move ctor not implicitly declared, see §12.8/9
    MyClass(MyClass const&) {std::cout << "Copy-Ctor!\n";}
    ~MyClass() {std::cout << "* Dtor!\n";}
};

int main() {
    MyClass c{7}, b{3},
            a = b + c;
}

Output on any decent compiler:

Ctor!
Ctor!
Ctor!
* Dtor!
* Dtor!
* Dtor!

Live on Coliru

Columbo
  • 60,038
  • 8
  • 155
  • 203
0

To give a more direct idea of what you can expect, let's start with a simple class like this:

class Integer {
    int a;
    public:
    Integer(int a) : a(a) {}

    friend Integer operator+(Integer a, Integer b) {
        return Integer(a.a + b.a);
    }

    friend std::ostream &operator<<(std::ostream &os, Integer const &i) {
        return os << i.a;
    }
};

For the sake of demonstration, let's add a main that reads some data from the outside world, creates a couple of Integer objects, and then prints out the result of adding them. The inputs and outputs will come from the outside world, so the compiler can't pull too fancy of tricks, and optimize everything out.

int main(int argc, char **argv) {
    Integer a(atoi(argv[1])), b(atoi(argv[2]));
    Integer c = a + b;    // Line 20
    std::cout << c;
}

Note the line 20 there--it becomes important below.

Now, let's compile it up and see what code the compiler produces. Using VC++ we get this:

[ normal "stuff" to set up entry to main elided ]

; Line 19
    mov rcx, QWORD PTR [rdx+8]
    mov rdi, rdx
    call    atoi
    mov rcx, QWORD PTR [rdi+16]
    mov ebx, eax
    call    atoi
; Line 20
    lea edx, DWORD PTR [rax+rbx]
; Line 21
    call    ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@H@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<

So even though we've created two Integer objects and added them using an overloaded operator that constructs and returns a third Integer object, the compiler has seen through all our subterfuge, and "realized" that what we're doing is just reading a couple of ints using atoi, adding them together, and printing out the int we get as a result.

Having seen that, it eliminates the function call completely and doesn't call or return anything--it just reads two ints, adds them together, and prints out the result.

Using gcc the result is pretty much the same:

movq    8(%rbx), %rcx
call    atoi                    ; <--- get first item
movq    16(%rbx), %rcx
movl    %eax, %esi
call    atoi                    ; <--- get second item
movq    .refptr._ZSt4cout(%rip), %rcx
leal    (%rsi,%rax), %edx       ; <--- the addition
call    _ZNSolsEi               ; <--- print result

It's rearranged the code slightly, but ultimate does pretty much the same thing--all vestiges of our Integer class have disappeared.

Let's compare that to what we get without using a class at all:

int main(int argc, char **argv) {
    int a = atoi(argv[1]);
    int b = atoi(argv[2]);
    int c = a + b;
    std::cout << c;
}

Using VC++, this produces the following:

; Line 5
    mov rcx, QWORD PTR [rdx+8]
    mov rdi, rdx
    call    atoi
; Line 6
    mov rcx, QWORD PTR [rdi+16]
    mov ebx, eax
    call    atoi
; Line 7
    lea edx, DWORD PTR [rbx+rax]
; Line 8
    call    ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@H@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<

Other than the comments showing the line numbers from the original file, the code is precisely identical to what we got using the class.

I won't waste the space to copy and paste the result from doing the same with g++; it also produces identical code regardless of whether we use our class and overloaded operator to do the addition or not.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111