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 ]