Reputation: 2621
I wonder why this choice is made. It would allow to write many functions in a very clear and neat way.. for instance:
int greatestCommonDivisor(int a, int b)
{
if (b > a)
std::tie(a, b) = { b, a };
while (b > 0)
std::tie(a, b) = { b, a % b };
return a;
}
Upvotes: 5
Views: 1284
Reputation: 275500
Why does
std::tie(a,b) = {b, a};
not compile?
{}
on the right hand side of an assignment can only call a non-explicit constructor of an argument of operator=
.
The operator=
overloads available are:
tuple& operator=( const tuple& other );
tuple& operator=( tuple&& other );
template< class... UTypes >
tuple& operator=( const tuple<UTypes...>& other );
template< class... UTypes >
tuple& operator=( tuple<UTypes...>&& other );
template< class U1, class U2 >
tuple& operator=( const pair<U1,U2>& p );
template< class U1, class U2 >
tuple& operator=( pair<U1,U2>&& p );
The template
operator overloads cannot deduce their types from {}
(note: this may change in C++17), leaving:
tuple& operator=( const tuple& other );
tuple& operator=( tuple&& other );
where tuple
is std::tuple<int&, int&>
in this case.
The tuple constructor for tuple<Ts...>
that perfect forwards element-wise construction is explicit (#3 on that list). {}
will not call an explicit constructor.
The conditionally non-explicit constructor takes Ts const&...
; it does not exist if the Ts
are non-copyable, and int&
is non-copyable.
So there are no viable types to construct from {int&, int&}
, and overload resolution fails.
Why does the standard not fix this? Well, we can do it ourselves!
In order to fix this, we'd have to add a special (Ts...)
non-explicit constructor to tuple
that only exists if the Ts
types are all references.
If we write a toy tuple:
struct toy {
std::tuple<int&, int&> data;
toy( int& a, int& b ):data(a,b) {} // note, non-explicit!
};
toy toy_tie( int& a, int& b ) { return {a,b}; }
and use it, you'll notice that
std::tie(a, b) = {b, a};
compiles and runs.
However,
std::tie(a, b) = { b, a % b };
does not, as a%b
cannot be bound to int&
.
We can then augment toy
with:
template<class...>
toy& operator=( std::tuple<int, int> o ) {
data = o;
return *this;
}
(+ defaulted special member functions. template<class...>
ensures it has lower priority than the special member functions, as it should).
This which lets assign-from {int,int}
. We then run it and... get the wrong result. The gcd of 5,20
is 20
. What went wrong?
toy_tie(a, b) = std::tie( b, a );
with both a
and b
bound to references is not safe code, and that is what
toy_tie(a, b) = { b, a };
does.
In short, doing this right is tricky. In this case, you need to take a copy of the right hand side before assigning to be safe. Knowing when to take a copy and when not to is also tricky.
Having this work implicitly looks error prone. So it is, in a sense, accidental that it doesn't work, but fixing it (while possible) looks like a bad idea.
Upvotes: 4
Reputation: 93294
std::initializer_list
is a homogeneous collection of items, while std::tuple
is heterogeneous. The only case where it makes sense to define a std::tuple::operator=
for std::initializer_list
is when the tuple is homogeneous and has the same size as the initializer list, which is a rare occurrence.
(Additional information in this question.)
Solution/workaround: you can use std::make_tuple
instead:
int greatestCommonDivisor(int a, int b)
{
if (b > a)
std::tie(a, b) = std::make_tuple(b, a);
while (b > 0)
std::tie(a, b) = std::make_tuple(b, a % b);
return a;
}
...or std::tuple
's constructor in C++17 (thanks to Template argument deduction for class templates):
int greatestCommonDivisor(int a, int b)
{
if (b > a)
std::tie(a, b) = std::tuple{b, a};
while (b > 0)
std::tie(a, b) = std::tuple{b, a % b};
return a;
}
Upvotes: 7