c++templatesc++17variadic-templatesclass-template

How to only add methods to class if type pack contains specific type?


I'm trying to create a class that can contain a type pack.

// Pack.hpp
template <typename... Types>
class Pack final
{
    Pack(const std::tuple<Types...> items) : items_(std::move(items)) {};

    std::tuple<Types...> items_;
};

But I would like Pack to expose some methods based on the Types inside of it. For example, I want to do this:

// Consumer.cpp
Person person{ ... };
Car car{ ... };
Pack<Person, Car> personAndCarPack{ { person, car } };
Pack<Person> personPack{ { person } };

// Ok
personAndCarPack.getCar();

// Ok
personAndCarPack.getPerson();

// Ok
personPack.getPerson();

// Shouldn't compile - getCar() shouldn't exist!
personPack.getCar();

I'm assuming I have to declare it more or less this way:

// Pack.hpp

template <typename... Types>
class Pack final 
{
    Pack(const std::tuple<Types...> items) : items_(std::move(items)) {};

    std::tuple<Types...> items_;

    Car getCar() 
    {
        return std::get<Car>(items_);
    }

    Person getPerson() 
    {
        return std::get<Person>(items_);
    }
};

But of course that exposes the methods in Pack regardless of the types.

The closest I've got is this:

typename std::enable_if<(std::is_same_v<Types, Car> && ...), Car>::type
getCar()
{
    return std::get<Car>(items_);
}

But it doesn't compile; I get

'enable_if' cannot be used to disable this declaration

in the std::is_same_v() call.

I am probably misunderstanding how it all works. I've found some answers that are somewhat related, but none with the right combination of factors according to what I need.

Is it possible to implement this functionality at all? If so, how?


Solution

  • Is it possible to implement this functionality at all? If so, how?

    Yes, it is possible. The minimal change solution is without std::enable_if/ SFINAE.

    // Method to get a Car from the Pack (enabled only if Car is in Types...)
    template <typename T = Car>
    constexpr auto getCar() -> decltype(std::get<Car>(items_))
    {
        return std::get<Car>(items_);
    }
    
    // Method to get a Person from the Pack (enabled only if Person is in Types...)
    template <typename T = Person>
    constexpr auto getPerson() -> decltype(std::get<Person>(items_))
    {
        return std::get<Person>(items_);
    }
    

    See live demo

    Keep in mind that the above does not disable the function from usage in unevaluated context (i.e. decltype(personPack.getCar()) x = personAndCarPack.getCar(); will still be valid).


    Alternatively, with std::enable_if_t and std::disjunction, you might achieve the same effect as well.

    // Method to get a Car from the Pack (enabled only if Car is in Types...)
    template <typename T = Car>
    constexpr auto getCar()
             -> std::enable_if_t<std::disjunction<std::is_same<Types, T>...>::value, T>
    {
        static_assert(std::is_same_v<T, Car>, "T must be of type Car!");
        return std::get<T>(items_);
    }
    
    // Method to get a Person from the Pack (enabled only if Person is in Types...)
    template <typename T = Person>
    constexpr auto getPerson()
             -> std::enable_if_t<std::disjunction<std::is_same<Types, T>...>::value, T>
    {
        static_assert(std::is_same_v<T, Person>, "T must be of type Person!");
        return std::get<T>(items_);
    }
    

    See live demo


    (For future readers) Since , however, the solution will be much simpler with requires constraints

    // Method to get a Car from the Pack (enabled only if Car is in Types...)
    constexpr auto getCar() requires (std::is_same_v<Types, Car> || ...)
    {
        return std::get<Car>(items_);
    }
    
    // Method to get a Person from the Pack (enabled only if Person is in Types...)
    constexpr auto getPerson() requires (std::is_same_v<Types, Person> || ...)
    {
        return std::get<Person>(items_);
    }
    

    See live demo