There are three adjectives here, which specify three orthogonal requirements:
- same
- unambiguous
- public
In order, it might help to look at counter-examples. In all cases, assume that template <class T> T make();
exists.
Counter-example for "same": there are two members of D
, but they are not members of the same base of D
- i
is a member of B
but j
is a member of D
:
struct B { int i; };
struct D : B { int j; };
auto [i, j] = make<D>(); // error
To fix this, either j
needs to be a direct member of B
or i
needs to be direct member of D
:
struct B { int i, j; };
struct D : B { };
auto [i, j] = make<D>(); // ok
struct B { };
struct D : B { int i, j; };
auto [i, j] = make<D>(); // ok
Counter-example for "unambiguous": there are two members of D
, they are both members of B
, but it's an ambiguous base class of D
.
struct B { int i; };
struct M1 : B { };
struct M2 : B { };
struct D : M1, M2 { };
auto [i, j] = make<D>(); // error
If B
were a virtual
base of both M1
and M2
, then this would be ok:
struct B { int i; };
struct M1 : virtual B { };
struct M2 : virtual B { };
struct D : M1, M2 { };
auto [i] = make<D>(); // ok
Counter-example for "public". This is the simplest one. If the members are in a private base, they aren't accessible anyway:
struct B { int i; };
struct D : private B { };
make<D>().i; // error, as-is
auto [i] = make<D>(); // error, non-public base, but really same reason
Note also that, as TC points out, the requirement is that the base be public, not that the members be accessible. That is, making the members accessible from the private base will still not work:
struct B { int i; };
struct D : private B { using B::i; };
make<D>().i; // ok now, due to the using-declaration
auto [i] = make<D>(); // still error, B is still private base
Of course, in all of these counterexample cases, just because all the members are not in the same, unambiguous, public base class of E
doesn't mean it's unusuable with structured bindings. It just means that you have to write out the bindings yourself:
struct B { int i; };
struct D : B { int j; };
namespace std {
template <> struct tuple_size<D> : std::integral_constant<int, 2> { };
template <size_t I> struct tuple_element<I, D> { using type = int; };
}
template <size_t I>
int& get(D& d) {
if constexpr (I == 0) { return d.i; }
else { return d.j; }
}
template <size_t I>
int const& get(D const& d) {
if constexpr (I == 0) { return d.i; }
else { return d.j; }
}
template <size_t I>
int&& get(D&& d) {
if constexpr (I == 0) { return std::move(d).i; }
else { return std::move(d).j; }
}
auto [i,j] = make<D>(); // now ok: we're in case 2 instead of 3