Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

A tuple-like container that only allows unique and non-convertible types

I needed a type that could generalize several types that cannot be converted to each other. For example: There are 2 types A and B. A is not convertible to B, B is not convertible to A. That is, these are 2 "completely different" types. And there is a type unique_tuple<A, B>. An instance of this type can contain A and B, like a regular std::tuple, but it differs in that in a context where A is expected, it behaves like A, and in a context where B is expected, it behaves like B. The basic idea is that: "unique_tuple<..., T, ...> behaves as much as possible like T in the context where T is expected". I would like to have a tuple that can provide this behavior without using explicit type conversions. That is, if a function takes T as an argument, then it should also take unique_tuple<..., T, ...>. Of course, if it can also take U, then func(unique_tuple<..., T, ..., U, ...>()) should throw an error.

void proc1(int arg);
void proc2(MyClass arg);
void proc3(double arg);
void proc4(int arg);
void proc4(MyClass arg);
...
unique_tuple<int, MyClass> my_tuple; // OK
unique_tuple<int, double> my_tuple2; // Compilation error: int and double can be cast to each other.
my_tuple = 42; // OK
my_tuple = 3.14; // OK my_tuple.as<int> == 3
my_tuple = MyClass{}; // OK
proc1(my_tuple); // OK
proc2(my_tuple); // OK
proc3(my_tuple); // OK
proc4(my_tuple); // Error. Call is ambiguous

Note that unique_tuple<int,double>, as a legitimate type, loses its right to exist in this case, since it leads to ambiguity in ANY attempt to use it in the context of the target type.
An additional but optional requirement is that unique_tuple be such that unique_tuple<int, std::string> is the same as unique_tuple<std::string, int>. This can be a simple using that selects a specific container implementation using some template parameter ordering algorithm. The specific implementation of some ordering can even be compiler-dependent. I am not interested in the specific data representation in memory and serialization (yet).

I'm pretty sure that someone has already done this at a more professional level than I can afford. However, I could not find ready-made solutions. The closest in meaning that I found is boost::mpl. Yes, this library solves part of my problems in processing a package of template parameters at the compilation stage, but I did not find an implementation of the tuple itself. Perhaps I did not fully understand the concept of boost::mpl and my problem is solved with its help.

Question: Are there C++ idioms that provide a tuple/container of unique/non-convertible types? I would appreciate any examples or links to open source repositories with similar functionality.

Applicability: Let's say I want to create a graph whose elements will be annotated with different data. And different algorithms for working with this graph know only about their types. Or to model physical processes, I need objects to be able to have different characteristics, as if they were something different at the same time, depending on the context.

Added ------------------
I fixed the usage examples because they had a bug. Actually

int j;
std::string s;
s = j;
s = 1;

is a legal entry. So the code

unique_tuple<int, std::string> my_tuple;
my_tuple = 42;

should result in a call ambiguity error

like image 919
Halturin Evgeniy Avatar asked Sep 20 '25 12:09

Halturin Evgeniy


2 Answers

You could create a class that inherits from a std::tuple<Ts...> but restricts the allowed types so that no contained type is convertible to another contained type.

You'd also need user defined conversion operators and assignment operators for all the types.

First a concept to be able to check if any type is convertible to another:

template <class T, class... Ts>
struct convertible_to_another {
    static constexpr bool value = ((std::convertible_to<T, Ts> || ...) ||
                                   (std::convertible_to<Ts, T> || ...) ||
                                   convertible_to_another<Ts...>::value);
};
template <class T>
struct convertible_to_another<T> {
    static constexpr bool value = false;
};
template <class... Ts>
concept ConvertibleToAnother = convertible_to_another<Ts...>::value;

Then a class template to be able to assign and implicitly cast to one of the contained types:

template <class UTup, class T>
struct caster {
    UTup& bref() { return *static_cast<UTup*>(this); }
    const UTup& bref() const { return *static_cast<const UTup*>(this); }
    T& ref() { return std::get<T>(bref()); }
    const T& ref() const { return std::get<T>(bref()); }

    // implicit conversions
    operator T&() { return ref(); }
    operator const T&() const { return ref(); }

    // assignment operators
    UTup& operator=(const T& val) {
        ref() = val;
        return bref();
    }
    UTup& operator=(T&& val) {
        ref() = std::move(val);
        return bref();
    }
};

With that, your unique_tuple would inherit from one caster per type in Ts... plus std::tuple<Ts...>:

template <class... Ts>
    requires(not ConvertibleToAnother<Ts...>)
class unique_tuple : public caster<unique_tuple<Ts...>, Ts>...,
                     public std::tuple<Ts...> {
    using base = std::tuple<Ts...>;

public:
    using base::base;
    using base::operator=;
    using caster<unique_tuple<Ts...>, Ts>::operator=...;
};

This would let your test cases pass / fail where specified.

Demo

like image 56
Ted Lyngmo Avatar answered Sep 23 '25 02:09

Ted Lyngmo


This is a small variant on Ted's answer and reuses its helper classes (please upvote his post if you like this one).

template <class UTup, class T>
struct tbase {
    operator T&() { return t; }
    operator T const&() const { return t; }
    UTup& operator=(T const& u) {
        t = u;
        return *static_cast<UTup*>(this);
    }

   private:
    [[no_unique_address]] T t;
};

template <class... Ts>
    requires(not ConvertibleToAnother<Ts...>)
struct unique_tuple : tbase<unique_tuple<Ts...>, Ts>... {
    using tbase<unique_tuple<Ts...>, Ts>::operator=...;
};

Demo

You can think of it as having unique_tuple derive from all of the types passed as arguments (then you can naturally static_cast it to a reference to any of them), but with a small wrapper to reduce the drawbacks.

This is closer to an implementation of std::tuple, essentially you would add an index parameter to tbase for that. Of course it still doesn't do the type reordering and other niceties mentioned in the question.

Personally, I think I'd rather use something like properties (cf Boost.Graph).

like image 41
Marc Glisse Avatar answered Sep 23 '25 03:09

Marc Glisse