2

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
    
xyf
  • 664
  • 1
  • 6
  • 16
  • I know that I can put references into tuples. It's allowed. It's legal. `std::tie` does that. But I will prefer to have my two remaining wisdom teeth yanked out before I will agree to write any code, myself, that does that. – Sam Varshavchik Feb 16 '23 at 00:57

2 Answers2

0

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.

Etienne Laurin
  • 6,731
  • 2
  • 27
  • 31
  • so basically `decltype` doesn't run the reference type but just the type of the element itself? does that mean the type of `a1` is `const int&` then? – xyf Feb 16 '23 at 01:21
  • No, its type is `const int`. See my expanded answer. – Etienne Laurin Feb 16 '23 at 01:36
  • `&` applies to the intermediary object but doesn't get propagated further but `const` does? – xyf Feb 16 '23 at 01:51
  • Yes. Just like the members of a const struct are const, but the members of a reference to a struct are not references. – Etienne Laurin Feb 16 '23 at 02:27
  • 1
    `decltype(value.a)` is `int`, not `const int` (unlike the structured binding case). – Barry Feb 16 '23 at 12:34
  • Oops, I tried to stretch that comparison too far. Removed `const` from the answer. – Etienne Laurin Feb 16 '23 at 15:18
  • @EtienneLaurin 1) If `a1` is still `const int`, then what's up with `decltype` naming a referenced type of the structured binding? Doesn't `decltype` return the exact type that's expected anyway? 2) What if `value` was `const tup`&`? Would `const` not make `tup::a` `const`? – xyf Feb 16 '23 at 16:43
  • do you mind providing more examples? (`const auto& tup`, `const tup`). So if you have `const tup value`, `decltype(value.a)` would get you `int` and not `const int` even though it technically is const – xyf Feb 17 '23 at 02:24
  • @xyf, for both `const tup& value` and `const tup value`, `decltype(value.a)` is `int`, which is the declared type of `a`. However, `decltype(value)` is `struct tup const &` and `struct tup const`, respectively. – pascal754 Jun 18 '23 at 00:38
0

Here is an example from cppreference(https://en.cppreference.com/w/cpp/language/structured_binding).

float x{};
char  y{};
int   z{};
 
std::tuple<float&, char&&, int> tpl(x, std::move(y), z);
const auto& [a, b, c] = tpl;
// using Tpl = const std::tuple<float&, char&&, int>;
// `a` names a structured binding that refers to x (initialized from get<0>(tpl))
// decltype(a) is std::tuple_element<0, Tpl>::type, i.e. float&
// `b` names a structured binding that refers to y (initialized from get<1>(tpl))
// decltype(b) is std::tuple_element<1, Tpl>::type, i.e. char&&
// `c` names a structured binding that refers to the third component of tpl, get<2>(tpl)
// decltype(c) is std::tuple_element<2, Tpl>::type, i.e. const int

In const auto& [a, b, c] = tpl;, & applies to the unnamed object, which is a reference to tpl, and const propagates to each identifier. const for a reference is shallow: analogously, a const pointer to a non-const object.

const auto& [a1, b1] = get(); in your example follows the same pattern. & is for the unnamed object, which is a reference to the returned tuple. const applies to a1 and b1, but b1 is a reference. So the types of a1 and b1 are const int and int &, respectively.

Each identifier is initialized with std::get, which always returns a reference, but their types are determined by std::tuple_element<i, E>::type, where i is the i-th element and E is the type of an expression.

In the following example,

std::tuple<int> tx = std::make_tuple(42);
auto& [xz] = tx;
decltype(xz) yz = 0;  // int& yz = 0; --> int yz = 0;
static_assert(std::is_same_v<int, decltype(yz)>);
xz = 31; // tx<0> => 31

the unnamed object is a reference to tx: the unnamed object == tx. The type of xz is int: the type of the zeroth element of the tuple. xz == zeroth element of unnamed object == zeroth element of tx, so xz = 31; modifies the value of tx.

FYI, Boost TypeIndex library gives accurate type information.

#include <iostream>
#include <boost/type_index.hpp>

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();

    using boost::typeindex::type_id_with_cvr;
    std::cout << type_id_with_cvr<decltype(a)>().pretty_name() << '\n';
    std::cout << type_id_with_cvr<decltype(b)>().pretty_name() << '\n';

    std::cout << type_id_with_cvr<decltype(a1)>().pretty_name() << '\n';
    std::cout << type_id_with_cvr<decltype(b1)>().pretty_name() << '\n';
}

Output:

int const
int & __ptr64
int const
int & __ptr64
pascal754
  • 189
  • 6