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 a std::tuple<U...>
  • A pair-converting assignment-operator template with two type parameters U1, U2, that takes a std::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:

  1. Use the templated assignment-operators:
    You need to publicly derive from a type the templated assignment-operator matches exactly.
  2. 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.
  3. 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.