IOD callable_traits

The next topic I would like to write about in IOD library is callable_traits. callable_traits does not have much role in IOD library, however, it is mostly being used by dependency injection module implemented with IOD. Although this callable_traits has little share inside the library, I am going to cover this because: 1) it can still be used in many other places, and 2) to keep the topic move smoothly from my last post.

Now, let’s look at the code. The below code snippet is the declaration of default callable_traits from callable_traits.hh.

// Traits on callable (function, functors and lambda functions).

// callable_traits<F>::is_callable = true_type if F is callable.
// callable_traits<F>::arity = N if F takes N arguments.
// callable_traits<F>::arguments_tuple = tuple<Arg1, ..., ArgN>
// callable_traits<F>::return_type = return type of F if F is callable, void otherwise.

The callable_traits struct consists of 4 member variables: is_callable, arity, arguments, and return_type. In brief, is_callable is boolean constant being true if given type F is callable object. arity is the number of parameters taken for calling F as a function. arguments_tuple is a tuple made up of types of arguments to function F. return_type is simply the type which callable type F returns. For example, if you have a function like this,

int foo(double arg1, float arg2);

What values members of callable_traits will have is_callable set to true, arity set to 2, arguments_tuple set to tuple, and return_type set to int. Now, let’s take a look at more detailed implementation.

What we’ll do first is to see what happens in the default case, when type F is not a callable type (e.g. class without parenthesis operator, POD type).

template <typename Function, typename X = void>
struct callable_traits
{
  typedef std::false_type is_callable;
  static const int arity = 0;
  typedef std::tuple<> arguments_tuple;
  typedef void return_type;
};

It is pretty straightforward, isn’t it? Putting aside the second template parameter for a while, the default version of callable_traits sets member variables to default values. is_callable is false, arity is 0, arguments_tuple to empty tuple, and return_type to void. All the other magic is done in partial specialization side. Let’s continue.

template <typename Function, typename X>
struct callable_traits<Function&, X> : public callable_traits<Function, X> {};
template <typename Function, typename X>
struct callable_traits<Function&&, X> : public callable_traits<Function, X> {};
template <typename Function, typename X>
struct callable_traits<const Function&, X> : public callable_traits<Function, X> {};

These 6 lines of partial specialization cover cases where type Function is actually a reference, r-value reference, or a const-reference of other type. It simply adopts result of callable_traits for types without references from given type Function. There are 5 more specializations to left.

template <typename R, typename... ARGS>
struct callable_traits<R(ARGS...)>
{
  typedef std::true_type is_callable;
  static const int arity = sizeof...(ARGS);
  typedef std::tuple<ARGS...> arguments_tuple;
  typedef R return_type;
};

template <typename C, typename R, typename... ARGS>
struct callable_traits<R (C::*)(ARGS...) const>
{
/// ... same as the top one
};

template <typename C, typename R, typename... ARGS>
struct callable_traits<R (C::*)(ARGS...)>
{
/// ... same as the top one
};
  
template <typename R, typename... ARGS>
struct callable_traits<R(*)(ARGS...)>
{
/// ... same as the top one
};

These partial specialization covers non-member function, pointer to const member function, pointer to member function, pointer to non-member function, from top to bottom. In more detail, all functions are made of 3 distinct parts: return value, function name, and list of arguments. What the declaration above is actually saying by writing callable_traits is that for any type Function that is given to callable_traits struct, if the type can be represented in a form of function type taking types ARGS… and returning type R, apply this template specialization. The same for other three specializations: if Function is of type pointer to non-member function, if Function is of type pointer to member function, and if Function is of type pointer to const member function.

Going back to the foo example above, callable_traits will be callable_traits. This gives above specialization more information about R and ARGS; R is int and ARGS is double,float. Having gotten to this point, understanding how these 4 member variables are pretty straightforward. One could add more information such as if a callable object is whether pointer to member function or not, if so, what is the type of class that the function is member of, and so on. It seems like we are done here, but we have one more specialization to cover, which is the case of functor types.

template <typename Functor>
struct callable_traits<Functor, std::enable_if_t<internal::has_parenthesis_operator<Functor>::value>>
{
  typedef callable_traits<decltype(&Functor::operator())> super;
  typedef std::true_type is_callable;
  static const int arity = super::arity;
  typedef typename super::arguments_tuple arguments_tuple;
  typedef typename super::return_type return_type;
};

A functor class not a function, yet it is callable by overloading operator(). The implementation above detects functor object by using has_parenthesis_operator traits, which we covered on the last post. All it does is checking if a Functor has operator() defined, and only if when operator() is defined, the specialization will take place. The traits struct will explicitly typedef callable_traits for it’s member operator() as super, and just forward the members of super to the struct.

Now, there is one other traits struct left in this header file, we will get back to this traits class when we cover grammar.hh header file. The next topic I am would like to cover is foreach and apply.

IOD callable_traits