3

I have an application where I am constructing tuple out of a parameter pack:

template<class... T>
struct foo {
 typedef std::tuple<T...> A;
}

And I have another tuple-type B defined as:

typedef std::tuple<char, int> B;

Is there a way to obtain a new tuple C, such that C is a set subtraction of the types in the sets A and B at compile-time?

A = {int, double, float, bool}
B = {char, int}
C = A - B = {double, float, bool} // answer

Some more context to the complete problem:

template<class... T>
struct foo {
 using first_type = typename std::tuple_element<0, // n'th type 
                     std::tuple<T...>>::type;
}

I can do the above to find the 0th type, but I am interested in the first type that is not contained in set B above. So, somewhat like a compile-time search to find the first valid type.

Blizzard
  • 1,117
  • 2
  • 11
  • 28

2 Answers2

3

Here's a compile time set subtraction, use it as set_diff<tuple1, tuple2>::type:

template <class...> class set_diff;

template <class... A, class... B>
struct set_diff<std::tuple<A...>, std::tuple<B...>>
{
    template <class J, class... Js>
    constexpr static bool exists_in = std::disjunction_v<std::is_same<J, Js>...>;

    using type = decltype(tuple_cat(std::declval<
      std::conditional_t<exists_in<A, B...>, std::tuple<>, std::tuple<A>>>()...));
};

Demo

Note that:

  • The order of the operands matters (according to the problem description)
  • There are various "optimizations" of compilation complexity possible (e.g. sorting the types and removing duplicates prior to diff etc.).

My solution works in two steps:

  1. Marks the "unwanted" types as tuple<>
  2. Generates the desired type using tuple_cat, which evicts empty tuples

A more self-explanatory (alas more verbose) demo can be found here Demo

Lorah Attkins
  • 5,331
  • 3
  • 29
  • 63
  • You know `std::tuple_cat` accepts any number of tuples, not just two? Unfortunately, it is not required to accept tuple-like, that would open even more possibilities. – Deduplicator May 02 '21 at 00:32
  • 1
    @Deduplicator [Of course](http://coliru.stacked-crooked.com/a/8d9f5529390140a9). I think you made a typo, it **is** required to accept tuple-like. – Lorah Attkins May 02 '21 at 00:47
  • @Deduplicator Ok I see what you mean , we don't even need a filter class – Lorah Attkins May 02 '21 at 00:49
  • According to [cppreference](//en.cppreference.com/w/cpp/utility/tuple/tuple_cat) it can but need not accept tuple-like aside from `std::tuple`. No idea why it shouldn't, but hey. Also see [the current draft](//eel.is/c++draft/tuple.creation#12) – Deduplicator May 02 '21 at 00:53
  • Wow, that's so much simpler than what I did. Maybe replace `tuple_cat` with `std::tuple_cat` though, ADL is pretty confusing. – asynts May 02 '21 at 07:34
  • Notice also that `set_diff, std::tuple>::type` would be `std::tuple<>` and not `std::tuple` (contrary to runtime [`std::set_difference`](https://en.cppreference.com/w/cpp/algorithm/set_difference)) – Jarod42 May 03 '21 at 13:02
  • @Jarod42 That's why I mention, you could "sort" types or "remove duplicates" as an optimization. It behaves as `set difference` not `multiset difference`. – Lorah Attkins May 03 '21 at 14:12
  • From OP intention, "set difference" is expected, but from title, we might expect "multiset difference", that's why I added the note to clarify. Moreover, not sure that those ("runtime") *"optimizations"* would be valid at compile time though. – Jarod42 May 03 '21 at 14:31
  • @Jarod42 Sorting types is never a runtime operation, you can sort types in a meta-function. I don't know how you deduced this is a "runtime" optimization. Extending my solution is possible but I like the current simplicity and compactness, so why do it when it's not explicitly requested in the question. I'd love to see the multi-set flavor if you can provide an alternative answer, ideally w/o compile time recursion (omg tried that and it looks awful - you just have to turn the filter found in the linked Demo into a Stencil). – Lorah Attkins Aug 08 '21 at 18:53
  • I meant that runtime and compile time doesn't count the same as "unit" operations. For runtime, it is mostly the number of conditions, or number of "call"; for compile time, it is the number of instantiations (so some `O(n)` for runtime could be `O(1)` for compile time). An optimization to transform `O(n²)` into `O(n log n)` at runtime, might not apply successfully for compile time (and even give worse result). – Jarod42 Aug 09 '21 at 07:59
1

Disclaimer. Please don't put code like this into an actual code base. If you do, this implementation is by no means "optimal" and should be cleaned up before use (maybe add an answer if you do).

I found this an interesting challenge, and found the following solution:

// Try this live at https://compiler-explorer.com/z/cqebd81ss

#include <type_traits>

template <typename... Pack>
struct ClassList;

template<typename...>
struct Join {
};
template<typename... Pack1, typename... Pack2>
struct Join<ClassList<Pack1...>, ClassList<Pack2...>> {
    using Type = ClassList<Pack1..., Pack2...>;
};

template<typename...>
struct RemoveSingleTypeFromList {
};
template<typename Target, typename... Pack>
struct RemoveSingleTypeFromList<Target, ClassList<Pack...>> {
    using Type = ClassList<Pack...>;
};
template<typename Target, typename Parameter, typename... Pack>
struct RemoveSingleTypeFromList<Target, ClassList<Parameter, Pack...>> {
    using Type = typename Join<
        std::conditional_t<
            std::is_same_v<Target, Parameter>,
            ClassList<>,
            ClassList<Parameter>
        >,
        typename RemoveSingleTypeFromList<Target, ClassList<Pack...>>::Type
    >::Type;
};

template<typename... Pack>
struct RemoveTypesFromList {
};
template<typename... Types>
struct RemoveTypesFromList<ClassList<>, ClassList<Types...>> {
    using Type = ClassList<Types...>;
};
template<typename Target, typename... RemainingTargets, typename... Types>
struct RemoveTypesFromList<ClassList<Target, RemainingTargets...>, ClassList<Types...>> {
    using Type = typename RemoveSingleTypeFromList<
        Target,
        typename RemoveTypesFromList<
            ClassList<RemainingTargets...>,
            ClassList<Types...>
        >::Type
    >::Type;
};

// A few test cases to verify that it works

static_assert(std::is_same_v<
    typename RemoveTypesFromList<
        ClassList<int, float>,
        ClassList<float, double, int, long>
    >::Type,
    ClassList<double, long>>);

static_assert(std::is_same_v<
    typename RemoveTypesFromList<
        ClassList<float>,
        ClassList<float, double, float>
    >::Type,
    ClassList<double>>);

static_assert(std::is_same_v<
    typename RemoveTypesFromList<
        ClassList<int, int>,
        ClassList<float, double, float>
    >::Type,
    ClassList<float, double, float>>);

If you are interested in understanding exactly how this works, remember that this is a solution and not something you could write top-to-bottom. When reading template code like this, I find it most useful to build a similar implementation step by step.

asynts
  • 2,213
  • 2
  • 21
  • 35
  • Wow, this is brilliant! Exactly what I needed, I'll try and build a similar version myself, now trying to envision if it can be made simpler. Thank you! – Blizzard May 01 '21 at 19:04
  • `using Type` is curious. I would expect `using type` like everybody else uses. – Deduplicator May 02 '21 at 00:30
  • @Deduplicator That's just a naming convention, personally, I work on projects which don't use the STL and have CamelCase for types. – asynts May 02 '21 at 07:27
  • 1
    *"Funny"* to see that most projects I saw use convention which doesn't match std's one. – Jarod42 May 03 '21 at 14:33
  • @Jarod42 By using CamelCase versus snake_case there are many name collisions avoided, no idea why the standard library uses snake_case for everything. I wonder if that has a history. – asynts May 03 '21 at 14:34