Decomposition of tuple with structured bindings into const, and const& variables - c++

Trying to understand working of structured bindings with const and references in particular as std::tuple is decomposed into named variables.
In the following case, it makes sense that a would be of type const int, with int& for b since int& const == int&, but how come the type of a1 isn't const int&? Is & only applied to the return object of get()?
int x;
std::tuple<int, int&> get()
{
return std::tuple<int, int&>(9, x);
}
int main()
{
const auto [a, b] = get();
const auto& [a1, b1] = get();
static_assert(std::is_same_v<const int, decltype(a)>);
static_assert(std::is_same_v<int&, decltype(b)>);
static_assert(std::is_same_v<const int, decltype(a1)>);
static_assert(std::is_same_v<int&, decltype(b1)>);
}
According to cpp-insights, that's how unpacking works. It's clear how it's const int& a1. however static_assert claims otherwise. Why the conflict? How else would the return from get() be decomposed?
const std::tuple<int, int &> __get12 = get();
const int && a = std::get<0UL>(static_cast<const std::tuple<int, int &> &&>(__get12));
int & b = std::get<1UL>(static_cast<const std::tuple<int, int &> &&>(__get12));
const std::tuple<int, int &> & __get13 = get();
const int & a1 = std::get<0UL>(__get13);
int & b1 = std::get<1UL>(__get13);
In simple terms, that's how I imagined would happen but it doesn't seem like it:
const auto& t = get();
const int& a1 = std::get<0>(t);
int& b1= std::get<1>(t);
EDIT:
The following works then that means structured binding does indeed not discard references, and perhaps it's just what decltype does as it returns the type of the element only not including the reference?
std::tuple<int> tx = std::make_tuple(42);
auto& [xz] = tx;
decltype(xz) yz = 0; // int& yz = 0;
static_assert(std::is_same_v<int, decltype(yz)>);
xz = 31; // tx<0> => 31

Structured bindings are not regular variables and are treated differently by decltype. The output of cppinsights is only a close approximation. cppreference says:
decltype(x), where x denotes a structured binding, names the referenced type of that structured binding. In the tuple-like case, this is the type returned by std::tuple_element, which may not be a reference even though a hidden reference is always introduced in this case. This effectively emulates the behavior of binding to a struct whose non-static data members have the types returned by tuple_element, with the referenceness of the binding itself being a mere implementation detail.
The reference qualifier (&) does apply to the intermediary object (named __get31 in your question).
Conceptually, it helps to compare with a struct used in this way:
struct tup {
int a;
int& b;
};
tup& value = get();
In that case, even though decltype(value) is tup &, decltype(value.a) is int, just like the structured binding a1 of a std::tuple<int, int&> & has a decltype of int.

Related

cv-qualifiers and rvalue-reference

Suppose we have this piece of code shown as below, the question is that why the cv qualifier (const) for "c" is not kept which the behavior is distinct from "v"?
int main(int argc, char **argv) {
int x{};
int y{};
const auto [v] = std::tuple<int>(x);
const auto [c] = std::tuple<int&&>(std::move(y));
decltype(v) vv = 10; // vv -> const int;
decltype(c) cc = 100; // cc -> int&&;
return 0;
}
Also, can I mimic the same type deduction process with template argument deduction somehow like below?
template<class T>
void foo(T t) { // here should be T rather than universal reference;
// mimic the same behavior as above somehow ...
}
Doubt 2:
For the code as below, it seems the "auto" inference for "Structured Binding" does not align the same rule as the normal usage of "auto"?
What I expect is that for the first "auto", the decltype(v) should be the type of const int rather than int& like the second one since I do not specify a "&" beside "auto. So, any special rules for "Structured Binding" with "auto"?
int main(int argc, char **argv) {
int x{};
const auto [v] = std::tuple<int&>(x); // v -> int&;
static_assert(std::is_same_v<decltype(v), int&>);
int& rx = x;
const auto c = rx; // c -> const int;
static_assert(std::is_same_v<decltype(c), const int>);
return 0;
}
Given
const auto [v] = std::tuple<T>(x);
The type decltype(v) is T const, i.e. const-qualified T. If T is int, then decltype(v) is int const (which also may be written const int). If T is int&&, then decltype(v) is int&& const which is int&& (not const int&& which is a reference to const int). The type int&& const is the same as int&& because references are always effectively const. The objects they refer to may be mutable, but references themselves in C++ are immutable.
With template type deduction without universal references, you cannot mimic this type transformation (add const to T) as far as I can tell. But there is a type transformation trait std::add_const_t<T>.
Update for Doubt 2
The structured binding
const auto [v] = std::tuple<int&>(x); // v -> int&;
is not analogous to
int& rx = x;
const auto c = rx; // c -> const int;
It is instead analogous to
const auto e = std::tuple<int&>(x);
auto&& v = std::get<0>(std::move(e));
The const qualification applies to the tuple, not to the binding of v. The reference qualifier or lack of it applies to the tuple. The binding of v is always reference-like.
The oddness is actually in the other case:
const auto [v] = std::tuple<int>(x);
Still v is reference-like, but decltype(v) is int. The difference is that bindings in structured bindings are aliases, not references. They are different names for the things referred to, but don't themselves have reference types.
So:
const auto [v] = std::tuple<T>(x);
is most analagous to:
const auto e = std::tuple<T>(x);
auto&& r = std::get<0>(std::move(e));
(introduce v as a name for that which r refers to)
where that third line is not something we have the ability to write.
As Jeff Garret mentioned, const will apply to the entire auto type deduced. If the type is int&, for example, then const auto will be the same as auto const, which is the same as int& const. The reference is const, not the data.
Now, what can you do about this? You don't have to stop using auto. You can simply specify const auto&. auto will deduce to int, and the whole type will be const int& (equivalently int const&). This is how you get your data to be const.

Why auto drops the reference in return value? [duplicate]

Please take a look at the following simple code:
class Foo
{
public:
Foo(){}
~Foo(){}
Foo(const Foo&){}
Foo& operator=(const Foo&) { return *this; }
};
static Foo g_temp;
const Foo& GetFoo() { return g_temp; }
I tried to use auto like this:
auto my_foo = GetFoo();
I expected that my_foo will be a constant reference to Foo, which is the return type of the function. However, the type of auto is Foo, not the reference. Furthermore, my_foo is created by copying g_temp. This behavior isn't that obvious to me.
In order to get the reference to Foo, I needed to write like this:
const auto& my_foo2 = GetFoo();
auto& my_foo3 = GetFoo();
Question: Why does auto deduce the return type of GetFoo as an object, not a reference?
Read this article: Appearing and Disappearing consts in C++
Type deduction for auto variables in C++0x is essentially the same as
for template parameters. (As far as I know, the only difference
between the two is that the type of auto variables may be deduced from
initializer lists, while the types of template parameters may not be.)
Each of the following declarations therefore declare variables of type
int (never const int):
auto a1 = i;
auto a2 = ci;
auto a3 = *pci;
auto a4 = pcs->i;
During type deduction for template parameters and auto variables, only
top-level consts are removed. Given a function template taking a
pointer or reference parameter, the constness of whatever is pointed
or referred to is retained:
template<typename T>
void f(T& p);
int i;
const int ci = 0;
const int *pci = &i;
f(i); // as before, calls f<int>, i.e., T is int
f(ci); // now calls f<const int>, i.e., T is const int
f(*pci); // also calls f<const int>, i.e., T is const int
This behavior is old news, applying as it does to both C++98 and
C++03. The corresponding behavior for auto variables is, of course,
new to C++0x:
auto& a1 = i; // a1 is of type int&
auto& a2 = ci; // a2 is of type const int&
auto& a3 = *pci; // a3 is also of type const int&
auto& a4 = pcs->i; // a4 is of type const int&, too
Since you can retain the cv-qualifier if the type is a reference or pointer, you can do:
auto& my_foo2 = GetFoo();
Instead of having to specify it as const (same goes for volatile).
Edit: As for why auto deduces the return type of GetFoo() as a value instead of a reference (which was your main question, sorry), consider this:
const Foo my_foo = GetFoo();
The above will create a copy, since my_foo is a value. If auto were to return an lvalue reference, the above wouldn't be possible.
You can take simple answer as granted by MSVC Technical documentation:
Using auto drops references, const qualifiers, and volatile
qualifiers.
You don't need a function to achieve similar results. Consider:
int var;
const int & cref = var;
auto avar = cref;
The type of avar will be int not const int & nor int &.
To go a bit deeper into this we can go to cppreference.com and refer to template argument deduction analogy.
For example, given const auto& i = expr;, the type of i is exactly the type of the argument u in an imaginary template template<class U> void f(const U& u).
We tend to intuitively use templates and until it works we never really bother about template argument deduction rules. These rules are in fact quite complex. But we can make the "imaginary template for auto" non-imaginary and test what would have happened if we provided auto i = expr; to the template. In our case the template may look like this:
template<class U>
void f(U u)
{
cout << type_name<decltype(u)>() << "\n";
}
Let's feed it with variables with types of our interest:
int i = 0;
int & ir = i;
const int & cir = i;
f(i);
f(ir);
f(cir);
And the what's at the output?
int
int
int
The same thing would have happened with auto as predicted by MSVC docs :)
Here's the code if you wanted to play.
Here's also example from MS website if it disappeared one day
// cl.exe /analyze /EHsc /W4
#include <iostream>
using namespace std;
int main( )
{
int count = 10;
int& countRef = count;
auto myAuto = countRef;
countRef = 11;
cout << count << " ";
myAuto = 12;
cout << count << endl;
}
In the previous example, myAuto is an int, not an int reference, so the output is 11 11, not 11 12 as would be the case if the reference qualifier hadn't been dropped by auto

Why does "const auto [x, y]" not behave as expected when binding to reference types?

The following code snippet is excerpted from cppref:
std::tuple<int, int&> f();
auto [x, y] = f();
// decltype(x) is int
// decltype(y) is int&
const auto [z, w] = f();
// decltype(z) is const int
// decltype(w) is int&
My question is at the last line:
Why is decltype(w) int& rather than const int&?
Jarod42 answered the question the question in the comments, let me just cite the relevant part of the standard here, from [dcl.struct.bind]¹:
Given the type Ti designated by std​::​tuple_­element​::​type, variables are introduced with unique names ri of type “reference to Ti” initialized with the initializer ([dcl.init.ref]), where the reference is an lvalue reference if the initializer is an lvalue and an rvalue reference otherwise.
Each vi is the name of an lvalue of type Ti that refers to the object bound to ri; the referenced type is Ti.
Hence in const auto [z, w] = f();, you have const T1 with T1 being int and const T2 with T2 being int&. As const modifies what's on its left, this becomes int& const and results in int&.
Note that int& const becoming int& is only possible in template argument substitution, i.e., this won't compile:
int n = 42;
int& const doesntWork = n; // Error: 'const' qualifiers cannot be applied to 'int&'
but this does:
template <class T> void f(const T t)
{
++t;
}
int n = 42;
f<int&>(n);
where the identical contraction from int& const to int& as above takes place.
¹ Thanks to #cpplearner for pointing me to the exact paragraph here.
It would be the reference itself rather than the referenced value that would be const. As references aren't modifiable anyway there is no such thing as a constant reference.

Providing tuple-like structured binding access for a class

I'm trying to support tuple-like structured binding access for a class. For simplicity, I'll use the following class in the rest of this post:
struct Test
{
int v = 42;
};
(I'm aware that this class supports structured bindings out of the box but let's assume it does not.)
To enable tuple-like access to the member of Test, we must specialize std::tuple_size and std::tuple_element:
namespace std
{
template<>
struct tuple_size<Test>
{
static const std::size_t value = 1;
};
template<std::size_t I>
struct tuple_element<I, Test>
{
using type = int;
};
}
And the last part we need is either Test::get<i> or a function get<i>(Test) in Test's namespace. Let's implement the latter:
template<std::size_t I>
int get(Test t)
{
return t.v;
}
This works. However, I would like to return a reference to Test's member, just like std::get(std::tuple), for example. Therefore, I implement get as follows:
template<std::size_t I>
int& get(Test& t)
{
return t.v;
}
template<std::size_t I>
const int& get(const Test& t)
{
return t.v;
}
With this version, however, the following code
auto test = Test{};
auto [v] = test;
produces an error (GCC 7.1):
binding reference of type ‘std::tuple_element<0, Test>::type& {aka int&}’ to ‘const int’ discards qualifiers
So it seems as if the get<i>(const Test&) overload is selected for the structured binding. Since this overload returns a const int&, and v acts like a non-const reference to int, the code fails to compile.
According to this, however, the line auto [v] = test; should be roughly equivalent to
auto e = test;
std::tuple_element<0, Test>::type& v = get<0>(e)
Which does work since it uses the get<i>(Test&) overload.
Any ideas on why my implementation of get does not work for structured bindings?
The problem is that auto [v] is a non-reference declaration, so test is copied and the copy of test is passed to get as an xvalue.
So you need to add an rvalue qualified get:
template<std::size_t I>
int&& get(Test&& t)
{
return std::move(t.v);
}

C++11 auto: what if it gets a constant reference?

Please take a look at the following simple code:
class Foo
{
public:
Foo(){}
~Foo(){}
Foo(const Foo&){}
Foo& operator=(const Foo&) { return *this; }
};
static Foo g_temp;
const Foo& GetFoo() { return g_temp; }
I tried to use auto like this:
auto my_foo = GetFoo();
I expected that my_foo will be a constant reference to Foo, which is the return type of the function. However, the type of auto is Foo, not the reference. Furthermore, my_foo is created by copying g_temp. This behavior isn't that obvious to me.
In order to get the reference to Foo, I needed to write like this:
const auto& my_foo2 = GetFoo();
auto& my_foo3 = GetFoo();
Question: Why does auto deduce the return type of GetFoo as an object, not a reference?
Read this article: Appearing and Disappearing consts in C++
Type deduction for auto variables in C++0x is essentially the same as
for template parameters. (As far as I know, the only difference
between the two is that the type of auto variables may be deduced from
initializer lists, while the types of template parameters may not be.)
Each of the following declarations therefore declare variables of type
int (never const int):
auto a1 = i;
auto a2 = ci;
auto a3 = *pci;
auto a4 = pcs->i;
During type deduction for template parameters and auto variables, only
top-level consts are removed. Given a function template taking a
pointer or reference parameter, the constness of whatever is pointed
or referred to is retained:
template<typename T>
void f(T& p);
int i;
const int ci = 0;
const int *pci = &i;
f(i); // as before, calls f<int>, i.e., T is int
f(ci); // now calls f<const int>, i.e., T is const int
f(*pci); // also calls f<const int>, i.e., T is const int
This behavior is old news, applying as it does to both C++98 and
C++03. The corresponding behavior for auto variables is, of course,
new to C++0x:
auto& a1 = i; // a1 is of type int&
auto& a2 = ci; // a2 is of type const int&
auto& a3 = *pci; // a3 is also of type const int&
auto& a4 = pcs->i; // a4 is of type const int&, too
Since you can retain the cv-qualifier if the type is a reference or pointer, you can do:
auto& my_foo2 = GetFoo();
Instead of having to specify it as const (same goes for volatile).
Edit: As for why auto deduces the return type of GetFoo() as a value instead of a reference (which was your main question, sorry), consider this:
const Foo my_foo = GetFoo();
The above will create a copy, since my_foo is a value. If auto were to return an lvalue reference, the above wouldn't be possible.
You can take simple answer as granted by MSVC Technical documentation:
Using auto drops references, const qualifiers, and volatile
qualifiers.
You don't need a function to achieve similar results. Consider:
int var;
const int & cref = var;
auto avar = cref;
The type of avar will be int not const int & nor int &.
To go a bit deeper into this we can go to cppreference.com and refer to template argument deduction analogy.
For example, given const auto& i = expr;, the type of i is exactly the type of the argument u in an imaginary template template<class U> void f(const U& u).
We tend to intuitively use templates and until it works we never really bother about template argument deduction rules. These rules are in fact quite complex. But we can make the "imaginary template for auto" non-imaginary and test what would have happened if we provided auto i = expr; to the template. In our case the template may look like this:
template<class U>
void f(U u)
{
cout << type_name<decltype(u)>() << "\n";
}
Let's feed it with variables with types of our interest:
int i = 0;
int & ir = i;
const int & cir = i;
f(i);
f(ir);
f(cir);
And the what's at the output?
int
int
int
The same thing would have happened with auto as predicted by MSVC docs :)
Here's the code if you wanted to play.
Here's also example from MS website if it disappeared one day
// cl.exe /analyze /EHsc /W4
#include <iostream>
using namespace std;
int main( )
{
int count = 10;
int& countRef = count;
auto myAuto = countRef;
countRef = 11;
cout << count << " ";
myAuto = 12;
cout << count << endl;
}
In the previous example, myAuto is an int, not an int reference, so the output is 11 11, not 11 12 as would be the case if the reference qualifier hadn't been dropped by auto