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.

Advertisements
IOD callable_traits

Using traits to determine whether a class has a specific member function named “X”

Let’s look at the example first. The code is slightly modified from has_parenthesis_operator in IOD library.

template <typename T>
struct HasClone
{
    template<typename C>
    static char test(int x, decltype(&C::Clone)* = nullptr);
    template<typename C>
    static int test(...);
    static const bool value = sizeof(test<T>(0)) == sizeof(char);
};

This is a small piece of code that defines traits struct which will have true when type T has function named Clone in its public scope. All the interesting things happen in two highlighted lines: 5 and 8. At line 5, I declare a static function takes two arguments: an int and a pointer to a pointer to member function of class C, named Clone. The function’s return type is char. The second function takes any inputs and returns int. Note that the return for two functions differs. The line 8 declares a static const bool variable named value which will be initialized to true if sizeof(test(0)) == sizeof(char).

This code makes use of SFINAE (Substitution Failure Is Not An Error) in a way that if type C has a function named Clone, decltype(&C::Clone) will refer to a pointer to the member function Clone, whereas if type C does not have a function named Clone, decltype(&C::Clone) will refer to unknown-type. When compiler reaches at line 8, it will try to resolve overloading of function test(0). If type T has member of Clone, the first test function – test(int, decltype<&C::Clone>*) matches the call test(0) perfectly, since the second parameter has default value of nullptr. The compiler will prefer this match to the match between test(…) and test(0) since the latter match is more general than the first perfect match, thus evaluating the size of return type of test(0) will result in sizeof(char).

When type T does not have a function named Clone, the compiler cannot generate code for char test(int, unknown-type*) since the second parameter’s type is not specified. Yet, this substitution failure will not result in a compile error, it will just make compiler refuse to create code for templatized function which incurred a substitution failure. Thus, HasClone for T without Clone() will only have int test(…), resulting sizeof(test(0)) to be sizeof(int). This will initialize value to be false.

And here is a macro to save you some key strokes.

#define HAS_MEMBER_FUNC_TRAIT(funcName) \
template <typename T> \
struct Has##funcName \
{ \
    template<typename C> \
    static char test(int x, decltype(&C::funcName)* = nullptr); \
    template<typename C> \
    static int test(...); \
 \
    static const bool value = sizeof(test<T>(0)) == sizeof(char);\
}

//! Declares exact same traits as above
HAS_MEMBER_FUNC_TRAIT(Clone);

EDIT:

Matthieu gave me few advice to enhance HasFunc trait. The first was to get rid of redundant * operator, and the second was that above example might not work for templatized member functions when compiled by compilers other than MSVC. When Clone() is templatized function like below:

class A
{
    template<typename T>
    void Clone();
}

MSVC matches this Clone to C::Clone in line 5, but GCC does not. Since we don’t want compiler dependent output from anything, I added some code that enables HasClone compiled by GCC to find class A’s Clone function.

#define HAS_MEMBER_FUNC_TRAIT(funcName) \
template <typename T> \
struct Has##funcName \
{ \
    template<typename C> \
    static char test(int x, decltype(&C::funcName) = nullptr); \
    template<typename C> \
    static char testTemplate(int x, decltype(&C::template funcName<>) = nullptr); \
    template<typename C> \
    static int test(...); \
    template<typename C> \
    static int testTemplate(...); \
 \
    static const bool value = (sizeof(test<T>(0)) && sizeof(char) && sizeof(testTemplate<T>(0)) != sizeof(char)); \
}

Here, I added a second test overload to test if there is a template member function named funcName. The template keyword is added to notice the compiler that name after this point is indeed with a template specifier, rather than two comparison operators . This implementation rejects a template member function named funcName in MSVC. The reason that I chose to separate template member function from HasFuncName is that as someone adds another template type parameter in a function funcName, you have to add matching test function inside HasFuncName struct for GCC, which is messy and something that I don’t want to do.

EDIT2:

Using explicit specialization of function can circumvent most of cases associated with template instantiation, except one situation where all instantiation is prohibited except specific types. For example, below code will not compile with HasFunc

class B
{
public:
    template<typename T>
    void Clone() { static_assert(false, "Clone cannot be instantiated!"); }
};
template <> void B::Clone<int>() {}

The static_assert will fire when you try to check if Clone is in it by HasClone::value. If you must check on template member function for presence, you’d have to use the version below and provide types that enable template instantiation as variadic arguments to the macro.

#define HAS_MEMBER_FUNC_TRAIT(funcName, ...) \
template <typename T> \
struct Has##funcName \
{ \
    template<typename C> \
    static char test(int x, decltype(&C::funcName) = nullptr); \
    template<typename C> \
    static char testTemplate(int x, decltype(&C::template funcName<__VAR_ARGS__>) = nullptr); \
    template<typename C> \
    static int test(...); \
    template<typename C> \
    static int testTemplate(...); \
 \
    static const bool value = (sizeof(test<T>(0)) && sizeof(char) && sizeof(testTemplate<T>(0)) != sizeof(char)); \
}

Nonetheless, this approach also has pitfalls. If different classes have different set of instantiable types for its template member functions, then you’d have to generate multiple versions with different names of HasFuncName struct! Ha, this is never going to end, and I am going to stop here and move on to other topics now. Thank you for reading!

Using traits to determine whether a class has a specific member function named “X”