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

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

在情况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;
}

在情况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;
}

static_cast
下面展示一个稍复杂的示例:
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;
}

假设头文件 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与列表构造函数简介》中介绍了列表构造函数。

浙公网安备 33010602011771号