GCC 和 clang 上的构造函数调用顺序不同
Constructor call sequence different on GCC and clang
我有以下程序:
#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;
}
编译与执行:
[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]$
我知道编译器可以去掉一些构造函数调用,但不是只有在没有副作用的情况下才允许这样做吗?看起来Clang在这里是正确的,它是GCC中的错误吗?
两者都不正确。这称为复制省略。正如@chris 在下面指出的那样,这只是 C++17 中必需的优化。可以在 cppreference.com 上找到更多详细信息。 C++17 之前的相关部分是:
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".
在 C++14 中,两种编译器都是正确的。来自 N4296 中的 [class.copy],我认为它接近 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 [...]
此声明:
foo f{foo{foo{foo{}}}};
恰好满足第三个条件,因此编译器 允许 ,但 不需要 ,省略 copy/move.因此,gcc 和 clang 都是正确的。请注意,如果您不想复制省略,可以添加标志 -fno-elide-constructors
.
在C++17模式下,连省略的动作都没有。 [dcl.init] 中的初始化规则本身更改为:
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 ]
我有以下程序:
#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;
}
编译与执行:
[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]$
我知道编译器可以去掉一些构造函数调用,但不是只有在没有副作用的情况下才允许这样做吗?看起来Clang在这里是正确的,它是GCC中的错误吗?
两者都不正确。这称为复制省略。正如@chris 在下面指出的那样,这只是 C++17 中必需的优化。可以在 cppreference.com 上找到更多详细信息。 C++17 之前的相关部分是:
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".
在 C++14 中,两种编译器都是正确的。来自 N4296 中的 [class.copy],我认为它接近 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 areturn
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 [...]
此声明:
foo f{foo{foo{foo{}}}};
恰好满足第三个条件,因此编译器 允许 ,但 不需要 ,省略 copy/move.因此,gcc 和 clang 都是正确的。请注意,如果您不想复制省略,可以添加标志 -fno-elide-constructors
.
在C++17模式下,连省略的动作都没有。 [dcl.init] 中的初始化规则本身更改为:
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 theT
default constructor to initializex
. —end example ]