How to overload a template function depending on an argument or operator?

Note. I work with VS2013, so the available features of C ++ 11 are limited.

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

Here is a very simplified code example that I have, my problem is 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 when overloading update_callabe() (in order of priority) :

  • if T is called with Args arguments, then call all T objects with arguments.
  • if T cannot be called with Args arguments, then nothing is done.

B. That would be good for me, but ideally I would like to overload update_callabe() that follow these rules (in order of priority) :

  • if T is called with Args arguments, then call all T objects with arguments.
  • if T is called with NO arguments, then calls all T objects with no arguments.
  • if T cannot be called by Args or NO arguments, then nothing is done.

I tried with enable_if, conditional and a few best practices, but I am not an expert (yet), so I can not express it correctly.

Some notes about the example here:

  • This is a simplified example; I did not try to compile it, but it is close to my code;
  • (2) basically we want to call the default update for objects that are stored if the type of objects is one, “default update” here means a call operator with either arguments from the update context or without arguments if the type does not need them;
  • (3) there is a second update cycle for “controller” objects that can manage external stored objects;
+6
source share
3 answers

When I want an 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 will also work with simpler types of priority_helper<N>* instead of int priority_helper<N>::* , but then higher N values ​​will become more preferable, since the pointer-derived is more specific than the pointer-to-base. Using a pointer to a member, implicit conversions and, therefore, overload preferences go in the opposite direction (converter-to-member-base is converted to a pointer-to-member-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 is simply easier to use SFINAE directly in overload declarations, rather than doing something with std::result_of (especially since the C ++ 11 requirements for result_of not as useful as C ++ 14). Whenever the output arguments for T and Args lead to an illegal expression in decltype , this overload is thrown while resolving the overload.

+3
source

Let's say with two attributes and some dispatching tags ( Demo at Coliru ). First, define the attributes that determine if T be called 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 without 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 do a two-level send tag to get the priority 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 force argument types to lvalue reference values ​​to avoid any of the arguments to the "eat" rvalue link to make subsequent calls to view the moved objects. If you want r-values ​​to be moved, remove the coercion in update_callable :

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

and forward them to each callee in update_callable_args :

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

VS2013, at least, seems to be compiling it correctly .

+1
source

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

 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(); } 

This is a partial solution because the varargs ... argument of the first overload only supports POD arguments (so I inserted std::vector<T> in the argument since the vector is not POD).

You can check it as follows:

 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 the third

0
source

Source: https://habr.com/ru/post/971548/


All Articles