Klaim
Klaim

Reputation: 69752

How to overload a template function depending on argument's call operator args or existence?

Note: I am working with VS2013 so the C++11 features available are limited.

I am having trouble with overloading a template function depending on if the argument type is callable or not and, ideally, if the arguments match a specific pattern.

Here is a very simplified example of the code I have, my problem being how to implement update_callabe() overloads :

template< class T, class... Args >
void update_callable( const std::vector<T>& objects, Args&&... args ); // 1: How to implement this?

template< class T, class... UpdateArgs>
class Controller
{ //...
    virtual void update( T&, UpdateArgs... args ) = 0;
public:
    template< class IterBegin, class IterEnd, class... Args  >
    void update_batch( IterBegin first, IterEnd last, Args&&... args )
    {
        std::for_each( first, last, [&]( T& object ){ update(object, args...); }
    }
    //...
};

template< class T, class... UpdateArgs >
class Group
{
public:
    using ControllerType = Controller<T,UpdateArgs...>;

    void add( ControllerType& controler ) { /* ... */ m_controllers.emplace_back( &controller ); }

    template< class... Args >
    void update( Args&&... args )
    {
        update_callable(m_objects, std::forward<Args>(args)); // 2
        for( auto* controller : m_controllers )
        {
            controller->update_batch( begin(m_objects), end(m_objects), std::forward<Args>(args)); // 3
        }
    }

private:
    std::vector<T> m_objects;
    std::vector<ControllerType*> m_controllers;
    //...
};

A. What I want to achieve with update_callabe() overloads (in priority order):

  1. if T is callable with Args arguments, then call all T objects with the arguments.
  2. if T is not callable with Args arguments, then do exactly nothing.

B. That would be ok for me, but ideally I would like update_callabe() overloads that follow these rules (in priority order):

  1. if T is callable with Args arguments, then call all T objects with the arguments.
  2. if T is callable with NO arguments, then call all T objects with no arguments.
  3. if T is not callable with Args arguments nor NO arguments, then do exactly nothing.

I have tried with enable_if, conditional and several advanced techniques but I'm no expert (yet) so I'm failing to express this correctly.

Some notes about the example here:

Upvotes: 6

Views: 665

Answers (3)

aschepler
aschepler

Reputation: 72431

When I want if/else if/else-like behavior at compile time, I use a trick like this:

template <unsigned int N>
struct priority_helper
    : public priority_helper<N-1> {};

template <>
struct priority_helper<0U> {};

template <unsigned int N>
using priority = int priority_helper<N>::*;

constexpr priority<0> by_priority{};

template <typename Arg>
auto do_thing_detail(Arg&& arg, priority<1>)
    -> typename std::enable_if<cond1<Arg>::value>::type
{ /*...*/ }
template <typename Arg>
auto do_thing_detail(Arg&& arg, priority<2>)
    -> typename std::enable_if<cond2<Arg>::value>::type
{ /*...*/ }
template <typename Arg>
void do_thing_detail(Arg&& arg, priority<3>)
{ /*...*/ }

template <typename Arg>
void do_thing(Arg&& arg)
{ do_thing_detail(std::forward<Arg>(arg), by_priority); }

This would also work using the simpler types priority_helper<N>* instead of int priority_helper<N>::*, but then the larger values of N would be the preferred overloads, since pointer-to-derived is more specific than pointer-to-base. By using the pointer to member, the implicit conversions and therefore overload preferences go the other way around (pointer-to-member-of-base converts to pointer-to-member-of-derived).

So for your problem, after defining priority<N> as above...

template < class T, class... Args >
auto update_callable_detail(
    priority<1>,
    const std::vector<T>& objects,
    Args&& ... args )
    -> decltype(std::declval<const T&>()(std::forward<Args>(args)...), void())
{
    for ( const T& obj : objects )
        obj( std::forward<Args>(args)... );
}

template < class T, class... Args >
auto update_callable_detail(
    priority<2>,
    const std::vector<T>& objects,
    Args&& ... )
    -> decltype(std::declval<const T&>()(), void())
{
    for ( const T& obj : objects )
        obj();
}

template < class T, class... Args >
void update_callable_detail(
    priority<3>,
    const std::vector<T>&,
    Args&& ... )
{
}

template < class T, class... Args >
void update_callable( const std::vector<T>& objects, Args&& ... args )
{
    update_callable_detail( by_priority, objects, std::forward<Args>(args)... );
}

In this case it just seemed simpler to use SFINAE directly in the overload declarations, rather than do anything with std::result_of (especially since the C++11 requirements for result_of aren't as helpful as the C++14 version). Whenever the deduced arguments for T and Args result in an illegal expression in the decltype, that overload is thrown out during overload resolution.

Upvotes: 3

Casey
Casey

Reputation: 42574

Doable with a couple of traits and some tag dispatching (Demo at Coliru). First, define traits that determine if T is either callable with the specified argument types:

template <typename T, typename... Args>
struct callable_with_args_ {
    template <typename U=T>
    static auto test(int) ->
      decltype((void)std::declval<U>()(std::declval<Args>()...), std::true_type());
    static auto test(...) -> std::false_type;
    using type = decltype(test(0));
};

template <typename T, typename... Args>
using callable_with_args = typename callable_with_args_<T, Args...>::type;

or with no arguments:

template <typename T>
struct callable_without_args_ {
    template <typename U=T>
    static auto test(int) ->
      decltype((void)std::declval<U>()(), std::true_type());
    static auto test(...) -> std::false_type;
    using type = decltype(test(0));
};

template <typename T>
using callable_without_args = typename callable_without_args_<T>::type;

Then implement a two-level tag dispatch to get the precedence that you want:

template < class T >
void update_callable_no_args(std::false_type, const std::vector<T>&) {}

template < class T >
void update_callable_no_args(std::true_type, const std::vector<T>& objects) {
    for (auto&& i : objects) {
        i();
    }
}

template< class T, class... Args >
void update_callable_args(std::false_type,
                          const std::vector<T>& objects,
                          Args&&... ) {
    update_callable_no_args(callable_without_args<T const&>(), objects);
}

template< class T, class... Args >
void update_callable_args(std::true_type,
                          const std::vector<T>& objects,
                          Args&&... args ) {
    for (auto&& i : objects) {
        i(args...);
    }
}

template< class T, class... Args >
void update_callable( const std::vector<T>& objects, Args&&... args ) {
    using callable = callable_with_args<
      T const&, typename std::add_lvalue_reference<Args>::type...
    >;
    update_callable_args(callable(), objects, std::forward<Args>(args)...);
}

Note that I coerce the argument types to lvalue references, to avoid having any of the callables "eat" rvalue reference arguments causing later callables to see moved-from objects. If you want the rvalues to possibly be moved from, remove the coercion in update_callable:

using callable = callable_with_args<
  T const&, Args&&...
>;

and forward them to each callable in update_callable_args:

for (auto&& i : objects) {
  i(std::forward<Args>(args)...);
}

VS2013 at least seems to compile it properly.

Upvotes: 1

TemplateRex
TemplateRex

Reputation: 70546

Here is a partial solution using expression SFINAE (not sure if your VC version supports that)

template<class T>
auto update_callable(const std::vector<T>&, ...) 
    -> void
{ 
}

template< class T, class... Args>
auto update_callable( const std::vector<T>& objects, Args&&... args) 
    -> decltype(std::declval<T>()(std::forward<Args>(args)...))
{ 
    for (auto&& elem : objects)
        elem(std::forward<Args>(args)...);
}

template< class T, class... Args>
auto update_callable( const std::vector<T>& objects, Args&&... args) 
    -> decltype(std::declval<T>()())
{ 
    for (auto&& elem : objects)
        elem();
}

It's a partial solution because the varargs ... argument of the first overload only supports POD arguments (which is why I pasted in the std::vector<T> argument since a vector isn't POD).

You can test it like this:

struct N {};

struct Z 
{ 
    void operator()() const
    { std::cout << "Z(), "; }
};

struct T
{   
    void operator()(int x, int y) const
    { std::cout << "T(," << x << "," << y << "), "; }
};

int main()
{
    auto vN = std::vector<N>(1); 
    auto vZ = std::vector<Z>(2); 
    auto vT = std::vector<T>(3); 
    
    update_callable(vN, 1, 2);std::cout << "end of 1st\n";
    update_callable(vZ, 1, 2);std::cout << "end of 2nd\n";
    update_callable(vT, 1, 2);std::cout << "end of 3rd\n";
}

Live Example that outputs

end of 1st

Z(), Z(), end of 2nd

T(1,2), T(1,2), T(1,2), end of 3rd

Upvotes: 0

Related Questions