ShaKeSPeaR
ShaKeSPeaR

Reputation: 163

Generalised function template for standard library containers

I'm trying to write a simple generalised function for iteration over container elements. Every element is converted to std::string (no matter how) and stored in another place. The basic version was trivial:

template<class Container>
void ContainerWork(const Container& c)
{
    for(const auto& elem : c) {
        /* convert to string and store*/
    }
}

Then it became necessary to add specialisation for containers with value type std::string and code transformed to:

template<typename T, template<typename, typename> class Container, class Allocator>
void ContainerWork(Container<T, Allocator> c)
{
    for(const T& elem : c) {
        /* convert to string and store*/
    }
}

template<template<typename, typename> class Container, class Allocator>
void ContainerWork(Container<std::string, Allocator> c)
{
    for(const std::string& elem : c) {
        /* frame elem in quotes*/
    }
}

It works great, but now I can use only sequenced containers (vector, list, etc.), but I want also use set and and unordered_set. Any ideas how do this without "copy-paste" realisation for container with 4 params? I am trying to play with decltype(Container)::value_type but with no luck.

I may use most of c++11 features (compiler - VS2012 or GCC 4.8.x)

Upvotes: 6

Views: 362

Answers (4)

Bj&#246;rn Pollex
Bj&#246;rn Pollex

Reputation: 76778

As far as I understand this, you could just use std::for_each with a polymorphic functor:

namespace detail {

    template<typename T>
    std::string do_conversion(const T& item) {
        // convert to string
    }

    template<>
    std::string do_conversion<std::string>(const std::string& item) {
        // frame in quotes
    }
}

struct convert_to_string {
    template<typename T>
    void operator()(const T& item) {
        // delegate to free function in namespace scope, because otherwise we 
        // cannot specialize
        std::string result = detail::do_conversion(item);
    }
};

// in calling code

std::for_each(std::begin(container), std::end(container), convert_to_string{});

Does this work for you?

Upvotes: 1

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275310

// implement the bodies as functors to allow for partial
// specialization for value_type:
template <class value_type>
struct DoContainerWorkImpl {
  template<typename Container>
  void operator()(const Container& c) const
  {
    for (auto&& x : c) {
      // code
    }
  }
};
template <>
struct DoContainerWorkImpl<std::string> {
  template<typename Container>
  void operator()(const Container& c) const
  {
    for (std::string const& x : c) {
      // code
    }
  }
};

template <class Container>
void DoContainerWork(const Container& c)
{
  using std::begin; // enable ADL of begin
  // extract the iterator for the container c:
  typedef typename std::decay<decltype( std::begin(c) )>::type iterator;
  // extract the value type of the container:
  typedef typename std::iterator_traits<iterator>::value_type value_type;
  DoContainerWorkImpl<value_type>()( c );
}

another approach would involve making the algorithm one thing, and determining if we want to call the conversion within the body of the algorithm using a traits class or other technique "locally".

Here is a stab at that approach:

 // convert non-strings:
 template<typename T>
 std::string as_string( T&& t )
 {
   // converting code goes here
 }
 // block dangling rvalues:
 std::string as_string( std::string&& s ) { return std::move(s); }
 // pass lvalues through:
 std::string const& as_string( std::string const& s ) { return s; }
 std::string const& as_string( std::string& s ) { return s; }

now, we do this in your loop body:

for(auto&& elem : c) {
  std::string const& s = as_string(std::forward<decltype(elem)>(elem));
  // code using s as a string
}

which compiles to a noop (or, at worst a few moves) if the container is a container of std::string, and to converting code if it is not.

Lifetime extension of temporaries assigned to references keeps any temporary generated std::string around, and lvalue lifetime of the iterator keeps the "string container whose iterators return references" lifetime long enough.

Upvotes: 2

That's why all of the standard library's algorithms work on iterators rather than containers.

You can change your core function to work on iterators rather than containers. This will require partial specialisation, which doesn't exist for function templates, so we'll use the delegate-to-class trick:

template <typename It>
void DoIteratorWork(It start, It end)
{
  DoIteratorWork_Impl<It, typename std::iterator_traits<It>::value_type>::call(start, end);
}

template <typename It, typename ValueType>
struct DoIteratorWork_Impl
{
  static void call(It start, It end)
  {
    for (; start != end; ++start) {
      // operate on *it
    }
  }
};

template <typename It>
struct DoIteratorWork_Impl<It, std::string>
{
  static void call(It start, It end)
  {
    for (; start != end; ++start) {
      // operate on *it
    }
  }
};

If you really want to, you can then create a wrapper around this:

template <class Container>
void DoContainerWork(const Container& c)
{
  using std::begin; using std::end; // enable ADL of begin and end
  return DoIteratorWork(begin(c), end(c));
}

Upvotes: 7

ForEveR
ForEveR

Reputation: 55887

You can use variadic-templates, something like

template<typename T, template<typename...> class Container, typename... Args>
void ContainerWork(Container<T, Args...> c)
{
}

template<template<typename...> class Container, typename... Args>
void ContainerWork(Container<std::string, Args...> c)
{
}

And probably you can of course use simple dispatch

template<typename Container>
void ContainerWork(Container c, 
typename std::enable_if<!std::is_same<typename Container::value_type,
std::string>::value>::type* = 0)
{
}

template<typename Container>
void ContainerWork(Container c,
typename std::enable_if<std::is_same<typename Container::value_type,
std::string>::value>::type* = 0)
{
}

But anyway, if difference is only in call to convert function - you can simply overload it for T and for string, version for string will simple return this string.

Also you can use SFINAE with C++11 decltype feature

template<typename Container>
auto ContainerWork(Container c) -> 
decltype(c.begin(),
typename std::enable_if<!std::is_same<typename Container::value_type,
std::string>::value, void>::type())
{
}

template<typename Container>
auto ContainerWork(Container c) ->
decltype(c.begin(),
typename std::enable_if<std::is_same<typename Container::value_type,
std::string>::value, void>::type())
{
}

Upvotes: 5

Related Questions