Make custom type "tie-able" (compatible with std::tie)
Why the current attempts fail
std::tie(a, b)
produces a std::tuple<int&, string&>
.
This type is not related to std::tuple<int, string>
etc.
std::tuple<T...>
s have several assignment-operators:
- A default assignment-operator, that takes a
std::tuple<T...>
- A tuple-converting assignment-operator template with a type parameter pack
U...
, that takes astd::tuple<U...>
- A pair-converting assignment-operator template with two type parameters
U1, U2
, that takes astd::pair<U1, U2>
For those three versions exist copy- and move-variants; add either a const&
or a &&
to the types they take.
The assignment-operator templates have to deduce their template arguments from the function argument type (i.e. of the type of the RHS of the assignment-expression).
Without a conversion operator in Foo
, none of those assignment-operators are viable for std::tie(a,b) = foo
.
If you add a conversion operator to Foo
,
then only the default assignment-operator becomes viable:
Template type deduction does not take user-defined conversions into account.
That is, you cannot deduce template arguments for the assignment-operator templates from the type Foo
.
Since only one user-defined conversion is allowed in an implicit conversion sequence, the type the conversion operator converts to must match the type of the default assignment operator exactly. That is, it must use the exact same tuple element types as the result of std::tie
.
To support conversions of the element types (e.g. assignment of Foo::a
to a long
), the conversion operator of Foo
has to be a template:
struct Foo {
int a;
string b;
template<typename T, typename U>
operator std::tuple<T, U>();
};
However, the element types of std::tie
are references.
Since you should not return a reference to a temporary,
the options for conversions inside the operator template are quite limited
(heap, type punning, static, thread local, etc).
There are only two ways you can try to go:
- Use the templated assignment-operators:
You need to publicly derive from a type the templated assignment-operator matches exactly. - Use the non-templated assignment-operators:
Offer a non-explicit
conversion to the type the non-templated copy-operator expects, so it will be used. - There is no third option.
In both cases, your type must contain the elements you want to assign, no way around it.
#include <iostream>
#include <tuple>
using namespace std;
struct X : tuple<int,int> {
};
struct Y {
int i;
operator tuple<int&,int&>() {return tuple<int&,int&>{i,i};}
};
int main()
{
int a, b;
tie(a, b) = make_tuple(9,9);
tie(a, b) = X{};
tie(a, b) = Y{};
cout << a << ' ' << b << '\n';
}
On coliru: http://coliru.stacked-crooked.com/a/315d4a43c62eec8d
As the other answers already explain, you have to either inherit from a tuple
(in order to match the assignment operator template) or convert to the exact same tuple
of references (in order to match the non-templated assignment operator taking a tuple
of references of the same types).
If you'd inherit from a tuple, you'd lose the named members, i.e. foo.a
is no longer possible.
In this answer, I present another option: If you're willing to pay some space overhead (constant per member), you can have both named members and tuple inheritance simultaneously by inheriting from a tuple of const references, i.e. a const tie of the object itself:
struct Foo : tuple<const int&, const string&> {
int a;
string b;
Foo(int a, string b) :
tuple{std::tie(this->a, this->b)},
a{a}, b{b}
{}
};
This "attached tie" makes it possible to assign a (non-const!) Foo
to a tie of convertible component types. Since the "attached tie" is a tuple of references, it automatically assigns the current values of the members, even though you initialized it in the constructor.
Why is the "attached tie" const
? Because otherwise, a const Foo
could be modified via its attached tie.
Example usage with non-exact component types of the tie (note the long long
vs int
):
int main()
{
Foo foo(0, "bar");
foo.a = 42;
long long a;
string b;
tie(a, b) = foo;
cout << a << ' ' << b << '\n';
}
will print
42 bar
Live demo
So this solves problems 1. + 3. by introducing some space overhead.