什么是C++临时的

What are C++ temporaries?

本文关键字:C++ 什么      更新时间:2023-10-16

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

f(int &a){}

现在我试图在网上查看其他一些临时参考资料,但陷入了困境

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

所有的临时值都在C++吗?

这促使我,临时不仅仅是传递引用并在函数内为其创建 const 对象。现在我可以从这两个环节中得到一些东西,但不能说我已经理解了整个临时的工作、功能和使用。如果有人能解释临时的概念,那将非常有帮助。提前谢谢。
布鲁斯·埃克尔的原始例子是:

// 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 语言中,右值/临时值并不是真正的对象(从某种意义上说(该标准使用"对象"一词:位于在内存中(。 因此,例如,他们不符合简历资格(像 3 + 5 这样的表达式具有类型 int ,而不是 int const ,和 CV 限定符在函数返回值上被忽略(和你无法获取他们的地址(因为他们不在内存中,所以他们没有地址(。 在C++,这个问题被阶级所笼罩类型:可以在右值上调用成员函数,并且成员函数将有一个this指针,这意味着甚至右值(类类型(也必须在内存中有一个地址,并且简历资格是有意义的,因为如果返回类型是 const,你不能在它上面调用一个非常量函数。

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

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

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

从这个链接:

创建临时对象

以初始化引用变量时,临时对象的名称与引用变量的名称具有相同的作用域。在计算完整表达式(不是另一个表达式的子表达式的表达式(期间创建临时对象时,该对象将作为其计算的最后一步销毁,该对象在词法上包含创建该对象的点。

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

  1. 该表达式显示为定义对象的声明的初始值设定项:初始化完成后,临时对象将被销毁。
  2. 引用
  3. 绑定到临时对象:临时对象在引用的生存期结束时销毁。

如果为具有构造函数的类创建了临时对象,则编译器将调用相应的(匹配(构造函数来创建临时对象。

当临时对象被销毁并且析构函数存在时,编译器将调用析构函数来销毁临时对象。当您退出创建临时对象的作用域时,该对象将被销毁。如果引用绑定到临时对象,则当引用超出范围时,将销毁临时对象,除非它因控制流中断而提前销毁。例如,由构造函数初始值设定项为引用成员创建的临时对象在离开构造函数时被销毁。

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

让我们这样总结一下。可以出于以下原因创建这些临时对象:

  1. 使用与正在初始化的引用的基础类型不同的初始值设定项初始化 const 引用。

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

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

让我们考虑一下这个例子:

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 的复制构造函数将其复制到 b 之前,可以使用临时来保存 f(X(2)) 的结果;或者,f()的结果可以用b来构造。另一方面,表达式 a=f(a) 需要临时的 f(a) 结果,然后将其分配给 a

让我们从一个例子开始:

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(( 之间的内存。

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

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

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

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

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

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

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

  1. 通过使用 = 运算符可以避免临时。例如,代码

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