Specializing a template for a container of type T
Asked Answered
S

3

5

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>>?

Saval answered 9/3, 2020 at 21:26 Comment(2)
Does this answer your question? Check if a variable is iterable?Programmer
I have read that and it might be what I'm looking for but I'm unsure how to apply it in this situation. It seems overly complex for what I'm trying to do. Perhaps the basic concept I'm attempting is not possible without the complications.Saval
H
5

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;
};
Hejira answered 9/3, 2020 at 23:22 Comment(4)
I'm guessing the benefit of doing it this way instead of the current answer using a template-template parameter is that you might have templates that accept the type but are not iterable or a container?Saval
@MatthewCotton I would say this better captures the intent of your question (as far as I understood it correctly). As max66 himself mentions his answer won't work for e.g. std::array. This solution should work with anything that you can write a range-based for-loop for.Hejira
@MatthewCotton Updated code so specializing for e.g. std::string is easy now.Hejira
Thank you. This is probably a much safer implementation of the more general version. Both of these answers have been super helpful.Saval
L
2

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.

Leatherette answered 9/3, 2020 at 21:58 Comment(2)
Great! This looks like it does what I want. I have realised however, that it also captures std::string but I want std::string handled with the basic template. I'm betting I can fix that with either some more specialisation or maybe something more sneaky.Saval
@MatthewCotton - my intent was only show you as intercept most containers but, yes, this solution give you a lot of false positives (intercept also container with elements that doesn't support the DoSomething() method (as std::string, as you have seen)). More sneaky can be the Darhuuk's solution that check if the type support begin()/end() and the contained element support DoSomething().Leatherette
L
1

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();
  }
};
Leaguer answered 5/7 at 20:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.