【问题标题】:Understanding function call in context of temporary objects了解临时对象上下文中的函数调用
【发布时间】:2014-12-17 02:28:28
【问题描述】:

看看这个简单的代码:

class A
{};

A f(A a)
{
    return a;
}

int main(void)
{
    A a;
    A b = f(a);
    return 0;
}

它创建一个局部变量a,调用一个函数f()并将其返回值分配给另一个变量b。但我想知道函数调用期间会发生什么。

谁能描述一下我,在这个过程中创建了哪些对象(临时或其他),调用了哪些构造函数、析构函数和赋值/移动运算符以及何时调用?

【问题讨论】:

    标签: c++ c++11 function-calls


    【解决方案1】:

    如有疑问,请使用Noisy 类:

    struct Noisy {
        Noisy() { std::cout << "Default construct" << std::endl; }
        Noisy(const Noisy&) { std::cout << "Copy construct" << std::endl; }
        Noisy(Noisy&&) { std::cout << "Move construct" << std::endl; }
        Noisy& operator=(const Noisy&) { std::cout << "Copy assignment" << std::endl; return *this; }
        Noisy& operator=(Noisy&&) { std::cout << "Move assignment" << std::endl; return *this; }
        ~Noisy() { std::cout << "Destructor" << std::endl; }
    };
    
    Noisy f(Noisy a) {
        return a;
    }
    
    int main(void) {
        Noisy a;
        Noisy b = f(a);
    }
    

    使用选项g++ -fno-elide-constructors -std=c++11 t.cc 使用 gcc-4.9.1 编译,给出输出:

    Default construct // 1. 'a' is default constructed.
    Copy construct    // 2. Local argument 'a' in function 'f' is copied.
    Move construct    // 3. Return value is move constructed (*see note below).
    Move construct    // 4. 'b' is move constructed from return value.
    Destructor        // 5. Local argument 'a' is destroyed.
    Destructor        // 6. Return value is destroyed.
    Destructor        // 7. 'b' is destroyed.
    Destructor        // 8. 'a' is destroyed.
    

    注意:即使本地参数a 是左值,编译器也知道它即将超出范围并将其视为右值。

    编译不带选项-fno-elide-constructors将启用编译器复制省略优化并产生输出:

    Default construct // 1. 'a' is default constructed.
    Copy construct    // 2. Local argument 'a' in function 'f' is copied.
    Move construct    // 3. 'b' is move constructed from argument 'a' (elision).
    Destructor        // 4. Local argument 'a' is destroyed.
    Destructor        // 5. 'b' is destroyed.
    Destructor        // 6. 'a' is destroyed.
    

    使用-std=c++03 编译,即 C++03 将导致所有移动都被替换为副本。

    有关复制省略的更多信息,请参见此处:What are copy elision and return value optimization?

    【讨论】:

    • 你确定优化案例中的“复制构造”是构造返回值而不是构造参数吗?据我了解,省略的构造是从临时返回值到b 的移动构造,通过让f 就地构造结果。
    • @MichaelKarcher 在过去的几分钟里,我自己一直在思考这件事。我认为你实际上是对的。将对此进行编辑。
    • @Snps 说“本地参数被破坏”而不是“返回值被破坏”不是更正确吗?
    • @Pradhan 是的,你是对的,我在之前的编辑中错过了那部分。感谢您指出!
    【解决方案2】:
    A f(A a)
    {
        return a;
    }
    
    A a;
    A b = f(a);
    
    1. 参数 (a) 使用相应的参数 (a) 进行复制初始化。这仅涉及复制构造函数。

    2. 临时返回值被复制初始化为a

    3. b 使用函数调用的返回值进行复制初始化。调用隐式定义的移动构造函数(因为初始值设定项是 (p)rvalue)。

    请注意,复制省略不适用于此处,因为在 return 语句中,它仅适用于不是函数(或 catch 子句)参数的变量。

    【讨论】:

    • 事实上,在符合 C++ 11 的编译器上,在第 2 步中,临时返回值是用 a 移动初始化的。请参阅我的标准报价答案。
    • @MichaelKarcher 没有移动初始化之类的东西。复制初始化指定语法、上下文和重载决议,而不是是否发生复制或移动。
    • 对不起,你是对的。在步骤 2 中,使用重载决议选择的移动构造函数对返回值临时值进行复制初始化(术语在 n3337 中的 [dcl.init]/14 中定义)。在这一点上明确提到了使用移动构造函数进行复制初始化的可能性。
    【解决方案3】:
    int main(void)
    {
        A a;        // creates `a` using default constructor of `A`
        A b = f(a); // initializes `b` using implicitly-defined
                    // move-constructor from temporary copy of `a` (see [1])
        /* where
            A f(A a) // gets copy of `A` object as argument
            {
                return a; // return-value is copy-initialized
            }
        */
        return 0;
    }
    

    [1] The implicit generation of move constructors

    【讨论】:

      【解决方案4】:

      假设优化器不简化流程:

      • 您编写的函数f 接受其参数a 的值。因此调用函数 f 会调用将参数设置为f,这意味着将main 函数的局部变量a 复制到用于将参数传递给f(临时对象)的堆栈空间中。该对象是使用复制构造函数创建的。
      • 由于 f 按值返回,所有典型的 C++ 实现都通过将指向 A 类型对象(尚未创建的对象)的 存储空间 的指针作为隐藏参数传递给 f .在最简单的情况下,这是在涉及函数调用的表达式时在 main 堆栈上创建的临时对象。
      • 现在f 的参数设置完毕,进入函数f。由于它的唯一语句是一个返回语句,f 唯一要做的就是将参数a 复制到调用者提供的存储中。这是使用移动构造函数完成的,但是您在此处遇到了语言规范中的一个极端情况,因此编译器行为可能会有所不同,您可能会得到一个副本。更多内容在最后一段中。
      • 构造返回值后,f 退出。 main 重新获得控制权并创建局部变量b,将f 返回的临时对象作为源。由于这个临时对象是一个右值,b 的初始化是使用移动构造完成的(在 C++11 上)。
      • 随着语句现在完成,临时对象(参数和返回值)被销毁。我觉得没有指定销毁顺序,但是如果指定了,会先最后构建,所以返回值会在参数之前销毁。

      通常,“移动省略”优化会应用于您的代码。 f 的隐藏参数没有给出临时对象的地址,该临时对象随后被构造成 b,而是 f 直接构造 int b

      (未命名的)返回值优化与给出的代码无关,因为返回语句不包含构造函数调用。

      命名的返回值优化也不适用。这种优化会将您返回的对象(即参数a)放在调用者为返回值提供的位置,因此可以避免复制/移动操作。在您编写的函数中,返回的对象是一个参数,因此编译器在编译f 时没有机会“将其放在返回值的位置”,因为机器调用约定规定了该对象的位置找到了。

      前面提到的语言规范中的极端情况(f 的返回值是移动构造还是复制构造的)植根于隐含的“std::move”,允许在 return 语句上移动。这在第 32 段的子句 [class.copy](n3337 中的 12.8)中指定。它指定如果允许省略副本(根据上一段),则给出作为副本源的 l 值(在这种情况下参数名称a) 被视为右值(即可以从中移动)。允许复制省略的标准在 12.8/31 中给出,其中包括其他标准列表:

      在具有类返回类型的函数中的 return 语句,当表达式是具有与函数 return 相同的 cv 非限定类型的非易失性自动对象(函数或 catch 子句参数除外)的名称时类型,可以通过将自动对象直接构造到函数的返回值中来省略复制/移动操作

      这其实就是命名返回值优化的定义!并且上面已经解释过,命名返回值优化是行不通的,因为参数和返回对象位于不同的空间,但现在让我们回到 [12.8/32] 并查看精确的措辞:

      当满足或将满足省略复制操作的条件时,除了源对象是函数参数的事实,并且要复制的对象由左值指定时,重载决策选择构造函数首先执行复制,就好像对象是由右值指定的一样

      "or would be meet with the fact that source object is a function parameter" 部分是我们的救命稻草。即使在命名返回值优化仅因为对象存储位置而不是因为语义约束而无法实现的情况下,它也可以将其视为 r 值。

      编辑:我称之为极端情况的原因丢失了:在 C++11 标准化过程中很晚才添加了救生条款,因此有一些部分符合的编译器不会隐式地从参数中移动.

      【讨论】:

        猜你喜欢
        • 2012-09-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-10-04
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多