16

Consider the following program:

#include <tuple>
#include <vector>
#include <iostream>
#include <type_traits>

template <class T>
struct ordered {};

template <class... T>
struct ordered<std::tuple<T...>>
{
    using type = /* a reordered tuple */;
};

template <class T>
using ordered_t = typename ordered<T>::type;

int main(int argc, char* argv[])
{
    using type1 = std::tuple<char, std::vector<int>, double>;
    using type2 = std::tuple<std::vector<int>, double, char>;
    std::cout << std::is_same_v<type1, type2> << "\n"; // 0
    std::cout << std::is_same_v<ordered_t<type1>, ordered_t<type2>> << "\n"; // 1
    return 0;
}

The ordered helper has to reorder types in a tuple, such that two tuples with the sames types, but ordered differently lead to the same tuple type: which can be the first one, the second one, or even another one: it just has to have the same size, and the same elements but in a unique order (regardless of this order).

Is it possible to do this at compile-time using template metaprogramming techniques?

Vincent
  • 57,703
  • 61
  • 205
  • 388
  • 5
    Is it possible? Yes. Whatever answer you get, Andrei Alexandrescu wrote about doing such things in "Modern C++ Design" way back in 2001. The details undoubtedly will differ, but the core idea is the same. – StoryTeller - Unslander Monica Feb 10 '18 at 18:11
  • 3
    @StoryTeller: What if `T != T2` but `sizeof(T1) == sizeof(T2)`? How would you *uniquely* sort them so that `std::is_same` works as expected? – Nawaz Feb 10 '18 at 18:28
  • @Nawaz - Same way one does it for any other sort of data. Force the user to supply an arbitrary ordering if the library can't. It's not impossible. Could be as simple as adding a template specialization or two. – StoryTeller - Unslander Monica Feb 10 '18 at 18:33
  • @StoryTeller: and that turns the algorithm into a monster since it'd be very difficult to provide template specialization or such for each such pair which are equals in size. then why the question would be, why would one use such sort metafn to begin with? why not do this manually? Seems like C++ is lacking in this area. There should be a way to order types at compile-time.. a compile-time operator, equivalent to `std::less<>` for types, or such. – Nawaz Feb 10 '18 at 18:38
  • @Nawaz - Not really a monster. A user of the library knows the handful of types they use. So they can provide specialization. And as for why using such a sort? Likely to minimize the resulting tuple size. Make it fit in a cache line, all those good stuff. – StoryTeller - Unslander Monica Feb 10 '18 at 18:41
  • @StoryTeller: To minimize the resulting tuple-size, one doesn't need to sort in any unique way when `T1` and `T2` are equal size, as any ordering (for such pair) would work then. – Nawaz Feb 10 '18 at 18:44
  • @Nawaz - Yes, but what does that have to do with the issue of having more types which can be reordered to reduce a tuple size? The fact two are different and may be presented in any order is immaterial to the goal. – StoryTeller - Unslander Monica Feb 10 '18 at 18:49
  • @StoryTeller: Exactly. But the OP seems to be interested in the producing types which can be compared using `std::is_same`. So not sure about OP's eventual goal. – Nawaz Feb 10 '18 at 18:51
  • @Nawaz - Well, maybe typeid could be used to break the tie in C++20. I hear there's a "`constexpr` all the things" proposal. Maybe... – StoryTeller - Unslander Monica Feb 10 '18 at 19:01
  • If your types do not repeat in tuple and these types reside in single .dll/.so I can write you a template function. Otherwise it's a lost fight – bartop Feb 10 '18 at 21:44
  • 1
    @Vincent: Is it possible that this whole question is the XY problem? If all you need is to compare two tuples for equivalency, than probably there is no need to sort them - searching using type aliases techniques, even though n^2 might be faster than sorting two collections and comparing them (well, I wouldn't bet on it, but it's worth checking). – Michał Łoś Feb 12 '18 at 10:01

3 Answers3

19

The hard part is coming up with a way to order types. Sorting a type list by a predicate is a chore, but is doable. I'll focus here on just the comparison predicate.

One way is to just create a class template that defines a unique id for each type. That works and makes for an easy comparator to write:

template <typename T, typename U>
constexpr bool cmp() { return unique_id_v<T> < unique_id_v<U>; }

But coming up with these unique ids is a hurdle that isn't necessarily feasible. Do you register them all in one file? That doesn't scale super well.

What would be great is if we could just... get the names of all the types as compile time strings. Reflection will give us that, and then this problem is trivial. Until then, we could do something slightly more dirty: use __PRETTY_FUNCTION__. Both gcc and clang are okay with using that macro in a constexpr context, although they have different formats for this string. If we have a signature like:

template <typename T, typename U>
constexpr bool cmp();

Then gcc reports cmp<char, int> as "constexpr bool cmp() [with T = char; U = int]" while clang reports it as "bool cmp() [T = char, U = int]". It's different... but close enough that we can use the same algorithm. Which is basically: figure out where T and U are in there and just do normal string lexicographic comparison:

constexpr size_t cstrlen(const char* p) {
    size_t len = 0;
    while (*p) {
        ++len;
        ++p;
    }
    return len;
}

template <typename T, typename U>
constexpr bool cmp() {
    const char* pf = __PRETTY_FUNCTION__;
    const char* a = pf + 
#ifdef __clang__
        cstrlen("bool cmp() [T = ")
#else
        cstrlen("constexpr bool cmp() [with T = ")
#endif
        ;

    const char* b = a + 1;
#ifdef __clang__
    while (*b != ',') ++b;
#else
    while (*b != ';') ++b;
#endif
    size_t a_len = b - a;
    b += cstrlen("; U = ");
    const char* end = b + 1;
    while (*end != ']') ++end;
    size_t b_len = end - b;    

    for (size_t i = 0; i < std::min(a_len, b_len); ++i) {
        if (a[i] != b[i]) return a[i] < b[i];
    }

    return a_len < b_len;
}

with some tests:

static_assert(cmp<char, int>());
static_assert(!cmp<int, char>());
static_assert(!cmp<int, int>());
static_assert(!cmp<char, char>());
static_assert(cmp<int, std::vector<int>>());

It's not the prettiest implementation, and I'm not sure it's meaningfully sanctioned by the standard, but it lets you write your sort without having to manually and carefully register all your types. And it compiles on clang and gcc. So maybe it's good enough.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • In `boost::hana`, representations of types are equality comparable `static_assert( boost::hana::type_c == boost::hana::type_c );`, but unfortunately not less-than comparable because the type are not `Orderable`. This here would be a nice addition to Hana to make them orderables, a potential pitfall is that the order can end up being platform/compiler dependent. – alfC Jun 03 '18 at 10:36
  • Given `template S{};`, do you have any ideas on `cmp, S<42u>>()`? Clang and gcc both doesn't have the type of an `auto` argument in the signature. BTW here's a more complete implementation https://godbolt.org/z/My3ktR. – Passer By Apr 08 '19 at 03:20
  • @PasserBy Uh... well that's unfortunate. `typeid` differentiates the two, but that doesn't really help here. – Barry Apr 08 '19 at 12:25
1

This is a slight modification of the method presented by Barry, that works with Visual Studio. Instead of creating compile-time string that stores name of a function:

template <typename T, typename U>
constexpr bool cmp() 

this method directly compares names of two types returned by type_name< T>::name(). Barry's method does not work when names of types T and U returned by macro __PRETTY_FUNCTION__ are separated by comma, since comma may also separate template arguments, when T or U are a class or function templates.

// length of null-terminated string
constexpr size_t cstrlen(const char* p) 
{
    size_t len = 0;
    while (*p) 
    {
        ++len;
        ++p;
    }

    return len;
}

// constexpr string representing type name
template<class T>
struct type_name
{
    static constexpr const char* name() 
    { 
        #if defined (_MSC_VER)
            return __FUNCSIG__; 
        #else
            return __PRETTY_FUNCTION__;
        #endif
    };
};

// comparison of types based on type names
template<class T1, class T2>
constexpr bool less()
{
    const char* A   = type_name<T1>::name();
    const char* B   = type_name<T2>::name();

    size_t a_len    = cstrlen(A);
    size_t b_len    = cstrlen(B);

    size_t ab_len   = (a_len < b_len) ? a_len : b_len;

    for (size_t i = 0; i < ab_len; ++i) 
    {
        if (A[i] != B[i]) 
            return A[i] < B[i];
    }

    return a_len < b_len;
}

// simple checks
template<class ... Type>
struct list;

static_assert(less<list<int, void, list<void, int>>, list<int, void, list<void, void>>>());
static_assert(less<list<int, void, list<void, int>>, list<int, void, list<void, int>, int>>());

This method works on VS. I am not sure if it works on Clang or GCC.

Fabio A.
  • 2,517
  • 26
  • 35
Pawel Kowal
  • 121
  • 2
  • It actually does work on Clang and GCC, bar a minor error (`name()` needs to return a `const char *`: I've submitted an edit to your post). See it live on godbolt: https://godbolt.org/z/6FwH7x – Fabio A. Mar 18 '20 at 20:49
1

tl;dr: Get the type name at compile-time, and order by that.

Previous answers are, in my opinion, a bit idiosyncratic - at least in implementation.

At this point we have a very nice, multi-compiler-supporting, function for obtaining a type's name as a compile-time string, as a string view. I'll only quote its signature here:

template <typename T>
constexpr std::string_view type_name();

This constitutes an injective mapping from types to compile-time-comparable values. Given those, you can easily implement a selection-sort-like procedure to get the relative order of each type. Finally, you assemble a new tuple using those orders.

einpoklum
  • 118,144
  • 57
  • 340
  • 684
  • This answer worked great for me, but I guess I am the only upvote atm since it does not contain full working code... But yeah you solved my problem(with difference that I did not bother to trim the name since I do not care what content is as long as it enables me to order types). – NoSenseEtAl Nov 04 '20 at 15:34
  • @NoSenseEtAl: Fixed a typo and added the missing include. Seems to compile now. – einpoklum Nov 04 '20 at 17:18
  • See this [working on GodBolt](https://godbolt.org/z/Tae1bM). – einpoklum Nov 04 '20 at 17:26
  • nice, nitpick: add the msvc to link also. people will not see output since it is not supported, but it is nice to see it compiles with latest msvc on godbolt. – NoSenseEtAl Nov 04 '20 at 17:33
  • flags I suggest are /std:c++latest /O2 – NoSenseEtAl Nov 04 '20 at 17:34
  • 1
    @NoSenseEtAl: That actually makes more sense on the page of the question about determing type names rather than here. So I actually just decided to remove the whole thing. Plus, on that page, I've suggested a slightly more robust (though longer) implementation. – einpoklum Nov 04 '20 at 17:39