【问题标题】:What are C++ temporaries?什么是 C++ 临时对象?
【发布时间】:2013-02-28 07:59:17
【问题描述】:

我正在阅读 Eckel 的固定章节,并在解释 Temporaries 的部分混淆了。我能得到的是,当我们将引用传递给函数时,编译器会创建一个临时对象,它是一个 const 对象,因此即使我们将引用传递为

,我们也无法修改它
f(int &a){}

现在我尝试在网上查看其他一些临时人员的参考资料,结果卡住了

http://publib.boulder.ibm.com/infocenter/lnxpcomp/v8v101/index.jsp?topic=%2Fcom.ibm.xlcpp8l.doc%2Flanguage%2Fref%2Fcplr382.htm

Are all temporaries rvalues in C++?

这促使我认为临时对象不仅仅是在函数内部传递引用和为其创建 const 对象。现在我可以从这两个链接中得到一些东西,但不能说我已经理解临时对象的工作、功能和使用所有的。如果有人可以解释临时人员的概念,那将非常有帮助。在此先感谢。
bruce eckel 的原始示例是:

// 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());
} ///:~

【问题讨论】:

  • 引用和临时没有直接关系。它与对象的创建/销毁方式有关。也许你最好重新开始..

标签: c++ constants


【解决方案1】:

临时对象是一个未命名的对象(某些结果 表达式),并且始终是右值。或者也许应该 最好说导致右值的表达式是 一个临时的。

在 C 中,右值/临时对象并不是真正的对象(在某种意义上 该标准使用“对象”一词:位于 在记忆中)。因此,例如,它们不是 cv 合格的 (像3 + 5 这样的表达式的类型为int,而不是int const, 和 cv 限定符在函数返回值上被忽略),你 无法获取他们的地址(因为他们不在内存中,他们 没有地址)。在 C++ 中,这个问题被类所掩盖 类型:您可以在右值上调用成员函数,并且 成员函数将有一个this 指针,这意味着 即使是右值(类类型)也必须在内存中有一个地址,并且 cv-qualifications 有意义,因为如果返回类型是 const,你不能在上面调用非常量函数。

最后,虽然右值和临时的概念是 非常密切相关,C++ 标准使用的词在 方式略有不同。表达式的结果是 右值或左值(C++11 增加了其他可能性, 但在你成为专家之前,你可以忽略它们),这 区别涉及所有类型。当 C++ 标准谈到 一个临时的,它是一个右值,它是(或已经成为)一个对象。 在大多数情况下,它们具有类类型;很少 你会有一个临时的 not 类的情况 输入编写良好的代码,except 模板所在的位置 涉及。区别很重要,因为即使 内置 & 运算符在右值上是非法的,类的右值 类型有一个定义的“生命周期”和一个内存地址。那 生命周期是直到完整表达式的结尾。所以上课的时候 类型方面,临时和之间的区别 命名值主要是临时没有名称, 它有不同的寿命。类类型的生命周期 之所以重要,有两个原因:首先,它决定了何时 析构函数被调用,其次,如果对象“泄漏” 指向内部数据的指针,例如像std::string::c_str(),它 确定此指针的有效期。

最后,我提到了模板,因为这是唯一一次 您将拥有对非类类型的 const 引用。通常 in 参数的约定是非类的值传递 类型,以及类类型的 const 引用;的作者 然而,模板不知道T 是否会是一个类 类型与否,并且在大多数情况下,将定义他的函数 采取T const&。在实践中,这将是唯一的 到时候你会得到一个非类类型的临时对象 (如果模板保存了参数的地址,你可以 有问题)。

【讨论】:

    【解决方案2】:

    在 C++ 中,临时对象是编译器在各种上下文中创建的未命名对象。典型用途包括引用初始化、参数传递、表达式求值(包括标准类型转换)、函数返回和异常(抛出表达式)。

    从此link:

    当创建临时对象来初始化引用变量时,临时对象的名称与引用变量的范围相同。如果在完整表达式(不是另一个表达式的子表达式的表达式)的求值过程中创建了一个临时对象,它会作为其求值的最后一步被销毁,在词法上包含创建它的点。

    完整表达式的销毁也有例外:

    1. 表达式作为定义对象的声明的初始化程序出现:初始化完成后临时对象被销毁。
    2. 引用绑定到临时对象:临时对象在引用的生命周期结束时被销毁。

    如果为具有构造函数的类创建了临时对象,编译器会调用适当的(匹配的)构造函数来创建临时对象。

    当一个临时对象被销毁并且存在析构函数时,编译器调用析构函数销毁该临时对象。当您退出创建临时对象的范围时,它会被销毁。如果引用绑定到临时对象,则临时对象在引用超出范围时被销毁,除非它在控制流中断之前被销毁。例如,由构造函数初始化程序为引用成员创建的临时对象在离开构造函数时被销毁。

    在此类临时对象是冗余的情况下,编译器不会构造它们,以便创建更有效的优化代码。当您调试程序时,尤其是内存问题时,这种行为可能是一个考虑因素。

    让我们总结一下this way。创建这些临时对象的原因如下:

    1. 使用与被初始化引用的基础类型不同类型的初始化程序来初始化 const 引用。

    2. 存储返回用户定义类型的函数的返回值。仅当您的程序不将返回值复制到对象时,才会创建这些临时对象。因为返回值没有复制到另一个对象,所以创建了一个临时对象。创建临时变量的更常见情况是在计算表达式期间必须调用重载的运算符函数。这些重载的运算符函数返回一个用户定义的类型,该类型通常不会复制到另一个对象。

    3. 将转换结果存储为用户定义的类型。当给定类型的对象显式转换为用户定义类型时,该新对象将被构造为临时对象。

    让我们考虑example

    class X {
       / / ...
    public:
       / / ...
       X(int);
       X(const X&);
       ~X();
    };
    
    X f(X);
    
    void g()
    {
       X a(1);
       X b = f(X(2));
       a = f(a);
    }
    

    在这里,一个实现可能会使用一个临时对象来构造X(2),然后使用X 的复制构造函数将其传递给f();或者,X(2) 可以在用于保存参数的空间中构造。此外,在使用X 的复制构造函数将f(X(2)) 的结果复制到b 之前,可能会使用一个临时值来保存f(X(2)) 的结果;或者,f() 的结果可能在b 中构造。另一方面,表达式a=f(a) 需要一个临时的f(a) 的结果,然后分配给a

    【讨论】:

    • 这里的完整表达部分是什么意思?
    • C++ 没有标准中提到的堆栈(除了 std::stack 是容器适配器之外的其他堆栈)。
    • @Kavish Dwivedi:我添加了示例。
    • @Öö Tiib:顺便说一句,标准中提到了stack unwinding。因此,隐式 C++ 确实有一个堆栈。
    • 当 throw 被执行时,对象必须以完全相反的创建顺序被销毁,这就是为什么该事件被称为“堆栈展开”,这并不意味着架构中有堆栈。
    【解决方案3】:

    让我们从一个例子开始:

    float f(int a);
    int g(float b);
    int main() { int a=10; std::cout << g(f(a)); }
    

    函数调用如下所示:

    int ---f----> float ---g---> int
    

    当编译器为 g(f(a)) 生成代码时,它需要一些内存来存储结果 在 f() 被调用后浮动。这个内存区域称为临时的。它是 f() 和 g() 之间的内存。

    【讨论】:

    • 这里作为临时存储的任何东西都是不变的吗?我认为只有在我们进行引用传递时才会发生这种情况。
    • 标准在这里没有说临时,只说右值。 fg 的结果不是对象,也不会(明显)占用内存——它们没有地址,没有 cv 限定等(如果你声明 float const f(),返回类型是float,而不是float const。)
    • @KavishDwivedi 这里没有存储任何内容,因此是否为 const 无关紧要。如果返回类型是类类型,则 const 变得相关,并且当且仅当它被声明为 const 时,该类型才是 const。
    • @james kanze:将浮点数放入寄存器还是将其保存在内存中不只是一些优化吗?未优化将始终将其保留在内存中,而临时将是内存区域。
    • @tp1 在这个概念起源的 C 语言中,观点是相反的:除非没有其他办法,否则右值不会在内存中(因此没有&amp;)。抽象地看,它只是一个“价值”,没有放在任何地方。 (例如,在整数文字的情况下,它通常是机器指令的一部分。)显然,即使在 C 中,如果返回类型类似于 struct { char x[1000000]; },它在记忆,但语言(C)仍然把它当作不是。只有当您需要 this 指针时,它才必须在内存中。
    【解决方案4】:

    临时对象是计算的“副产品”。它们没有显式声明,顾名思义,它们是临时的。不过,您应该知道编译器何时创建临时对象,因为通常可以防止这种情况发生。

    正如他的帖子中的几个答案详细介绍了临时对象是什么,但我想添加一些与它们相关的“额外开销”相关的内容,但我们也有一些方法可以避免它们。

    1. 临时变量最常见的地方是通过值将对象传递给方法。形式参数是在堆栈上创建的。这可以通过使用按地址传递或按引用传递来防止。

    2. 编译器可能会在分配对象时创建临时对象。例如,可以为将 int 作为参数的构造函数分配一个 int。编译器将使用 int 作为参数创建一个临时对象,然后对该对象调用赋值运算符。您可以通过在构造函数的声明中使用显式关键字来防止编译器在您背后执行此操作。

    3. 当按值返回对象时,经常使用临时对象。必须返回对象的方法通常必须创建要返回的对象。由于构造此对象需要时间,因此我们希望尽可能避免它。有几种方法可以做到这一点。

      3.a.不是返回一个对象,而是向方法添加另一个参数,允许程序员传入程序员想要存储结果的对象。这样,该方法就不必创建额外的对象。它只会使用传递给方法的参数。这种技术称为返回值优化 (RVO)。

    RVO 是否会导致实际优化取决于编译器。不同的编译器以不同的方式处理这个问题。帮助编译器的一种方法是使用计算构造函数。可以使用计算构造函数代替返回对象的方法。计算构造函数采用与要优化的方法相同的参数,但不是根据参数返回对象,而是根据参数的值进行初始化。

    1. 使用 = 运算符可以避免临时性。比如代码

      a = b + c; 可以写成 a=b; a+=c;.

    【讨论】:

    猜你喜欢
    • 2016-01-30
    • 2021-11-23
    • 2019-02-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-01
    • 1970-01-01
    相关资源
    最近更新 更多