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?
-
It's never been the case that the compiler has to consider whether or not there are side effects.user2100815– user21008152017年03月10日 21:30:34 +00:00Commented Mar 10, 2017 at 21:30
2 Answers 2
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 areturnstatement 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 theTdefault constructor to initializex. —end example ]
Comments
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".