Why does structured binding declaration call destructor?

140 Views Asked by At

I would like to use a structured binding declaration to bring members of a struct into scope. I hope that the optimizer is able to remove the variables I don't actually use, but that's not my primary question. I got bitten by a destructor call I didn't expect:

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

Running above program yields:

S: 0x7fff32c81778
f: 0
~S: 0x7fff32c81740
g: 0
h: 0
~S: 0x7fff32c81778

https://godbolt.org/z/cjeTnvx8G

The destructor call at the end of main() is expected. But why does f() call ~S()?

I've read the documentation on cppreference but apparently I don't understand C++'s inner workings well enough to spot why this causes a destructor call.

2

There are 2 best solutions below

1
j6t On BEST ANSWER

I am wording this not as a language lawyer, but hope to give some intuition how structured bindings work.

When you read

auto [key, value] = foo();

replace for a moment the [key, value] part by an invented name:

auto invented_name = foo();

Now it is obvious that the code holds onto an instance of the value returned by foo(). Furthermore, if there are any reference specifiers or cv qualifiers, they apply to the invented name. I.e.,

const auto& [key, value] = foo();

becomes

const auto& invented_name = foo();

and ditto for &&. Reference and cv qualifiers do not apply to the names key and value (but other rules achieve that they basically behave as if they were applied).

The names key and value now merely become aliases for the first and second structure members of invented_name (or the return values of get<0>(invented_name) and get<1>(invented_name)).

8
Mooing Duck On

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

A structured binding declaration first introduces a uniquely-named variable (here denoted by e) to hold the value of the initializer, as follows:...

We use E to denote the type of the expression e. (In other words, E is the equivalent of std::remove_reference_t<decltype((e))>.)...

A structured binding declaration then performs the binding in one of three possible ways, depending on E:... if E is a non-union class type..., then the names are bound to the accessible data members of E....

Each identifier in identifier-list becomes the name of an lvalue that refers to the next member of e in declaration order

My reading of this is that, in cases like yours, a structured binding effectively creates a local e from the expression, and then uses tuple-like methods to bind references to the initializer list.

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

And therefore what you're seeing is the destructor of the temporary E e;

Chris_se has helpfully linked to https://cppinsights.io/s/239fcf14, which is a tool that shows explicit interpretations of C++ code.