41

在 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)成功?这里发生了什么???

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

Q2。当然,临时对象用于存储X(1) 创建的结果对象,然后将其分配给对象f5()返回(这也将是一个临时对象)?鉴于f5()返回一个将很快被丢弃的临时对象,他如何将一个常量临时分配给另一个常量临时?有人可以清楚地解释为什么: f7(f5());应该导致一个持续的临时性而不是简单的旧f5();

4

4 回答 4

38

您所有的问题都归结为 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)是方法调用,并且可以在临时对象上调用方法:)

于 2012-06-05T13:32:20.323 回答
19

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

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

. 关于布鲁斯·埃克尔对“临时演员”的报道,好吧,正如我怀疑的那样,正如克里斯蒂安·劳直接指出的那样,这是完全错误的!呸!他(埃克尔的)把我们当作豚鼠!(一旦他纠正了所有的错误,这对于像我这样的新手来说将是一本好书)

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):“将创建一个临时的来绑定到该参数。”

因此,临时对象仅绑定到常量引用,并且本身不是“常量”对象。

关于:什么是X(1)

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

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

函数样式转换使用以下语法: T(expression) //将表达式转换为 T 类型"

X(1)函数式演员也是如此。1表达式被强制转换为 type 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。”

于 2012-06-10T05:08:06.780 回答
7
  1. 这确实是一个构造函数调用,一个计算类型为临时对象的表达式XX([...])作为类型名称的表单表达式是X创建临时类型对象的构造函数调用X(尽管我不知道如何用适当的标准来解释这一点,并且在某些特殊情况下解析器的行为可能会有所不同)。这与您在f5andf6函数中使用的构造相同,只是省略了可选ii参数。

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

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

于 2012-06-05T13:31:42.537 回答
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
于 2016-10-11T13:55:58.860 回答