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.
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)).
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
Eto denote the type of the expressione. (In other words,Eis the equivalent ofstd::remove_reference_t<decltype((e))>.)...A structured binding declaration then performs the binding in one of three possible ways, depending on
E:... ifEis a non-union class type..., then the names are bound to the accessible data members ofE....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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With