Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 714
leemes Avatar asked Oct 09 '14 11:10

leemes


2 Answers

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

like image 198
4 revs, 2 users 99% Avatar answered Nov 11 '22 09:11

4 revs, 2 users 99%


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

like image 7
Deduplicator Avatar answered Nov 11 '22 10:11

Deduplicator