Matthew Cotton
Matthew Cotton

Reputation: 75

Specializing a template for a container of type T

Given I have a template setup to do something on a type such as...

template<typename T>
class SimpleTemplate
{
private:
  T m_obj;
public:
  void operator()() { m_obj.DoSomething(); }
};

And I want to handle the case where I have a collection of type T the same way. I currently have a template setup like so for a vector...

template<typename T>
class SimpleTemplate<std::vector<T>>
{
private:
  std::vector<T> m_collection;
public:
  void operator()()
  {
    for (auto&& obj : m_collection) obj.DoSomething();
  }
};

Now I want to also support sets, unordered_sets and so on. I could write a template for each collection but I feel like this should be a perfect job for a template, only I can't figure out how it should be written, or even if it can be? Can I do something like template<typename C<T>>?

Upvotes: 5

Views: 1023

Answers (3)

LoS
LoS

Reputation: 1833

Since C++20, it is possible to constraint the template class with the concept std::ranges::forward_range. It is essentially a refinement of range for which the std::ranges::begin() function returns a model of std::forward_iterator.

Example:

template <std::ranges::forward_range T>
class SimpleTemplate
{
private:
  T  m_collection;

public:
  void operator()()
  {
    for (auto&& obj : m_collection)
      obj.DoSomething();
  }
};

Upvotes: 1

AVH
AVH

Reputation: 11516

As mentioned by Geoffroy, you can use a trait to detect whether T can be iterated over. You then use this to select the correct specialization.

So start off with the "is_iterable" trait shown by Jarod42 here:

// Code by Jarod42 (https://stackoverflow.com/a/29634934).
#include <iterator>
#include <type_traits>

namespace detail
{
    // To allow ADL with custom begin/end
    using std::begin;
    using std::end;

    template <typename T>
    auto is_iterable_impl(int)
    -> decltype (
        begin(std::declval<T&>()) != end(std::declval<T&>()), // begin/end and operator !=
        void(), // Handle evil operator ,
        ++std::declval<decltype(begin(std::declval<T&>()))&>(), // operator ++
        void(*begin(std::declval<T&>())), // operator*
        std::true_type{});

    template <typename T>
    std::false_type is_iterable_impl(...);

}

template <typename T>
using is_iterable = decltype(detail::is_iterable_impl<T>(0));

This gives you an is_iterable<T> trait which inherits from either std::true_type or std::false_type. Now use this with SFINAE to create two specializations:

template <class T, bool = is_iterable<T>::value>
class SimpleTemplate;

template <class T>
class SimpleTemplate<T, false> {
  private:
    T m_obj;

  public:
    SimpleTemplate (T obj) : m_obj(std::move(obj)) { }

    void operator() () { m_obj.DoSomething(); }
};

template <class T>
class SimpleTemplate<T, true> {
  private:
    T m_collection;

  public:
    SimpleTemplate (T obj) : m_collection(std::move(obj)) { }

    void operator() () {
      for (auto && obj : m_collection) { obj.DoSomething(); }
    }
};

Since both partial specializations are mutually exclusive for any given T, you won't get any errors about ambiguity.

Edit: Changed 2nd template argument into a bool instead of class. This makes it simple to fully specialize it in case the default behavior is unwanted.

E.g. for std::string, which for which is_iterable is true, simply do the following. Note that I added constructors to SimpleTemplate, I couldn't get the full specialization to inherit the base class' constructor otherwise.

template <>
class SimpleTemplate<std::string, true>
    : public SimpleTemplate<std::string, false> {
  // Inherit constructor.
  using base = SimpleTemplate<std::string, false>;
  using base::base;
};

Upvotes: 5

max66
max66

Reputation: 66230

Now I want to also support sets, unordered_sets and so on. I could write a template for each collection but I feel like this should be a perfect job for a template, only I can't figure out how it should be written, or even if it can be

Maybe you can use a template-template parameter

template <template <typename...> class C, typename... Ts>
class SimpleTemplate<C<Ts...>>
{
private:
  C<Ts...> m_collection;
public:
  void operator()()
  {
    for (auto&& obj : m_collection) obj.DoSomething();
  }
};

This should intercept std::(unordered_)(multi)set, std::vector, std::deque, etc.

Unfortunately doesn't intercept std::array, because it's second template parameter is a value, not a type.

Upvotes: 2

Related Questions