临时对象 - 何时创建,如何在代码中识别?

42

在Eckel的第一卷,第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)能成功?这是怎么回事?

Q1. 当他执行X(1)时,发生了什么?这是一个构造函数调用吗?那么不应该写成X::X(1);吗?这是类实例化吗?类实例化不是像X a(1);这样的吗?编译器如何确定X(1)是什么?我的意思是..名字装饰会发生,所以X(1)构造函数调用将被转换为像globalScope_X_int这样的函数名称..???

Q2. 确定会使用临时对象来存储X(1)创建的结果对象,那么它不会被分配给f5()返回的对象(也将是临时对象)吗?鉴于f5()返回即将被丢弃的临时对象,他如何将一个常量临时对象分配给另一个常量临时对象???有人能清楚地解释为什么:f7(f5());应该产生一个常量临时对象而不是普通的f5();


1
X::X指的是构造函数。它不允许在表达式中使用,只能在某些形式的声明中使用。构造函数总是隐式调用的。 - Johannes Schaub - litb
4个回答

39

所有你的问题归结为 C++ 中的一个规则:一个没有名称的临时对象不能被绑定到非 const 引用(因为 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)是一个方法调用,而且可以在临时变量上调用方法 :)


2
@paleywiener 临时对象不是常量对象,这一点一直是我们的答案。如果Eckel说的是这个(虽然我怀疑),他就是错的 - Christian Rau
@paleywiener 他代码中的那些 __0 是代表被创建的临时变量,它们只是为了演示代码实际运行方式而存在。 - Christian Rau
1
@paleywiener 常量和临时变量是完全独立的。 - fredoverflow
@paleywiener:是的,对不起,X x(1); 是一个变量定义,我觉得 X __0(1); 也应该被理解为这样。__0__1 是我引入的临时变量的名称,以使它们与常规变量区分开来。 - Matthieu M.
顺便说一句,我真的很喜欢TICPP Vol I,它深入了解了事物的细节。即使temps是const这个错误在某些后期阶段你必须理解它是错误的,但对于初学者来说,这种方法仍然有效。 - Don Slowik
显示剩余3条评论

20
我对答案并不完全满意,所以我查看了以下资料:

《More Effective C++》, Scott Meyers. 条款19: “理解临时对象的生成”

关于Bruce Eckel在“Temporaries”的覆盖范围,我怀疑并且正如Christian Rau所指出的那样,它是错误的!他(Eckel)正在利用我们作为小白鼠!!(一旦他纠正了所有的错误,这将是一个适合像我这样的新手阅读的好书)。

Meyer:“C ++中真正的临时对象是看不见的-它们不会出现在您的源代码中。它们在创建非堆对象但未命名时产生。这些未命名的对象通常发生在两种情况下:当应用隐式类型转换使函数调用成功时和当函数返回对象时。”

“首先考虑创建临时对象以使函数调用成功的情况。当传递给函数的对象的类型与其要绑定到的参数的类型不同时,就会发生这种情况。”

“这些转换仅在按值传递对象或传递给引用到const参数时发生。当将对象传递给引用到非const参数时,它们不会发生。”

“创建临时对象的第二组情况是函数返回对象时。每当您看到引用到const参数时,都存在创建临时对象以绑定到该参数的可能性。每当您看到函数返回对象时,都会创建一个临时对象(稍后会被销毁)。”

答案的另一部分可以在 “Meyer: Effective C++” 书的 “Introduction” 中找到:

“复制构造函数用于使用相同类型的不同对象来初始化一个对象:”

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);

因此,临时对象无法创建(Meyer:顶部段落)。
如果声明为:void f7(const X& x);,那么将创建一个临时对象。
关于临时对象是常量的问题:
Meyer和Matthieu都这么说:"将创建一个临时对象来绑定到该参数。"
因此,临时对象只能绑定到常量引用,本身不是“const”对象。
关于:什么是X(1)
在Meyer的Effective C++-3e的第27项中,他说:
“C风格的转换看起来像这样:(T)expression //将表达式转换为类型T
函数风格的转换使用此语法:T(expression) //将表达式转换为类型T”
因此,X(1)是一种函数风格的转换。 表达式1被转换为类型X
Meyer再次说:
“当我想调用显式构造函数向函数传递对象时,我几乎唯一使用旧式转换。例如:”
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。

我无法看出这个“答案”是如何解释在其他答案中未回答的问题。如果你想了解更多问题,就要提出来问。 - Christian Rau
1
X(1) 不是一个强制类型转换。在 X 构造函数中放置一个打印语句,你会看到它执行。C++(和 C)中的符号和关键字通常根据上下文改变其含义。 - gerardw
1
关于“并且位拷贝是由编译器完成的”——这将是一个赋值操作(可能是重载运算符),而不一定是按位复制。默认的编译器生成的赋值运算符仅对基本类型执行按位赋值,但对于复合类型则执行成员逐一赋值。 - Tanz87

7
  1. 这确实是一个构造函数调用,它会计算出一个临时的X类型对象。形如X([...])的表达式,其中X是类型名称,是创建临时X类型对象的构造函数调用(虽然我不知道如何用标准术语来解释这一点,而且有特殊情况下解析器的行为可能会有所不同)。这与您在f5f6函数中使用的结构相同,只是省略了可选的ii参数。

  2. X(1)创建的临时对象将会在包含它的完整表达式结束之前一直存在(不会被销毁或失效),通常意味着(就像在这个赋值表达式中)直到分号结束。同样,f5创建一个临时的X并将其返回到调用点(在main内部),因此进行复制。因此,在主函数中,f5调用也会返回一个临时的X。然后将该临时X分配给由X(1)创建的临时X。完成这些操作后(如果需要,可以到达分号),两个临时对象都将被销毁。这种赋值是有效的,因为这些函数返回普通的非常量对象,无论它们是否只是临时的,在表达式完全计算后被销毁(因此使得赋值有点毫无意义,但完全有效)。

    使用f6不起作用,因为它返回一个const X,你无法对其进行赋值。同样,f7(f5())不起作用,因为f5创建了一个临时对象,并且临时对象不能绑定到非常量左值引用X&(C++11引入了右值引用X&&来实现这个目的,但这是另一个故事)。如果f7采用了一个常量引用const X&,那么它将起作用,因为常量左值引用可以绑定到临时对象(但这样f7本身就无法工作了,当然)。


Eckel,第368页:“但是关于临时变量有一件事:它们自动成为常量” “通过使所有的临时变量自动成为const,编译器在您犯错误时通知您” - user621819
1
这个任务之所以能够正常工作,是因为那些函数返回的是普通的非常量对象,无论它们是否只是临时对象。 - user621819
2
@paleywiener 不,它们不是自动常量,这就是为什么这个赋值语句有效(以及为什么有一个额外的函数f6返回一个const,否则f5f6之间会有什么区别?)。我不知道这是否被断章取义,但你的引用明显是错误的。或者他只是建议你自己将所有返回值都设置为const。 - Christian Rau

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接