2

I have the following program:

#include <iostream>

#define PRINT_LOCATION()\
    do { std::cout << __PRETTY_FUNCTION__ << "\n"; } while (false)

struct foo
{
    int val;

    foo()
        : val(1)
    {
        PRINT_LOCATION();
    }

    foo(const foo& other)
        : val(other.val * 2)
    {
        PRINT_LOCATION();
    }

    foo(foo&& other)
        : val(other.val * 2)
    {
        PRINT_LOCATION();
    }
};

int main()
{
    foo f{foo{foo{foo{}}}};

    std::cout << "value = " << f.val << "\n";

    if (f.val == 1)
        throw f;
}

Compilation and execution:

[mkc /tmp]$ g++ -Wall -Wextra -pedantic -std=c++14 -O0 -o a.out main.cpp
[mkc /tmp]$ ./a.out 
foo::foo()
value = 1
foo::foo(foo&&)
terminate called after throwing an instance of 'foo'
Aborted (core dumped)
[mkc /tmp]$ clang++ -Wall -Wextra -pedantic -std=c++14 -O0 -o a.out main.cpp
[mkc /tmp]$ ./a.out 
foo::foo()
foo::foo(foo &&)
foo::foo(foo &&)
value = 4
[mkc /tmp]$

I know that the compiler is allowed to remove some constructor calls, but isn't it only allowed to do it when there are no side effects? It looks like Clang is correct here, is it a bug in GCC?

Barry
  • 286,269
  • 29
  • 621
  • 977
mkcms
  • 262
  • 3
  • 9
  • It's never been the case that the compiler has to consider whether or not there are side effects. –  Mar 10 '17 at 21:30

2 Answers2

3

In C++14, both compilers are correct. From [class.copy] in N4296, which I think is close to C++14:

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects. [...] 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 [...]
— in a throw-expression (5.17), [...]
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
— when the exception-declaration of an exception handler [...]

This declaration:

foo f{foo{foo{foo{}}}};

precisely meets that third criteria, so the compiler is allowed to, but isn't required to, elide that copy/move. Hence, both gcc and clang are correct. Note that if you do not want copy elision, you can add the flag -fno-elide-constructors.


In C++17 mode, there would not even be a move to elide. The initialization rules themselves in [dcl.init] change to read:

If the destination type is a (possibly cv-qualified) class type:
— If the initializer expression is a prvalue and the cv-unqualified version of the source type is the same class as the class of the destination, the initializer expression is used to initialize the destination object. [ Example: T x = T(T(T())); calls the T default constructor to initialize x. —end example ]

Barry
  • 286,269
  • 29
  • 621
  • 977
1

Neither is incorrect. This is called copy elision. As @chris pointed out below, it is only a required optimization in C++17. More details can be found on cppreference.com. The relevant section prior to C++17 is:

Under the following circumstances, the compilers are permitted to omit the copy- and move- (since C++11)constructors of class objects even if copy/move (since C++11) constructor and the destructor have observable side-effects.

When a nameless temporary, not bound to any references, would be moved or (since C++11) copied into an object of the same type (ignoring top-level cv-qualification), the copy/move (since C++11) is omitted. When that temporary is constructed, it is constructed directly in the storage where it would otherwise be moved or (since C++11) copied to. When the nameless temporary is the argument of a return statement, this variant of copy elision is known as RVO, "return value optimization".

Kurt Stutsman
  • 3,994
  • 17
  • 23
  • 2
    Clang is not incorrect. That text has a *(since C++17)* beside it because C++17 introduces guaranteed copy elision. Note that a) The OP is compiling with C++14; and b) This is a new feature that has not been supported for very long on either compiler (possibly GCC 7 and Clang 4.0, going from memory). Before C++17, copy elision can happen, it's just not guaranteed. – chris Mar 10 '17 at 21:21
  • 1
    Ahh that explains why I was confused. I thought it was optional and I didn't see the C++17 tag next to that excerpt. – Kurt Stutsman Mar 10 '17 at 21:23
  • @chris Updated my answer based on your comments. Thanks – Kurt Stutsman Mar 10 '17 at 21:30
  • Is there a way to tell GCC not to perform this optimization? – mkcms Mar 10 '17 at 21:32
  • 1
    @mkcms I think [this answer](http://stackoverflow.com/a/8758168/459615) might be able to do it, but it's probably not a good idea for production code. – Kurt Stutsman Mar 10 '17 at 21:36