【问题标题】:Temporary objects - when are they created, how do you recognise them in code?临时对象——它们是什么时候创建的,你如何在代码中识别它们?
【发布时间】:2012-06-09 11:59:43
【问题描述】:

在 Eckel,第 1 卷,第 367 页

//: C08:ConstReturnValues.cpp
// Constant return by value
// Result cannot be used as an lvalue
class X {
   int i;
public:
   X(int ii = 0);
   void modify();
};

X::X(int ii) { i = ii; }

void X::modify() { i++; }

X f5() {
   return X();
}

const X f6() {
   return X();
}

void f7(X& x) { // Pass by non-const reference
   x.modify();
}

int main() {
   f5() = X(1); // OK -- non-const return value
   f5().modify(); // OK
// Causes compile-time errors:
//! f7(f5());
//! f6() = X(1);
//! f6().modify();
//! f7(f6());
} ///:~

为什么f5() = X(1) 成功了?这是怎么回事???

第一季度。当他这样做时X(1) - 这里发生了什么?这是一个构造函数调用 - 这不应该然后阅读X::X(1); 是类实例化 - 不是类 实例化类似:X a(1); 编译器如何确定什么 X(1) 是??我的意思是.. 名称装饰发生所以.. X(1) 构造函数 call 将转换为:globalScope_X_int 作为函数 名字..???

第二季度。肯定会使用一个临时对象来存储X(1) 的结果对象 创建然后将其分配给对象 f5() 返回 (这也将是一个临时对象)?鉴于 f5() 返回一个临时的 即将被丢弃的对象,他如何分配一个常量临时 到另一个常数临时???有人可以清楚地解释为什么: f7(f5()); 应该是一个恒定的临时而不是简单的旧 f5();

【问题讨论】:

  • X::X 指的是构造函数。这在表达式中是不允许的,而仅在某些形式的声明中是不允许的。构造函数总是被隐式调用。

标签: c++ temporary-objects


【解决方案1】:

以下是执行代码时实际发生的示例。我做了一些修改以澄清幕后的过程:

#include <iostream>

struct Object
{
    Object( int x = 0 ) {std::cout << this << ": " << __PRETTY_FUNCTION__ << std::endl;}
    ~Object() {std::cout << this << ": " << __PRETTY_FUNCTION__ << std::endl;}
    Object( const Object& rhs ){std::cout << this << ": " << __PRETTY_FUNCTION__ << " rhs = " << &rhs << std::endl;}
    Object& operator=( const Object& rhs )
    {
        std::cout << this << ": " << __PRETTY_FUNCTION__ << " rhs = " << &rhs << std::endl;
        return *this;
    }
    static Object getObject()
    {
        return Object();
    }
};

void TestTemporary()
{
    // Output on my machine
    //0x22fe0e: Object::Object(int) -> The Object from the right side of = is created Object();
    //0x22fdbf: Object::Object(int) -> In getObject method the Temporary Unnamed object is created
    //0x22fe0f: Object::Object(const Object&) rhs = 0x22fdbf -> Temporary is copy-constructed from the previous line object
    //0x22fdbf: Object::~Object() -> Temporary Unnamed is no longer needed and it is destroyed
    //0x22fe0f: Object& Object::operator=(const Object&) rhs = 0x22fe0e -> assignment operator of the returned object from getObject is called to assigne the right object
    //0x22fe0f: Object::~Object() - The return object from getObject is destroyed
    //0x22fe0e: Object::~Object() -> The Object from the right side of = is destroyed Object();

    Object::getObject() = Object();
}

您必须知道,在大多数现代编译器上都会避免复制构造。这是因为编译器进行了优化(返回值优化)。在我的输出中,我明确删除了优化以显示根据标准实际发生的情况。如果您也想删除此优化,请使用以下选项:

-fno-elide-constructors

【讨论】:

    【解决方案2】:

    我对答案并不完全满意,所以我看了一下:

    “更有效的 C++”,Scott Meyers。第十九条:“了解起源 临时对象”

    。关于布鲁斯·埃克尔对“临时”的报道, 好吧,正如我怀疑的那样,正如 Christian Rau 直接指出的那样,这很简单 错误的!呸!他(埃克尔的)把我们当作豚鼠! (这将是一个 一旦他纠正了所有的错误,对于像我这样的新手来说是一本好书)

    Meyer:“C++ 中真正的临时对象是不可见的——它们不 出现在您的源代码中。每当非堆对象出现时,它们就会出现 创建但未命名。这种未命名的对象通常出现在其中之一 两种情况:当应用隐式类型转换时 函数调用成功,函数何时返回对象。”

    "首先考虑创建临时对象的情况 使函数调用成功。当对象的类型 传递给函数的参数类型与传递给的参数类型不同 它被绑定了。”

    "这些转换只发生在按值传递对象或当 传递给一个引用到常量的参数。它们不会发生在 将对象传递给非 const 参数的引用。"

    "临时对象的第二组情况 created 是函数返回一个对象的时候。”

    "任何时候你看到一个引用到 const 的参数,都有可能 存在将创建一个临时文件以绑定到该参数。 任何时候你看到一个函数返回一个对象,一个临时的将是 被创建(后来被销毁)。”

    答案的另一部分在:“Meyer:Effective C++”,在 “介绍”:

    "复制构造函数用于初始化具有不同的对象 同类型对象:"

    String s1;       // call default constructor
    String s2(s1);   // call copy constructor
    String s3 = s2;  // call copy constructor
    

    "可能拷贝构造函数最重要的用途就是定义 按值传递和返回对象是什么意思。”

    关于我的问题:

    f5() = X(1) //what is happening?
    

    这里没有初始化一个新对象,所以这不是 初始化(复制构造函数):这是一个赋值(如 Matthieu M 指出)。

    创建临时对象是因为根据 Meyer(顶部段落), 这两个函数都返回值,因此正在创建临时对象。 正如 Matthieu 使用伪代码指出的那样,它变成: __0.operator=(__1) 并进行按位复制(由 编译器)。

    关于:

    void f7(X& x);
    f7(f5);
    

    ergo,临时无法创建(Meyer:顶部段落)。 如果已声明:void f7(const X&amp; x);,则临时将 已创建。

    关于临时对象是常量:

    Meyer 说(和 Matthieu):“将创建一个临时对象来绑定它 参数。”

    所以一个临时的只绑定到一个常量引用,它本身不是 一个“常量”对象。

    关于: X(1)是什么?

    Meyer,Item27,Effective C++ - 3e,他说:

    "C 风格的转换看起来像这样: (T)expression //将表达式转换为 T型

    函数式转换使用以下语法: T(expression) //cast expression 属于T型”

    所以X(1) 是一个函数式转换。 1 表达式被转换为 输入X

    迈耶又说了一遍:

    “我唯一一次使用旧式演员表是当我想调用 显式构造函数将对象传递给函数。例如:

    class Widget {
      public:
        explicit Widget(int size);
        ...
    };
    
    void doSomeWork(const Widget& w);
    doSomeWork(Widget(15)); //create Widget from int
                            //with function-style cast
    
    doSomeWork(static_cast<Widget>(15));
    

    不知何故,刻意创建对象并不“感觉”像演员表,所以我会 可能使用函数样式转换而不是 static_cast 这种情况。”

    【讨论】:

    • 我看不出这个“答案”以何种方式解释了问题中提出的问题在其他答案中没有得到回答的问题。如果您想了解比所提问题更多的信息,请问他们
    • X(1) 不是演员表。在 X 构造函数中放置一个 print 语句,您将看到它执行。 C++(和 C)中的符号和关键字通常会根据内容改变其含义。
    • 关于“并且发生按位复制(由编译器完成)” - 这将是发生的赋值操作(可能是重载运算符),不一定是按位复制。默认的编译器生成的赋值运算符只对基本类型进行按位赋值,但对复合类型进行成员赋值。
    【解决方案3】:
    1. 这确实是一个构造函数调用,一个表达式评估为X 类型的临时对象。 X([...]) 形式的表达式和 X 是类型的名称是构造函数调用,它们创建类型为 X 的临时对象(尽管我不知道如何用适当的标准来解释,并且在特殊情况下解析器的行为可能不同)。这与您在 f5f6 函数中使用的构造相同,只是省略了可选的 ii 参数。

    2. X(1) 创建的临时对象一直存在(不会被破坏/无效),直到包含它的完整表达式结束,这通常意味着(就像在这种情况下的赋值表达式一样)直到分号。同样f5 创建一个临时的X 并将其返回到调用站点(在main 内),从而复制它。所以主要f5调用也返回一个临时的X。然后为这个临时的X 分配由X(1) 创建的临时X。完成之后(如果你愿意,分号到达),两个临时对象都会被销毁。这个赋值是有效的,因为这些函数返回普通的 non-constant 对象,无论它们是否只是临时的并在表达式被完全评估后被销毁(从而使赋值或多或少毫无意义,即使完全有效) .

      它不适用于f6,因为它会返回一个您无法分配的const X。同样,f7(f5()) 也不起作用,因为 f5 创建了一个临时对象,并且临时对象不会绑定到非 const 左值引用 X&amp;(C++11 为此目的引入了右值引用 X&amp;&amp;,但这是不同的故事)。如果f7 采用常量引用const X&amp;,它将起作用,因为常量左值引用绑定到临时对象(但随后f7 本身将不再起作用,当然)。

    【讨论】:

    • Eckel, pg:368: “但是关于临时变量有一点:它们是自动恒定的” “通过自动将所有临时变量设为 const,编译器会在您犯错时通知您”
    • 我认为这是错误的 - “这个分配有效,因为这些函数返回普通的非常量对象,无论它们是否只是临时的”
    • @paleywiener 不,它们不是自动恒定的,这就是为什么这个赋值起作用的原因(以及为什么有一个额外的函数f6返回一个const,其中否则f5f6 之间会有区别吗?)。我不知道这是否断章取义,但您的引用完全是错误的。或者,也许他只是建议您自己将所有返回值设为 const。
    【解决方案4】:

    您的所有问题都归结为 C++ 中的一条规则,即临时对象(没有名称的对象)不能绑定到非常量引用。 (因为 Stroustrup 觉得这可能会引发逻辑错误……)

    有一个问题,您可以在临时上调用一个方法:所以X(1).modify() 可以,但f7(X(1)) 不行。

    至于创建临时的位置,这是编译器的工作。该语言的规则明确规定,临时对象只能存活到当前完整表达式结束(并且不再),这对于析构函数具有副作用的类的临时实例很重要。

    因此,以下语句X(1).modify();可以完全翻译为:

    {
        X __0(1);
        __0.modify();
    } // automatic cleanup of __0
    

    考虑到这一点,我们可以攻击f5() = X(1);。我们这里有两个临时工,还有一个任务。在调用赋值之前,赋值的两个参数都必须被完全评估,但顺序并不精确。一种可能的翻译是:

    {
        X __0(f5());
        X __1(1);
        __0.operator=(__1);
    }
    

    另一个翻译是交换__0__1的初始化顺序

    它起作用的关键是__0.operator=(__1)是一个方法调用,可以在临时对象上调用方法:)

    【讨论】:

    • 男:是的!临时对象是常量对象!但肯定是 X __0(f5());除非将 __0() 声明为 __0(const X&)??成员函数必须保证不会更改传递给它的内容!你能澄清一下 __0 和 __1 是什么(链接)吗?我不知道它们是什么。另外,我不明白您的意思是:“可以在临时对象上调用方法”。临时对象是 const 对象。所以除非您调用的方法承诺不会更改它。
    • 我的意思是这一直是我的问题.. 临时对象是一个常量对象,但 Eckel 在编译器将其标记为 const 后似乎很高兴地分配给它!!!跨度>
    • @paleywiener 临时对象不是一个常量对象,这一直是我们的答案。如果这就是 Eckel 所说的(尽管我对此表示怀疑),那他只是
    • @paleywiener 在他的代码中那些__0 的东西代表了创建的临时变量,它们只是为了演示代码的实际工作方式。
    • @paleywiener 常量和临时是完全正交的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-06-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-30
    • 2010-10-28
    • 2021-08-10
    相关资源
    最近更新 更多