Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  • it's a simplified example, I didn't try to compile it but it is close to my code;
  • (2) basically we want to call the default update of the objects stored if the type of the objects provide one, "default update" meaning here the call operator either with arguments from the update context or with no arguments if the type don't need them;
  • (3) there is a second update loop for "controller" objects that can manipulate externally the stored objects;
like image 552
Klaim Avatar asked Jun 30 '14 17:06

Klaim


People also ask

How we can overload the function template?

You may overload a function template either by a non-template function or by another function template. The function call f(1, 2) could match the argument types of both the template function and the non-template function.

Is template function Cannot be overloaded?

A function template can overload non-template functions of the same name. In this scenario, the compiler first attempts to resolve a function call by using template argument deduction to instantiate the function template with a unique specialization.

How function overloading and function templates are related to each other?

Both function overloading and templates are examples of polymorphism features of OOP. Function overloading is used when multiple functions do quite similar (not identical) operations, templates are used when multiple functions do identical operations.

How do you overload a function in C++?

You overload a function name f by declaring more than one function with the name f in the same scope. The declarations of f must differ from each other by the types and/or the number of arguments in the argument list.


2 Answers

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.

like image 161
aschepler Avatar answered Oct 18 '22 14:10

aschepler


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.

like image 40
Casey Avatar answered Oct 18 '22 14:10

Casey