Consider I have a custom type (which I can extend):
struct Foo {
int a;
string b;
};
How can I make an instance of this object assignable to a std::tie
, i.e. std::tuple
of references?
Foo foo = ...;
int a;
string b;
std::tie(a, b) = foo;
Failed attempts:
Overloading the assignment operator for tuple<int&,string&> = Foo
is not possible, since assignment operator is one of the binary operators which have to be members of the left hand side object.
So I tried to solve this by implementing a suitable tuple-conversion operator. The following versions fail:
operator tuple<int,string>() const
operator tuple<const int&,const string&>() const
They result in an error at the assignment, telling that "operator =
is not overloaded for tuple<int&,string&> = Foo
". I guess this is because "conversion to any type X + deducing template parameter X for operator=" don't work together, only one of them at once.
Imperfect attempt:
Hence I tried to implement a conversion operator for the exact type of the tie:
The assignment now works since types are now (after conversion) exactly the same, but this won't work for three scenarios which I'd like to support:
- If the tie has variables of different but convertible types bound (i.e. change
int a;
tolong long a;
on the client side), it fails since the types have to fully match. This contradicts the usual use of assigning a tuple to a tuple of references which allows convertible types.(1) - The conversion operator needs to return a tie which has to be given lvalue references. This won't work for temporary values or const members.(2)
- If the conversion operator is not const, the assignment also fails for a
const Foo
on the right hand side. To implement a const version of the conversion, we need to hack away const-ness of the members of the const subject. This is ugly and might be abused, resulting in undefined behavior.
I only see an alternative in providing my own tie
function + class together with my "tie-able" objects, which makes me force to duplicate the functionality of std::tie
which I don't like (not that I find it difficult to do so, but it feels wrong to have to do it).
I think at the end of the day, the conclusion is that this is one drawback of a library-only tuple implementation. They're not as magic as we'd like them to be.
EDIT:
As it turns out, there doesn't seem to be a real solution addressing all of the above problems. A very good answer would explain why this isn't solvable. In particular, I'd like someone to shed some light on why the "failed attempts" can't possibly work.
(1): A horrible hack is to write the conversion as a template and convert to the requested member types in the conversion operator. It's a horrible hack because I don't know where to store these converted members. In this demo I use static variables, but this is not thread-reentrant.
(2): Same hack as in (1) can be applied.
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 sametuple
of references (in order to match the non-templated assignment operator taking atuple
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:
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, aconst Foo
could be modified via its attached tie.Example usage with non-exact component types of the tie (note the
long long
vsint
):will print
Live demo
So this solves problems 1. + 3. by introducing some space overhead.
Why the current attempts fail
std::tie(a, b)
produces astd::tuple<int&, string&>
. This type is not related tostd::tuple<int, string>
etc.std::tuple<T...>
s have several assignment-operators:std::tuple<T...>
U...
, that takes astd::tuple<U...>
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 forstd::tie(a,b) = foo
. If you add a conversion operator toFoo
, 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 typeFoo
.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 along
), the conversion operator ofFoo
has to be a template: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:
You need to publicly derive from a type the templated assignment-operator matches exactly.
Offer a non-
explicit
conversion to the type the non-templated copy-operator expects, so it will be used.In both cases, your type must contain the elements you want to assign, no way around it.
On coliru: http://coliru.stacked-crooked.com/a/315d4a43c62eec8d
This kind of does what you want right? (assumes that your values can be linked to the types of course...)
Major downside is that each member can only be accessed by their types, now, you could potentially get around this with some other mechanism (for example, define a type per member, and wrap the reference to the type by the member type you want to access..)
Secondly the conversion operator is not explicit, it will convert to any tuple type requested (may be you don't want that..)
Major advantage is that you don't have to explicitly specify the conversion type, it's all deduced...