c++ - 通过值传递参数时的奇怪行为

标签 c++ gcc visual-c++

偶然发现几篇文章声称,如果函数仍然可以复制,按值传递可以提高性能。

我从未真正想到过如何在后台实现值(value)传递。当您执行如下操作时,堆栈上到底发生了什么:F v = f(g(h()))?

经过深思熟虑后,我得出的结论是我将以实现g()返回的值的方式在f()期望的位置创建它的方式来实现它。因此,基本上,没有复制/移动构造函数调用-f()将简单地获取g()返回的对象的所有权,并在执行离开f()的范围时销毁它。与g()相同-它将获取h()返回的对象的所有权并在返回时销毁它。

las,编译器似乎不同意。这是测试代码:

#include <cstdio>

using std::printf;

struct H
{
    H() { printf("H ctor\n"); }
    ~H() { printf("H dtor\n"); }
    H(H const&) {}
//    H(H&&) {}
//    H(H const&) = default;
//    H(H&&) = default;
};

H h() { return H(); }

struct G
{
    G() { printf("G ctor\n"); }
    ~G() { printf("G dtor\n"); }
    G(G const&) {}
//    G(G&&) {}
//    G(G const&) = default;
//    G(G&&) = default;
};

G g(H) { return G(); }

struct F
{
    F() { printf("F ctor\n"); }
    ~F() { printf("F dtor\n"); }
};

F f(G) { return F(); }

int main()
{
    F v = f(g(h()));
    return 0;
}

在MSVC 2015上,其输出正是我所期望的:
H ctor
G ctor
H dtor
F ctor
G dtor
F dtor

但是,如果您将副本构造函数注释掉,它看起来像这样:
H ctor
G ctor
H dtor
F ctor
G dtor
G dtor
H dtor
F dtor

我怀疑删除用户提供的副本构造函数会导致编译器生成move-constructor,这反过来会导致不必要的“移动”,无论有多大的对象都不会消失(尝试添加1MB数组作为成员变量)。即编译器非常喜欢“移动”,因此选择它而不是根本不做任何事情。

这似乎是MSVC中的错误,但是我真的很希望有人能解释(和/或证明)这里发生的事情。这是第一个问题。

现在,如果您尝试使用GCC 5.4.0,则输出毫无意义:
H ctor
G ctor
F ctor
G dtor
H dtor
F dtor

必须在创建F之前将H销毁! H对于g()的作用域是局部的!请注意,此处使用构造函数对GCC的影响为零。

与MSVC相同-对我来说似乎是个错误,但是有人可以解释/证明这里发生了什么吗?那就是问题2。

真是愚蠢的是,经过多年与C++的专业合作,我遇到了这样的问题……经过近四十年的时间,编译器仍然不能就如何传递值达成共识?

最佳答案

为了通过值传递参数,参数是函数的局部变量,并且已从相应的参数初始化为函数调用。

按值返回时,有一个值称为返回值。这由return表达式的“参数”初始化。它的生存期一直到包含函数调用的完整表达式结束为止。

还有一种称为copy elision的优化可以在少数情况下应用。其中两种情况适用于按值(value)返回:

  • 如果返回值由相同类型的另一个对象初始化,则两个对象都可以使用相同的内存位置,并且跳过了复制/移动步骤(确切的条件是何时允许或不允许)
  • 如果调用代码使用返回值来初始化相同类型的对象,则返回值和目标对象都可以使用相同的存储位置,并且跳过复制/移动步骤。 (此处“相同类型的对象”包括功能参数)。

  • 这两种可能同时应用。同样,从C++ 14开始,复制省略对于编译器是可选的。

    在调用f(g(h()))中,这是对象列表(不带复制省略):
  • H默认由return H();构造
  • H的返回值
  • h()从(步骤1)开始复制构造。
  • ~H(步骤1)
  • H的参数
  • g从(步骤2)开始复制构造。
  • G默认由return G();构造
  • 的返回值
  • G从(第5步)开始复制构造。
  • g()(第5步)
  • ~G(第4步)(参见下文)
  • ~H的参数
  • G从(步骤6)开始复制构造。
  • f默认由F构造
  • return F();的返回值
  • F从(步骤10)开始移动构造。
  • f()(步骤10)
  • ~F(第9步)(参见下文)
  • ~G从(步骤11)开始移动构建。
  • F v~F~G(步骤2、6、11)已销毁-我认为这三个
  • 不需要排序
  • ~H(步骤14)

  • 对于复制省略,可以将步骤1 + 2 + 3合并为“默认构造~F的返回值”。对于5 + 6 + 7和10 + 11 + 12同样。但是,也可以将2 + 4单独组合为“h()的参数是从1复制构造的”,并且还可以同时应用这两个元素,给出“g的参数是默认构造的”。

    由于复制省略是可选的,因此您可能会从不同的编译器看到不同的结果。这并不意味着存在编译器错误。您会很高兴听到在C++ 17中某些复制省略方案被强制执行。

    如果您包含move-constructor的输出文本,则在第二种MSVC情况下的输出将更具指导性。我猜想在第一种MSVC情况下,它同时执行了我上面提到的两个省略,而第二种情况则省略了“2 + 4”和“6 + 9”省略。

    之下的:gcc和clang延迟破坏函数参数,直到封闭函数调用的完整表达式结束。这就是为什么您的gcc输出与MSVC不同的原因。

    在C++ 17的起草过程中,这些破坏是发生在我将其包含在列表中的位置,还是出现在完整表达式的末尾,由实现定义。可以争辩说,在较早发布的标准中对它的定义不够充分。 See here有待进一步讨论。

    关于c++ - 通过值传递参数时的奇怪行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39803711/

    相关文章:

    c++ - 将文件内容的字符对读入数组

    c++ - 尽管满足 C++ 条件,但 if 语句仍未解析

    c - 堆栈保护器适用于 strcpy() 示例但不适用于 gets() 示例

    c++ - 在 Code::Blocks 中调试似乎不起作用 - 缺少调试符号

    gcc - 与 libtcmalloc ubuntu 链接

    c++ - 如何将.lib 导入.dll?

    c++ - 函数返回类型样式

    c++ - 使用 OpenCV 计算物体的面积

    c++ - 基于ATL的CWindowImpl的Direct2D桌面窗口类模板

    c++ - Visual Studio C++ : Refactoring between member and non-member functions