临时类对象

考虑以下示例:

#include <iostream>

int add(int x, int y)
{
    int sum{ x + y }; // 将 x + y 的结果存储在一个变量中
    return sum;       // 返回该变量的值
}

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; // 直接返回 x + y
}

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) // 非const引用传递需要左值
{
    ++value;
}

int main()
{
    int sum { 5 + 3 };
    addOne(sum);   // 好的,sum是一个左值

    addOne(5 + 3); // 编译错误:不是一个左值

    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()
{
    // 情况1:传递变量
    IntPair p { 3, 4 };
    print(p); // 打印 (3, 4)

    return 0;
}

在情况1中,我们实例化了变量 IntPair p,然后将 p 传递给函数 print()

然而,p 只被使用了一次,而函数 print() 可以接受右值,因此这里实际上没有理由定义一个变量。那么我们来去掉 p

我们可以通过传递一个临时对象而不是命名变量来做到这一点。临时对象(有时也称为匿名对象或无名对象)是一个没有名称且仅在单个表达式期间存在的对象。

有两种常见的方法可以创建临时类类型对象:

#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()
{
    // 情况1:传递变量
    IntPair p { 3, 4 };
    print(p);

    // 情况2:构造临时 IntPair 并传递给函数
    print(IntPair { 5, 6 } );

    // 情况3:隐式将 { 7, 8 } 转换为临时 Intpair 并传递给函数
    print( { 7, 8 } );

    return 0;
}

在情况2中,我们告诉编译器构造一个 IntPair 对象,并用 { 5, 6 } 初始化它。因为这个对象没有名称,所以它是一个临时对象。然后将临时对象传递给函数 print() 的参数 p。当函数调用返回时,临时对象被销毁。

在情况3中,我们也在创建一个临时 IntPair 对象来传递给函数 print()。然而,因为我们没有明确指定要构造的类型,编译器将从函数参数推导出必要的类型(IntPair),然后将 { 7, 8 } 隐式转换为一个 IntPair 对象。

总结一下:

IntPair p { 1, 2 }; // 创建名为 p 的对象,用 { 1, 2 } 初始化
IntPair { 1, 2 };   // 创建临时对象,用 { 1, 2 } 初始化
{ 1, 2 };           // 编译器将尝试将 { 1, 2 } 转换为符合预期类型的临时对象(通常是参数或返回类型)

我们将在第14.16课——转换构造函数和 explicit 关键字中更详细地讨论最后一种情况。

更多示例

std::string { "Hello" }; // 创建一个用 "Hello" 初始化的临时 std::string
std::string {};          // 使用值初始化/默认构造函数创建一个临时 std::string

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

由于我们可以通过直接列表初始化创建临时对象,你可能会好奇是否可以通过其他初始化形式创建临时对象。没有语法可以通过拷贝初始化创建临时对象。

然而,你可以通过直接初始化创建临时对象。例如:

Foo (1, 2); // 临时 Foo,用 (1, 2) 直接初始化(类似于 `Foo { 1, 2 }`)

撇开它看起来像函数调用这一点不谈,这与 Foo { 1, 2 } 产生相同的结果(只是没有防止窄化转换)。这很正常,对吧?

我们现在将花费本节的其余部分向你展示为什么你可能不应该这样做。

作者注:这主要是为了你的阅读乐趣,而不是你需要消化、记忆并能够解释的内容。

即使你读起来并不那么有趣,它也可能帮助你理解为什么在现代C++中更倾向于列表初始化!

现在让我们看看没有参数的情况:

Foo();     // 临时 Foo,值初始化(与 `Foo {}` 相同)

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

Foo bar{}; // 变量 bar 的定义,值初始化
Foo bar(); // 声明一个无参数且返回 Foo 的函数 bar(与 `Foo bar{}` 和 `Foo()` 不一致)

准备好变得更奇怪了吗?!

Foo(1);    // 对字面量 1 进行函数式转换,返回临时 Foo(类似于 `Foo { 1 }`)
Foo(bar);  // 定义类型为 Foo 的变量 bar(与 `Foo { bar }` 和 `Foo(1)` 不一致)

等等,什么?

  • 带括号的字面量 1 的版本与创建临时对象的其他所有语法版本的行为一致。
  • 带括号的标识符 bar 的版本定义了一个名为 bar 的变量(与 Foo bar; 相同)。如果 bar 已经定义,这将导致重复定义编译错误。
  • 编译器知道字面量不能用作变量的标识符,因此它能够将这种情况与其他情况一致地处理。

顺便说一下

如果你想知道为什么 Foo(bar); 的行为与 Foo bar; 相同

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"