来自 std::pair 右值构造函数的垃圾值

Garbage value from std::pair rvalues constructor

编辑 在底部


我发现 pair 的构造函数有一个我不完全理解的行为。

所以我尝试用右值初始化一对,代码如下:

pair<vector<int> &&, int> a([]() -> vector<int> {
    vector<int> b{1};
    cout << &b << ' ' << b[0] << '\n';
    return b;
}(), 0);

cout << &a.first << ' ' << a.first[0] << '\n';

输出为

0x62fdf0 1
0x62fdf0 14162480

显然 a.first 是垃圾。

然后我在网上找到pair的构造函数是这样的:

pair (const first_type& a, const second_type& b);
template<class U, class V> pair (U&& a, V&& b);

所以我猜第二个正在使用?然后我尝试删除 &&:

pair<vector<int>, int> a([]() -> vector<int> {
    vector<int> b{1};
    cout << &b << ' ' << b[0] << '\n';
    return b;
}(), 0);

cout << &a.first << ' ' << a.first[0] << '\n';

但现在 a.first 的地址与 b 不同:

0x62fdf0 1
0x62fdc0 1

但是如果我删除外部无用的 pair,代码将工作(即相同的地址和相同的值)。为什么?我怎样才能使 pair 起作用?


编辑

在尝试理解@cigien 的评论后,我将旧代码大幅缩减为

pair<int &&, int> a(1, 2);
cout << a.first << '\n';

提示我错误 warning: '<anonymous>' is used uninitialized in this function [-Wuninitialized]。然后我读了 .

我试图通过阅读 pair 构造函数 here 的定义来消除警告。这导致以下(更香草)代码:

struct s_t {
    int && first = 0;
} var;

int main() {
    var.first = 2;
    cout << var.first << '\n';
    cout << var.first << '\n';
}

这段代码警告消失了。但是这段代码的输出是:

2
0

老实说,这给我留下了零线索。感谢任何帮助。

您正在创建 临时对象,对它们进行(右值)引用,然后在底层对象消失后尝试使用这些引用。

在第一个例子中,[]() -> vector<int> { ... }() 是类型为 vector<int> 的纯右值(创建对象的指令)表达式。 pair 构造函数需要对真实对象的引用,而不是创建对象的指令,因此 调用构造函数之前,调用 lambda 并临时 vector<int> 被建造。然后构造函数将对该对象的引用存储到 pair 中,然后在构造函数完成并且控制权返回给您后销毁该对象。因此,该对的第一个元素现在是垃圾。

在第二种情况下,构造函数获取对临时对象 vector<int> 的引用(并且以相同的方式创建临时对象),但是这次,而不是存储引用(即构造一个 vector<int>&& 来自 vector<int>&&),它从引用构造了一个 vector<int>。从 vector<int>&& 构造 vector<int> 是由 vector<int> 的构造函数的特定重载处理的(此重载具有特殊名称“移动构造函数”)。此构造函数获取临时对象内堆分配的句柄,并简单地将所有权授予该对内的对象。现在,一旦临时文件被销毁,实际数据仍然存在,但句柄(即 vector<int> 对象)本身位于不同的位置。

你的最后一个例子和第一个例子有同样的问题。初始化 var 时,0int 纯右值)不引用真实对象,因此您不能仅将引用 var.first 绑定到它。 0 物化为一个临时对象,var.first 指向那个对象,然后临时对象被销毁。 var.first 现在是悬而未决的,你不能用它做任何事情,所以代码的其余部分是 UB,找出“幕后”出了什么问题没有多大意义(尽管它看起来确实很有趣)。事实上,如此无用 从这样的临时对象初始化引用成员 var 的初始化是非法的(应该是一个“硬”编译错误)缺陷报告 1696。(但旧的编译器可能会接受它,甚至当前的编译器可能(错误地)将其降级为警告。)

现在,如果您真的真的想在 pair 中“直接”构造 vector<int> 而无需调用其移动构造函数,您可以这样做,创建一个暂停构造 vector<int> 直到构造 pair 的字段。

template<typename F>
struct initializing {
    F self;
    initializing(F self) : self(std::move(self)) { }
    operator decltype(auto)() { return self(); }
};

现在说

pair<vector<int>, int> a(
    initializing([]() -> vector<int> {
        vector<int> b{1};
        cout << &b << ' ' << b[0] << '\n';
        return b;
    }), 0);

现在,lambda 是匿名闭包类型的纯右值。它具体化为一个临时对象,并且通过将对该临时对象的引用传递给 initializing 的构造函数来生成 initializing<that_anonymous_type> 纯右值。现在,该纯右值也被具体化为一个临时值,它调用构造函数并通过调用闭包类型的移动构造函数(在本例中为 no-op)构造 initializingself 字段对临时闭包类型对象的引用。然后对该临时 initializing 对象的引用被传递给 pair 的构造函数,该构造函数从 initializing 构造一个 vector<int>。这将调用 initializing 中定义的转换函数,并将结果对象设置为该对的未初始化字段。然后,转换函数调用 lambda,结果对象也设置为该对的字段。然后 lambda 通过在 return 处移动 b 直接初始化该结果对象(对的第一个元素)(或者,当应用 NRVO 时,b 成为结果的名称 object/first 对的元素,并在 lambda 的顶部初始化)。这样,除了在 lambda 中写入的一两个“可见”之外,没有 vector 构造函数被调用,如果应用 NRVO,ba.first 的地址将是相同的。稍微危险一点的版本是

template<typename F>
struct initializing {
    F &&self;
    initializing(F &&self) : self(std::forward<F>(self)) { }
    operator decltype(auto)() { return std::forward<F>(self)(); }
};

这也消除了从 lambda 中具体化的对象的移动。