Make custom type "tie-able" (compatible with std::tie)

C++C++11TuplesStd

C++ Problem Overview


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:

  • operator tuple<int&,string&>() const   Demo
  • operator tuple<int&,string&>()   Demo

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:

  1. If the tie has variables of different but convertible types bound (i.e. change int a; to long 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)
  2. 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)
  3. 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.

C++ Solutions


Solution 1 - C++

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).

Solution 2 - C++

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

Solution 3 - C++

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.

Solution 4 - C++

This kind of does what you want right? (assumes that your values can be linked to the types of course...)

#include <tuple>
#include <string>
#include <iostream>
#include <functional>
using namespace std;


struct Foo {
    int a;
    string b;
    
    template <template<typename ...Args> class tuple, typename ...Args>
    operator tuple<Args...>() const {
        return forward_as_tuple(get<Args>()...);
    }
    
    template <template<typename ...Args> class tuple, typename ...Args>
    operator tuple<Args...>() {
        return forward_as_tuple(get<Args>()...);
    }
    
    private:
    // This is hacky, may be there is a way to avoid it...
    template <typename T>
    T get()
    { static typename remove_reference<T>::type i; return i; }

    template <typename T>
    T get() const
    { static typename remove_reference<T>::type i; return i; }

};

template <>
int&
Foo::get()
{ return a; }

template <>
string&
Foo::get()
{ return b; }

template <>
int&
Foo::get() const
{ return *const_cast<int*>(&a); }

template <>
string&
Foo::get() const
{ return *const_cast<string*>(&b); }

int main() {
    Foo foo { 42, "bar" };
    const Foo foo2 { 43, "gah" };
    
    int a;
    string b;
    
    tie(a, b) = foo;
    cout << a << ", " << b << endl;
    
    tie(a, b) = foo2;
    cout << a << ", " << b << endl;
    
}

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...

Solution 5 - C++

This code works for me. I'd love it if someone could point out anything wrong with it.

Simple Version on Compiler Explorer

More Generic Version on Compiler Explorer

#include <tuple>
#include <cassert>

struct LevelBounds final
{
  int min;
  int max;

  operator std::tuple<int&, int&>() { return {min, max}; }
};

int main() {
    
    int a, b;

    auto lb = LevelBounds{30, 40};
    std::tie(a, b) = lb;

    assert(30 == a);
    assert(40 == b);    
    return 0;
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionleemesView Question on Stackoverflow
Solution 1 - C++dypView Answer on Stackoverflow
Solution 2 - C++DeduplicatorView Answer on Stackoverflow
Solution 3 - C++leemesView Answer on Stackoverflow
Solution 4 - C++NimView Answer on Stackoverflow
Solution 5 - C++ShortView Answer on Stackoverflow