Understanding SFINAE : declarations of member functions outside of partially specialized classes, and templates within variadic classes

122 Views Asked by At

What I want to do:

Create an 'InstructionSet' object that

  • is held generically (without having to specify types) by a container (of pointers) elsewhere
  • holds a variable number of instruction objects (with possible duplicate types) with type satisfying concept 'is_instruction_type'
  • has 'extract' function that produces a new 'InstructionSet' that contains all instructions of a single particular type

I have tried to implement this by using inheritance on a variadic class such that recursion could be used to store each instruction of correct type; a pointer to type 'InstructionSet<>' can then have a virtual function used on it to be able to access the bottom of the inheritance, and recur over the object.

//Define recursively
template<typename...Args> requires (is_instruction_type<Args> && ...)
class InstructionSet;


//Base
template<>
class InstructionSet<> 
{
public:
    InstructionSet() {};
    virtual ~InstructionSet() {};

    template<typename U>
    auto extract()
    {
        return extractHelper()->extract();
    };

protected:
    virtual InstructionSet<>* extractHelper();
};

//Recur
template<typename T, typename ...Rest> requires is_instruction_type<T>
class InstructionSet<T, Rest...> : public InstructionSet<Rest...>
{
public:
    InstructionSet(T&& t, Rest&&...rest);
    InstructionSet(T&& t, InstructionSet<Rest...>&& set);

    virtual ~InstructionSet() {};

    template<typename U> requires std::same_as<T, U>
    auto extract();

    template<typename U> requires !std::same_as<T, U>
    auto extract();


    virtual InstructionSet<T, Rest...>* extractHelper()
    {
        return this;
    };

private:
    T _instruction;

};



template<typename T, typename ...Rest> requires is_instruction_type<T>
inline InstructionSet<T, Rest...>::InstructionSet(T&& t, Rest&& ...rest) : InstructionSet<Rest...>(std::forward<Rest>(rest)...),
_instruction(std::forward<T>(t))
{

}
template<typename T, typename ...Rest> requires is_instruction_type<T>
inline InstructionSet<T, Rest...>::InstructionSet(T&& t, InstructionSet<Rest...>&& set)
{
    _instruction = std::forward<T>(t);
    std::construct_at((InstructionSet<Rest...>*)this, std::move(set));
}


template<typename T, typename ...Rest> requires is_instruction_type<T>
template<typename U> requires std::same_as<T, U>
auto InstructionSet<T, Rest...>::extract<U>()
{
    return InstructionSet(_instruction, std::move(((InstructionSet<Rest...>*)this)->extract()));
}

template<typename T, typename ...Rest> requires is_instruction_type<T>
template<typename U> requires (!std::same_as<T, U>)
auto InstructionSet<T, Rest...>::extract<U>()
{
    return ((InstructionSet<Rest...>*)this)->extract();
}

The two questions that I have:

  • How should 'extract' be defined outside of the class under multiple templates?
  • How should class members be treated inside the partially specialized class definition? (should a full class definition also come into it?)

I have tried different combinations of angle brackets being included/excluded as well as trying to move the function definitions to inside the class, but all throw errors.

I think that I understand the basics of SFINAE for classes with type definitions and static variables, however I am confused about, and cannot seem to find many examples, for more complex classes.

2

There are 2 best solutions below

3
Ted Lyngmo On BEST ANSWER

I think you could simplify it by not making specializations and by using a std::tuple for storing the instructions instead.

First a helper function to create a std::index_sequence with all the indices in a parameter pack where the type is T:

// get an index_sequence for where all T's are in Ts...
template <class T, class... Ts>
constexpr auto get_indices_for() {
    constexpr auto inds_sel = []<std::size_t... Is>(std::index_sequence<Is...>) {
        std::array<std::size_t, sizeof...(Ts)> indices{};

        std::size_t sel = 0;
        (..., (std::same_as<T, std::tuple_element_t<Is, std::tuple<Ts...>>> &&
               (indices[sel++] = Is)));

        return std::pair{indices, sel};
    }(std::make_index_sequence<sizeof...(Ts)>{});

    return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
        return std::index_sequence<inds_sel.first[Is]...>{};
    }(std::make_index_sequence<inds_sel.second>{});
}

Then if you store the instructions in a std::tuple, extraction could look like this:

struct InstructionSetBase {
    virtual ~InstructionSetBase() = default;
};

template <class... Ts>
    requires(is_instruction_type<Ts> && ...)
class InstructionSet : public InstructionSetBase {
public:
    template <class... Us>
    InstructionSet(Us&&... us) : _instructions{std::forward<Us>(us)...} {}

    template <std::size_t... Is>  // extract by indices
    InstructionSet<std::tuple_element_t<Is, std::tuple<Ts...>>...> extract();

    template <class U>  // extract all of a certain by type
    auto extract() {
        return [this]<std::size_t... Is>(std::index_sequence<Is...>) {
            return extract<Is...>();
        }(get_indices_for<U, Ts...>());
    }

private:
    std::tuple<Ts...> _instructions;
};

template <class... Ts>
InstructionSet(Ts&&...) -> InstructionSet<std::remove_cvref_t<Ts>...>;

template <class... Ts>
    requires(is_instruction_type<Ts> && ...)
template <std::size_t... Is>
InstructionSet<std::tuple_element_t<Is, std::tuple<Ts...>>...>
InstructionSet<Ts...>::extract() {
    return {std::get<Is>(_instructions)...};
}

Demo

0
Igor Tandetnik On

I came up with this:

#include <tuple>
#include <type_traits>

namespace internal {

// The number of occurrences of U in Ts
template <typename U, typename... Ts>
constexpr std::size_t CountSameType() {
    return (std::is_same_v<U, Ts> + ...);
}

// The index of Ith occurrence of U in {First, Rest...}
template <std::size_t I, typename U, typename First, typename ... Rest>
struct IthIndex : public std::integral_constant<std::size_t,
  1 + IthIndex<I - std::is_same_v<U, First>, U, Rest...>::value>
{};

template <typename U, typename ... Rest>
struct IthIndex<0, U, U, Rest...> : public std::integral_constant<std::size_t, 0> {};

// An index_sequence mapping occurrences of U in Ts,
// given a sequence {0, 1, CountSameType<U, Ts...>}
template <typename U, typename... Ts, std::size_t... Is>
std::index_sequence<IthIndex<Is, U, Ts...>::value...> 
    SameTypeIndexes(std::index_sequence<Is...>);

// Produce a tuple that's a subset of the given tuple,
// containing only elements at given indexes.
template <typename Tuple, std::size_t ... Is>
auto ExtractFromTuple(const Tuple& t, std::index_sequence<Is...>) {
    return std::make_tuple(std::get<Is>(t)...);
}

}  // namespace internal

template <typename... Ts>
class InstructionSet;

template <typename... Ts>
auto MakeInstructionSet(const std::tuple<Ts...>& t) { return InstructionSet<Ts...>(t); }

template <typename... Ts>
class InstructionSet {
    std::tuple<Ts...> instructions;
public:
  InstructionSet(Ts&&... args) : instructions(args...) {}
  InstructionSet(const std::tuple<Ts...>& t) : instructions(t) {}

  template <typename U>
  auto extract() {
     constexpr std::size_t N = internal::CountSameType<U, Ts...>();
     using SameTypeIndexes =
         decltype(internal::SameTypeIndexes<U, Ts...>(std::make_index_sequence<N>{}));
     return MakeInstructionSet(internal::ExtractFromTuple(instructions, SameTypeIndexes{}));
  }
};

Demo