Rinat Veliakhmedov
Rinat Veliakhmedov

Reputation: 1039

How to call a specific method only if class has one?

I have a tuple of objects of different classes. I want to iterate over the tuple and call a certain method only if those class has one.

For example (pseudo-code):

struct A {  int get( ) { return 5; }; };
struct B { }; 
struct C {  int get( ) { return 10; }; };
int i = 0;
tuple<A, B, C> t;
for ( auto t_element : t ) 
{
    if constexpr ( has_get_method( decltype(t_element) ) )
    {
        i += t_element.get( );
    }
}

I already know how to iterate over the tuple and check if a class has some method using sfinae but how do I skip object that do not have the required method?

EDIT: If you found this old question in the future, please know that this can be done much easier now using concepts from C++20 standard.

Upvotes: 2

Views: 1760

Answers (2)

max66
max66

Reputation: 66200

You can create a type-traits, to check if a class has a get() method, by declaring a couple of functions (no need to define them)

template <typename>
constexpr std::false_type withGetH (long);

template <typename T>
constexpr auto withGetH (int)
   -> decltype( std::declval<T>().get(), std::true_type{} );

template <typename T>
using withGet = decltype( withGetH<T>(0) );

The following is a fully working c++17 example

#include <tuple>
#include <iostream>
#include <type_traits>

template <typename>
constexpr std::false_type withGetH (long);

template <typename T>
constexpr auto withGetH (int)
   -> decltype( std::declval<T>().get(), std::true_type{} );

template <typename T>
using withGet = decltype( withGetH<T>(0) );

struct A {  int get( ) { return 5; }; };
struct B { }; 
struct C {  int get( ) { return 10; }; };

template <typename T>
int addGet (T & t)
 {
   int ret { 0 };

   if constexpr ( withGet<T>{} )
      ret += t.get();

   return ret;
 }

int main ()
 {
   int i = 0;

   std::tuple<A, B, C> t;

   i += addGet(std::get<0>(t));
   i += addGet(std::get<1>(t));
   i += addGet(std::get<2>(t));

   std::cout << i << std::endl;
 }

If you can't use if constexpr, you can write (in c++11/14) getAdd() as follows, using tag dispatching

template <typename T>
int addGet (T & t, std::true_type const &)
 { return t.get(); }

template <typename T>
int addGet (T & t, std::false_type const &)
 { return 0; }

template <typename T>
int addGet (T & t)
 { return addGet(t, withGet<T>{}); }

--EDIT--

The OP ask

my code needs to check for a templated method with parameters. Is it possible to modify your solution so that instead of int get() it could check for something like template<class T, class U> void process(T& t, U& u)?

I suppose it's possible.

You can create a type-traits withProcess2 (where 2 is the number of arguments) that receive three template type parameters: the class and the two template types

template <typename, typename, typename>
constexpr std::false_type withProcess2H (long);


template <typename T, typename U, typename V>
constexpr auto withProcess2H (int)
   -> decltype( std::declval<T>().process(std::declval<U>(),
                                          std::declval<V>()),
                std::true_type{} );

template <typename T, typename U, typename V>
using withProcess2 = decltype( withProcess2H<T, U, V>(0) );

The following is a fully working example (c++17, but now you know how to make it c++14) with A and C with a process() with 2 template parameters and B with a process() with only 1 template parameter.

#include <iostream>
#include <type_traits>

template <typename, typename, typename>
constexpr std::false_type withProcess2H (long);


template <typename T, typename U, typename V>
constexpr auto withProcess2H (int)
   -> decltype( std::declval<T>().process(std::declval<U>(),
                                          std::declval<V>()),
                std::true_type{} );

template <typename T, typename U, typename V>
using withProcess2 = decltype( withProcess2H<T, U, V>(0) );

struct A
 {
   template <typename T, typename U>
   void process(T const &, U const &)
    { std::cout << "A::process(T, U)" << std::endl; }
 };

struct B
 {
   template <typename T>
   void process(T const &)
    { std::cout << "B::process(T)" << std::endl; }
 };

struct C
 {
   template <typename T, typename U>
   void process(T &, U &)
    { std::cout << "C::process(T, U)" << std::endl; }
 };

template <typename T>
void callProcess (T & t)
 {
   static int  i0 { 0 };
   static long l0 { 0L };

   if constexpr ( withProcess2<T, int &, long &>{} )
      t.process(i0, l0);
 }

int main ()
 {
   std::tuple<A, B, C> t;

   callProcess(std::get<0>(t)); // print A::process(T, U)
   callProcess(std::get<1>(t)); // no print at all
   callProcess(std::get<2>(t)); // print C::process(T, U)
 }

Upvotes: 2

skypjack
skypjack

Reputation: 50540

Just write a sfinae'd function and a catchall in case the previous one fails. You don't have to use if constexpr for that and you can't use it actually in C++14 (that is how you tagged the question).
Here is a minimal, working example:

#include <tuple>
#include <iostream>

auto value(...) { return 0; }

template <typename T>
auto value(T &t) -> decltype(t.get()) {
    return t.get();
}

struct A { int get() { return 5; }; };
struct B {}; 
struct C { int get() { return 10; }; };

int main() {
   int i = 0;

   std::tuple<A, B, C> t;

   i += value(std::get<0>(t));
   i += value(std::get<1>(t));
   i += value(std::get<2>(t));

   std::cout << i << std::endl;
}

See it up and running on wandbox.

If you have any argument you want to use to test it, you can use std::forward as:

template <typename T, typename... Args>
auto value(T &t, Args&&... args)
-> decltype(t.get(std::forward<Args>(args)...))
{ return t.get(std::forward<Args>(args)...); }

Then invoke it as:

i += value(std::get<0>(t), params);

Upvotes: 6

Related Questions