【问题标题】:Constructor call sequence different on GCC and clangGCC和clang上的构造函数调用顺序不同
【发布时间】:2017-03-10 21:09:10
【问题描述】:

我有以下程序:

#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 中的错误吗?

【问题讨论】:

  • 从来没有编译器必须考虑是否有副作用的情况。

标签: c++ g++ c++14 clang++


【解决方案1】:

在 C++14 中,两个编译器都是正确的。来自 N4296 中的 [class.copy],我认为它接近 C++14:

当满足某些条件时,允许实现省略类的复制/移动构造 对象,即使为复制/移动操作选择了构造函数和/或对象的析构函数 有副作用。 [...] 这种省略复制/移动操作,称为复制省略,在以下情况下是允许的(其中 可以合并消除多个副本):
— 在函数中的 return 语句中 [...]
— 在 throw-expression (5.17) 中,[...]
当一个尚未绑定到引用 (12.2) 的临时类对象将被复制/移动时 对于具有相同 cv-unqualified 类型的类对象,可以通过以下方式省略复制/移动操作 将临时对象直接构造到省略复制/移动的目标中
— 当异常处理程序的异常声明 [...]

此声明:

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

完全符合第三个标准,因此编译器允许但不是必需忽略该复制/移动。因此,gcc 和 clang 都是正确的。请注意,如果您不想复制省略,可以添加标志-fno-elide-constructors


在 C++17 模式下,甚至不会有回避的动作。 [dcl.init] 中的初始化规则本身更改为:

如果目标类型是(可能是 cv 限定的)类类型:
— 如果初始化表达式是纯右值并且源类型的 cv 非限定版本相同 class 作为目标的类,初始化表达式用于初始化目标 目的。 [ 示例: T x = T(T(T())); 调用T 默认构造函数来初始化x——结束示例]

【讨论】:

    【解决方案2】:

    两者都不正确。这称为复制省略。正如@chris 在下面指出的,这只是 C++17 中必需的优化。更多详情请访问cppreference.com。 C++17之前的相关部分是:

    在以下情况下,允许编译器省略 类对象的复制和移动(C++11 起)构造函数,即使 复制/移动(C++11 起)构造函数和析构函数具有 observable 副作用。

    当一个未绑定到任何引用的无名临时对象将被移动时 或(C++11 起)复制到相同类型的对象中(忽略 顶级 cv 限定),复制/移动(C++11 起)被省略。 当那个临时的被建造时,它被直接建造在 否则它将被移动或(自 C++11 起)复制到的存储。 当无名临时变量是 return 语句的参数时, 这种复制省略的变体被称为 RVO,“返回值 优化”。

    【讨论】:

    • Clang 不正确。该文本旁边有一个 (since C++17),因为 C++17 引入了保证复制省略。注意 a) OP 是用 C++14 编译的; b) 这是一个新功能,在任何一个编译器(可能是 GCC 7 和 Clang 4.0,从内存中获取)上都没有得到很长时间的支持。在 C++17 之前,可能会发生复制省略,只是不能保证。
    • 啊,这解释了我为什么感到困惑。我认为它是可选的,我没有看到该摘录旁边的 C++17 标记。
    • @chris 根据您的 cmets 更新了我的答案。谢谢
    • 有没有办法告诉GCC不要执行这个优化?
    • @mkcms 我认为this answer 可能可以做到,但对于生产代码来说可能不是一个好主意。
    猜你喜欢
    • 2013-06-24
    • 2014-01-28
    • 2013-04-14
    • 2011-01-16
    • 1970-01-01
    • 2012-08-22
    • 2019-02-01
    • 2016-07-29
    相关资源
    最近更新 更多