结构化绑定声明为什么会调用析构函数?

3
我想使用结构化绑定声明将结构体的成员引入作用域。我希望优化器能够删除我实际上没有使用的变量,但这不是我的主要问题。我被一个意料之外的析构函数调用所困扰:
#include <iostream>

using namespace std;

struct S {
  int x = 0;
  S() { cout << "S: " << this << endl; }
  ~S() { cout << "~S: " << this << endl; }
};

// Why does f() call ~S()?
void f(S const& s) {
  auto [x] {s};
  cout << "f: " << x << endl;
}

void g(S const& s) {
  auto& [x] {s};
  cout << "g: " << x << endl;
}

void h(S const& s) {
  auto x = s.x;
  cout << "h: " << x << endl;
}

int main(int, char**) {
  S s;
  f(s);
  g(s);
  h(s);
}

运行上述程序会产生以下结果:
S: 0x7fff32c81778
f: 0
~S: 0x7fff32c81740
g: 0
h: 0
~S: 0x7fff32c81778

https://godbolt.org/z/cjeTnvx8G

main()函数末尾的析构函数调用是可以预料的。但为什么f() 会调用 ~S() 呢?

我已经阅读了cppreference上的文档,但显然我对C++的内部运作理解不够,无法发现这会导致析构函数的调用。


2
为什么不会呢?x 是自动存储中的 S。你是否期望它绑定为 const S& - Brian61354270
2
此外,将this输出到构造函数和析构函数中有助于您了解创建和销毁的对象是什么,并且不仅仅打印出〜Sf:。如果您在析构函数中看到一个神秘地出现而没有对应构造函数中的this,那么就知道您没有跟踪所有被调用的构造函数。 - PaulMcKenzie
关于Brian61354270和Holt: x是一个int。请注意,operator<<未为S定义,因此如果xS,程序将无法编译。 - jonas-schulze
RE PaulMcKenzie:我已经更新了问题和godbolt的链接。 - jonas-schulze
1
@jonas-schulze 0x7fff32c81740 -- 你会发现你没有追踪正在被销毁的对象的构造函数。像 S 的复制构造函数和移动构造函数之类的东西都没有在你的代码中输出。 - PaulMcKenzie
请看这个链接:https://godbolt.org/z/MTjf1ojc5。它输出了一个复制品正在被创建,并且正是这个复制品被销毁了。 - PaulMcKenzie
2个回答

5

我并不是以编程语言专家的身份来表述,而是希望通过一些直觉来解释结构化绑定的工作原理。

当你阅读到这里时

auto [key, value] = foo();

暂时用一个虚构的名称替换 [key, value] 部分:

auto invented_name = foo();

现在很明显,该代码保留了由foo()返回的值的实例。此外,如果有任何引用限定符或cv限定符,它们都适用于这个虚构的名称。也就是说,
const auto& [key, value] = foo();

变成

const auto& invented_name = foo();

对于&&也是如此。参考和cv限定符不适用于名称keyvalue(但其他规则使它们基本上表现得像已应用)。

名称keyvalue现在仅成为invented_name的第一个和第二个结构成员的别名(或get<0>(invented_name)get<1>(invented_name)的返回值)。


否则,e 的定义就像在声明中使用它的名称而不是 [标识符列表] 一样。 - jonas-schulze

4

https://en.cppreference.com/w/cpp/language/structured_binding

结构化绑定声明首先引入一个唯一命名的变量(这里用e表示)来保存初始化程序的值,如下所示:...
我们使用E来表示表达式e的类型。(换句话说,E相当于std::remove_reference_t<decltype((e))>。)...
然后,结构化绑定声明根据E的不同方式之一执行绑定:如果E是非联合类类型,则名称将绑定到E的可访问数据成员....
标识符列表中的每个标识符都成为一个lvalue的名称,该名称按声明顺序引用e的下一个成员。
我的理解是,在像你这样的情况下,结构化绑定实际上从表达式创建一个本地的e,然后使用类似元组的方法将引用绑定到初始化程序列表。
void f(S const& s) {
  using E = std::remove_reference_t<decltype(s)>;
  E e{s}; //expression
  const int& x = e.x; //initializer list

  cout << "f: " << x << endl;
}

void g(S const& s) {
  using E = std::remove_reference_t<decltype(s)>;
  E& e{s}; //expression
  const int& x = e.x; //initializer list

  cout << "g: " << x << endl;
}

因此,您所看到的是临时对象E e;的析构函数。

Chris_se友情提供了https://cppinsights.io/s/239fcf14链接,这是一个显示C++代码显式解释的工具。


初始化列表不应该是x吗?另外,我只看到S的构造函数被调用一次,但是没有被调用两次以供潜在的临时变量使用。 - jonas-schulze
@jonas-schulze:有一个句子用词错误,已经修正。E e是临时变量。 - Mooing Duck
请注意,您的代码无法编译。operator<<未定义为S,而x不是S - jonas-schulze
@jonas-schulze:我的问题一开始非常错误,所以我删除了它并进行了修正,但显然错过了一个地方,x不是S。我也修复了这个问题。 - Mooing Duck
1
补充一下,C++ Insights 可以告诉你编译器正在做什么:https://cppinsights.io/s/239fcf14 - chris_se
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接