Specializations for different types

161 views Asked by At

Can someone tell me how to remove the repeated specializations below?

#include <iostream>
#include <fstream>
#include <string>

struct Thing {
    int a, b;
    void load (std::istream& is) {is >> std::skipws >> a >> b;}
};

struct Object {
    int a, b, c;
    void load (std::istream& is) {is >> std::skipws >> a >> b >> c;}
};

template <typename...> struct PassArgs;

// General case.
template <typename First, typename... Rest>
struct PassArgs<First, Rest...> : PassArgs<Rest...> {
    void operator()(std::istream& is, First& first, Rest&... rest) const {
        is >> first;
        PassArgs<Rest...>::operator()(is, rest...);
    }
};

// Specialization for std::string needed.
template <typename... Rest>
struct PassArgs<std::string, Rest...> : PassArgs<Rest...> {
    void operator()(std::istream& is, std::string& first, Rest&... rest) const {
        while (std::getline (is, first) && first.empty());
        PassArgs<Rest...>::operator()(is, rest...);
    }
};

// Specialization for class Thing.
template <typename... Rest>
struct PassArgs<Thing, Rest...> : PassArgs<Rest...> {
    void operator()(std::istream& is, Thing& first, Rest&... rest) const {
        first.load(is);
        PassArgs<Rest...>::operator()(is, rest...);
    }
};

// Specialization for class Object, but is the exact same as that for Thing.
template <typename... Rest>
struct PassArgs<Object, Rest...> : PassArgs<Rest...> {
    void operator()(std::istream& is, Object& first, Rest&... rest) const {
        first.load(is);
        PassArgs<Rest...>::operator()(is, rest...);
    }
};


template <>
struct PassArgs<> {
    void operator()(std::istream&) const {}  // End of recursion.
};


int main() {}

Everything works correctly, but is there a way to avoid specializations for all classes that have the load(std::istream&) function (and there are many in my program). Currently, I have specializations for Thing, Object, and many other classes which all have the same lines in their specializations.

Incidentally, this is how the client uses PassArgs:

template <typename T, typename... Args>
T* create (std::istream& is, Args&... args) {
    PassArgs<Args...>()(is, args...);
    T* t = new T(args...);
    // Do whatever with t;
    return t;
}
3

There are 3 answers

6
Jonathan Wakely On BEST ANSWER

Define a trait to detect the load member:

template<typename T> using void_t = void;

template<typename T, typename = void_t<>>
  struct has_load
  : std::false_type { };

template<typename T>
  struct has_load<T, void_t<decltype(std::declval<T&>().load(std::declval<std::istream&>()))>>
  : std::true_type
  { };

Hoist the actual loading for a single type into a separate class template, specialized for types with a load member (using the trait):

template<typename T, bool use_load = has_load<T>::value>
  struct PassArg
  {
    static void pass(std::istream& is, T& t) { is >> t; }
  };

template<typename T>
  struct PassArg<T, true>
  {
    static void pass(std::istream& is, T& t) { t.load(is); }
  };

Then use that in your main template:

// General case.
template <typename First, typename... Rest>
  struct PassArgs : PassArgs<Rest...> {
    void operator()(std::istream& is, First& first, Rest&... rest) const
    {
        PassArg<First>::pass(is, first);
        PassArgs<Rest...>::operator()(is, rest...);
    }
};

The string case can be done by specializing PassArg too.

template<>
  struct PassArg<std::string, false>
  {
    static void pass(std::istream& is, std::string& s)
    { getline(is, s); }
  };

N.B. I made the new class have a static function called pass not operator(), because if you find yourself writing this:

PassArgs<Args...>()(is, args...);

or worse, calling operator() by name like this:

PassArgs<Rest...>::operator()(is, rest...);

Then you probably don't want a functor. PassArgs is stateless, so there's no point creating an instance of it, and if you have to name operator() explicitly then you're doing it wrong. Give the function a proper name and call that, and make it static:

PassArgs<Rest...>::sensible_name(is, rest...);
0
T.C. On

There are roughly one bazillion ways to do this. Here's one of them.

First, a trait to detect whether there is a member load() to be called. Here's one way to write it:

namespace details {    
    template<class T>
    auto has_load_impl(int) 
         -> decltype((void)std::declval<T&>().load(std::declval<std::istream&>()),
                     std::true_type());

    template<class T>
    std::false_type has_load_impl(...);

    template<class T>
    using has_load = decltype(has_load_impl<T>(0));
}

There are plenty of other ways to write this trait. Jonathan Wakely's answer uses Walter Brown's void_t, for instance. Or you may use std::experimental::is_detected.

Next, write a function that loads a single argument, dispatching based on the result of has_load. Here's one way to do it:

namespace details {
    template<class T>
    void do_load(std::istream& is, T& t, std::true_type /*has_load*/){
        t.load(is);
    }

    template<class T>
    void do_load(std::istream& is, T& t, std::false_type /*has_load*/){
        is >> t;
    }
}

template<class T>
void load(std::istream& is, T& t){
    details::do_load(is, t, details::has_load<T>());
}

It's also possible to dispense with the standalone trait, and SFINAE directly in the do_load functions, if you don't need the trait elsewhere:

namespace details {
    template<class T>
    auto do_load(std::istream& is, T& t, int) -> decltype((void)t.load(is)){
        t.load(is);
    }

    template<class T>
    void do_load(std::istream& is, T& t, ...){
        is >> t;
    }
}

template<class T>
void load(std::istream& is, T& t){
    details::do_load(is, t, 0);
}

Add overloads as needed for types requiring special treatment.

void load(std::istream& is, std::string& s){
    while (std::getline (is, s) && s.empty());
}

Finally, PassArgs itself can be simplified to a two-line function, using the familiar trick of pack expansion inside a braced-init-list:

template<class... Args>
void PassArgs(std::istream& is, Args&... args){
    using expander = int[];
    (void)expander{0, (load(is, args), void(), 0)... };
}

Demo.

In the above, the user may customize load using ADL. Alternatively, you might make load a member function of a class template loader<T> instead, and users can specialize loader as desired to customize the load:

template<class T>
struct loader {
    static void load(std::istream& is, T& t) {
        details::do_load(is, t, details::has_load<T>());
    }
};

template<> struct loader<std::string>{
    static void load(std::istream& is, std::string& s){
        while (std::getline (is, s) && s.empty());
    }
};

template<class... Args>
void PassArgs(std::istream& is, Args&... args){
    using expander = int[];
    (void)expander{0, (loader<T>::load(is, args), void(), 0)... };
}
0
Yakk - Adam Nevraumont On

I'm going to go all the way to an ADL solution.

First, here is friend based load support.

struct Thing {
  int a, b;
  friend void load (std::istream& is, Thing& t) {is >> std::skipws >> t.a >> t.b;}
};

member based load:

struct Object {
  int a, b, c;
  void load (std::istream& is) {is >> std::skipws >> a >> b >> c;}
};

First, some metaprogramming boilerplate. You can do this in fewer lines without the boilerplate, but it makes it cleaner:

namespace meta {
  namespace details {
    template<template<class...>class Z, class=void, class...Ts>
    struct can_apply : std::false_type {};
    template<template<class...>class Z, class...Ts>
    struct can_apply<Z, decltype((void)(std::declval<Z<Ts...>>())), Ts...>:
      std::true_type
    {};
  }
  template<template<class...>class Z, class...Ts>
  using can_apply = details::can_apply<Z,void,Ts...>;
}
template<class T>
using member_load = decltype( std::declval<T>().load(std::declval<std::istream&>()) );

template<class T>
using stream_load = decltype( std::declval<std::istream&>() >> std::declval<T>() );

which leads to this punchline:

template<class T>
using has_member_load = meta::can_apply< member_load, T >;
template<class T>
using has_stream_load = meta::can_apply< stream_load, T >;

Now we create a loading namespace:

namespace loading {
  void load(std::istream&is, std::string& s) {
    while (std::getline (is, s) && s.empty());
  }
  template<class T>
  std::enable_if_t<has_member_load<T&>::value>
  load(std::istream&is, T& t) {
    t.load(is);
  }
  // uses ... to keep lowest priority:
  template<class T>
  std::enable_if_t<has_stream_load<T&>::value>
  load(std::istream& is, T& t, ...) {
    is >> t;
  }

  template<class...Ts>
  void load_many(std::istream&is, Ts&...ts) {
    using discard=int[];
    (void)discard{0,((
      load(is, ts)
    ),void(),0)...};
  }
}

we can now call loading::load_many(is, a, b, c, d).

std::string has a custom loading::load function, as can any other specific types in std you want to support. For example, you can write template<class T, class A> void load(std::istream& is, std::vector<T,A>&) in namespace loading, and it will just work.

Any class X with a free function load(istream&, X&) defined in its namespace will have that function called.

Failing that, any class with a .load(istream&) method will have that method called.

Any class X with a istream& >> X& overload will get that called if everything above fails.

live example.