14-13 临时类对象

请考虑以下示例:

#include <iostream>

int add(int x, int y)
{
    int sum{ x + y }; // stores x + y in a variable
    return sum;       // returns value of that variable
}

int main()
{
    std::cout << add(5, 3) << '\n';

    return 0;
}

在 add() 函数中,变量 sum 用于存储表达式 x + y 的结果。该变量随后在 return 语句中被求值,以生成要返回的值。虽然这偶尔可能有助于调试(以便我们根据需要检查 sum 的值),但实际上通过定义一个仅使用一次的对象,使得函数比必要时更为复杂。

对于仅使用一次的变量,多数情况下我们其实无需定义变量。取而代之的是,在变量原使用位置直接代入用于初始化该变量的表达式。以下是采用此方式重写的 add() 函数:

#include <iostream>

int add(int x, int y)
{
    return x + y; // just return x + y directly
}

int main()
{
    std::cout << add(5, 3) << '\n';

    return 0;
}

这不仅适用于返回值,也适用于大多数函数参数。例如,与其这样写:

#include <iostream>

void printValue(int value)
{
    std::cout << value;
}

int main()
{
    int sum{ 5 + 3 };
    printValue(sum);

    return 0;
}

我们可以这样写:

#include <iostream>

void printValue(int value)
{
    std::cout << value;
}

int main()
{
    printValue(5 + 3);

    return 0;
}

请注意这种写法让代码多么简洁。我们无需定义变量并赋予其名称,也无需遍历整个函数来判断该变量是否在其他位置被使用。由于5 + 3是表达式,我们知道它仅在这行代码中被使用。

请注意,这种写法仅适用于接受右值表达式的情况。当需要左值表达式时,我们必须使用对象:

#include <iostream>

void addOne(int& value) // pass by non-const references requires lvalue
{
    ++value;
}

int main()
{
    int sum { 5 + 3 };
    addOne(sum);   // okay, sum is an lvalue

    addOne(5 + 3); // compile error: not an lvalue

    return 0;
}

临时类对象

在类类型上下文中同样存在此问题。

作者注
本节将使用类进行说明,但凡涉及列表初始化的内容,同样适用于采用聚合初始化方式初始化的结构体。

下例与前例类似,但使用程序定义的类类型 IntPair 替代 int:

#include <iostream>

class IntPair
{
private:
    int m_x{};
    int m_y{};

public:
    IntPair(int x, int y)
        : m_x { x }, m_y { y }
    {}

    int x() const { return m_x; }
    int y() const { return m_y; }
};

void print(IntPair p)
{
    std::cout << "(" << p.x() << ", " << p.y() << ")\n";
}

int main()
{
    // Case 1: Pass variable
    IntPair p { 3, 4 };
    print(p); // prints (3, 4)

    return 0;
}

image

IntPair p,再将p传递给函数print()。

然而p仅被使用一次,且函数print()接受右值参数,因此在此处定义变量实属多余。让我们移除p。

我们可以通过传递临时对象代替命名变量来实现。临时对象temporary object(有时称为匿名对象anonymous object无名对象unnamed object)是没有名称的对象,仅在单个表达式执行期间存在。

创建临时类对象有两种常见方式:

#include <iostream>

class IntPair
{
private:
    int m_x{};
    int m_y{};

public:
    IntPair(int x, int y)
        : m_x { x }, m_y { y }
    {}

    int x() const { return m_x; }
    int y() const{ return m_y; }
};

void print(IntPair p)
{
    std::cout << "(" << p.x() << ", " << p.y() << ")\n";
}

int main()
{
    // Case 1: Pass variable
    IntPair p { 3, 4 };
    print(p);

    // Case 2: Construct temporary IntPair and pass to function
    print(IntPair { 5, 6 } );

    // Case 3: Implicitly convert { 7, 8 } to a temporary Intpair and pass to function
    print( { 7, 8 } );

    return 0;
}

image

在情况2中,我们指示编译器构造一个IntPair对象,并用{ 5, 6 }初始化它。由于该对象没有名称,因此属于临时对象。该临时对象随后被传递给函数print()的参数p。当函数调用返回时,临时对象即被销毁。

在情况3中,我们同样创建临时IntPair对象传递给print()函数。但由于未显式指定构造类型,编译器将从函数参数推断出所需类型(IntPair),并隐式将{ 7, 8 }转换为IntPair对象。

总结:

IntPair p { 1, 2 }; // create named object p initialized with { 1, 2 }
IntPair { 1, 2 };   // create temporary object initialized with { 1, 2 }
{ 1, 2 };           // compiler will try to convert { 1, 2 } to temporary object matching expected type (typically a parameter or return type)

我们将在第14.16节——转换构造函数与显式关键字中详细讨论最后一种情况。

再举几个例子:

std::string { "Hello" }; // create a temporary std::string initialized with "Hello"
std::string {};          // create a temporary std::string using value initialization / default constructor

通过直接初始化创建临时对象(可选)

既然我们能通过直接列表初始化创建临时对象,您可能想知道是否能通过其他初始化形式创建临时对象。使用复制初始化创建临时对象没有对应的语法。

但您可以通过直接初始化创建临时对象。例如:

Foo (1, 2); //  temporary Foo, direct-initialized with (1, 2) (similar to `Foo { 1, 2 }`)

暂且不论它乍看像函数调用,这段代码与 Foo { 1, 2 } 效果相同(只是没有防止类型收窄转换)。挺正常的对吧?

本节剩余部分将向你说明为何不该这么做。

作者注:
本文主要为阅读趣味而写,无需强求理解、记忆或能完整阐释。
即便阅读过程不够有趣,或许也能帮助你理解为何现代C++更倾向于使用列表初始化!

现在来看没有参数的情况:

Foo();     // temporary Foo, value-initialized (identical to `Foo {}`)

你可能没想到,Foo() 会像 Foo {} 一样创建一个值初始化的临时对象。这很可能是因为当这种语法用于命名变量时,其含义完全不同!

Foo bar{}; // definition of variable bar, value-initialized
Foo bar(); // declaration of function bar that has no parameters and returns a Foo (inconsistent with `Foo bar{}` and `Foo()`)

准备好迎接超诡异的事了吗?!

Foo(1);    // Function-style cast of literal 1, returns temporary Foo (similar to `Foo { 1 }`)
Foo(bar);  // Defines variable bar of type Foo (inconsistent with `Foo { bar }` and `Foo(1)`)

等等,什么?

  • 括号内包含数字1的版本,其行为与所有其他创建临时对象的语法版本一致。
  • 括号内包含标识符bar的版本会定义名为bar的变量(与Foo bar;等效)。若bar已存在,将引发重新定义的编译错误。

编译器知道字面量不能用作变量标识符,因此能将其与其他情况统一处理。

顺带一提……:
如果你好奇为什么 Foo(bar); 和 Foo bar; 的行为完全相同……
括号最常见的用途之一就是分组。例如在数学中:(1 + 2) * 3 的结果是 9,而 1 + 2 * 3 的结果则是 7。既然能写 (1 + 2) * 3,自然也能写 (3) * 3。
基于相同原理,声明语法允许使用括号分组,且分组内可仅包含单个元素。Foo(bar) 被解释为变量定义:类型 Foo 后跟仅包含标识符 bar 的分组。这种写法看起来有些奇怪,主要是因为括号在此处没有实际作用。但没有充分理由禁止这种写法(否则只会让语言语法变得更加复杂)。

对于高级读者:
让我们看看一个稍复杂的案例。考虑语句 Foo * bar();。通过使用(或不使用)括号,我们可以彻底改变该语句的含义:

  • Foo * bar();(无额外括号)默认将 * 与 Foo 组合。Foo* bar(); 是声明一个名为 bar 的函数,该函数无参数且返回 Foo*。
  • Foo (*bar)(); 明确将 * 与 bar 组合。这定义了一个名为 bar 的函数指针,其存储的地址指向一个无参数且返回 Foo 类型的函数。
  • Foo (* bar()); 与 Foo * bar(); 效果相同——此处括号可省略。
    最后:
  • (Foo ) bar();。你可能以为这等同于 Foo bar(),但实际上这是个表达式语句:先调用函数 bar(),再用 C 风格强制将返回值转换为 Foo* 类型,最后直接丢弃该值!

C++ 有时真是怪异。

好了,玩够了。该回到无聊的事上了。


临时对象与按值返回

当函数按值返回时,返回的对象是临时对象(使用返回语句中指定的值或对象初始化)。

以下是一些示例:

#include <iostream>

class IntPair
{
private:
    int m_x{};
    int m_y{};

public:
    IntPair(int x, int y)
        : m_x { x }, m_y { y }
    {}

    int x() const { return m_x; }
    int y() const { return m_y; }
};

void print(IntPair p)
{
    std::cout << "(" << p.x() << ", " << p.y() << ")\n";
}

// Case 1: Create named variable and return
IntPair ret1()
{
    IntPair p { 3, 4 };
    return p; // returns temporary object (initialized using p)
}

// Case 2: Create temporary IntPair and return
IntPair ret2()
{
    return IntPair { 5, 6 }; // returns temporary object (initialized using another temporary object)
}

// Case 3: implicitly convert { 7, 8 } to IntPair and return
IntPair ret3()
{
    return { 7, 8 }; // returns temporary object (initialized using another temporary object)
}

int main()
{
    print(ret1());
    print(ret2());
    print(ret3());

    return 0;
}

image

在情况1中,当我们返回p时,会创建一个临时对象并使用p进行初始化。

本例中的情况与前例中的情况类似。


几点说明

首先,与整型变量类似,临时类对象在表达式中使用时属于右值。因此,此类对象仅能在接受右值表达式的上下文中使用。

其次,临时对象在定义处创建,并在包含其定义的完整表达式结束时销毁。完整表达式指非子表达式的表达式。


static_cast 与显式实例化临时对象的对比

当需要将值从一种类型转换为另一种类型且不涉及类型收窄转换时,我们通常可选择使用 static_cast 或显式实例化临时对象。

例如:

#include <iostream>

int main()
{
    char c { 'a' };

    std::cout << static_cast<int>( c ) << '\n'; // static_cast returns a temporary int direct-initialized with value of c
    std::cout << int { c } << '\n';             // explicitly creates a temporary int list-initialized with value c

    return 0;
}

image

static_cast(c) 返回一个临时 int 类型对象,该对象通过 c 的值进行直接初始化。int { c } 创建一个临时 int 类型对象,该对象通过 c 的值进行列表初始化。无论哪种方式,我们都能得到一个用 c 的值初始化的临时 int 对象,这正是我们想要的结果。

下面展示一个稍复杂的示例:

printString.h:

#include <string>
void printString(const std::string &s)
{
    std::cout << s << '\n';
}

main.cpp:

#include "printString.h"
#include <iostream>
#include <string>
#include <string_view>

int main()
{
    std::string_view sv { "Hello" };

    // We want to print sv using the printString() function

//    printString(sv); // compile error: a std::string_view won't implicitly convert to a std::string

    printString( static_cast<std::string>(sv) ); // Case 1: static_cast returns a temporary std::string direct-initialized with sv
    printString( std::string { sv } );           // Case 2: explicitly creates a temporary std::string list-initialized with sv
    printString( std::string ( sv ) );           // Case 3: C-style cast returns temporary std::string direct-initialized with sv (avoid this one!)

    return 0;
}

image

假设头文件 printString.h 中的代码无法修改(例如因为它是随我们使用的某个第三方库分发的,且为兼容 C++11 而编写,而 C++11 不支持 std::string_view)。那么如何使用sv调用printString()?由于std::string_view不会隐式转换为std::string(出于效率考虑),我们不能直接将sv作为参数传递。必须采用显式转换形式。

情况1中,static_caststd::string(sv)调用static_cast运算符将sv转换为std::string。该操作返回一个通过 sv 直接初始化的临时 std::string,随后作为函数调用的参数。

情况 2 中,std::string { sv } 创建一个使用 sv 列表初始化的临时 std::string。由于这是显式构造,转换被允许。该临时对象随后作为函数调用的参数。

在情况 3 中,std::string ( sv ) 使用 C 风格转换将 sv 转换为 std::string。虽然此处可行,但 C 风格转换通常存在风险,应避免使用。注意此处与前例的相似性!

最佳实践
快速经验法则:转换为基本类型时优先使用 static_cast,转换为类类型时优先使用列表初始化临时对象。

在以下任一情况需创建临时对象时优先使用 static_cast:

  • 需要执行窄化转换
  • 需明确标识转换将导致行为差异(如 char 转 int)
  • 因特殊需求需使用直接初始化(如避免列表构造函数优先)

当满足以下任一条件时,优先使用列表初始化创建新对象而非临时对象:

  • 需要使用列表初始化(例如为防范窄化转换,或因需调用列表构造函数)。
  • 需要向构造函数提供额外参数以完成转换。

相关内容
我们在第16.2节——《std::vector与列表构造函数简介》中介绍了列表构造函数。

posted @ 2025-12-30 10:27  游翔  阅读(17)  评论(0)    收藏  举报