Readable function pointers

Filed under Programming
Tagged as , ,

Consider the following declaration

char*(*(*foo)(int(*)(char*,char*)))[]

It’s artificial (generated with the ever-helpful geordi bot), although I’m sure that if you looked hard enough, a similar one would appear somewhere in the wild. In the above case, foo is a pointer to function taking pointer to function taking char* and char* and returning int returning pointer to array of char*. I think that even seasoned C and C++ programmers will agree that this is quite confusing at first glance. Or second. And third. Especially for people less versed in “C gibberish”, as cdecl.org aptly calls it.

Even though C++ programmers, with the advent of functors and templates, don’t need to use raw function pointers often, they are still sometimes present – for example when paying for std::function‘s type erasure is unacceptable and there’s no state to be held in an object. Fortunately in C++11, instead of creating a C-style typedef for each such type, it’s possible to use a generalized solution that uses familiar syntax already used in the aforementioned std::function.

template <typename T>
using function_ptr = typename add_pointer<typename enable_if<is_function<T>::value,T>::type>::type;

The usage is identical to that of std::function, but it’s lightweight and doesn’t add any overhead over the function pointer – it is exactly the same type (int (*foo)(int) becomes function_ptr<int(int)> foo).

In the same vein, C-style arrays could be replaced with saner (the type being on the left hand side of the declaration, not all around it) aliases, with the syntax being close to that of the std::array class:

template <typename T, std::size_t S>
using c_array = T[S];

And the example usage would look as follows:

// int arr1[10]
c_array<int,10> arr2;

Although for writing new code, I’d stick with std::array. The above implementation doesn’t account for arrays of unknown bound, but it can be easily fixed by using a helper struct (as alias templates cannot be specialized). Zero-sized arrays are illegal in C++ so zero may be used to specify array of unknown bound:

namespace detail{
template<typename T, std::size_t S>
struct array_helper{ typedef T type[S]; };
 
template<typename T>
struct array_helper<T,0>{ typedef T type[]; };
}
 
template <typename T, std::size_t S = 0>
using c_array = typename detail::array_helper<T,S>::type;

With the above, main could be declared as int main(int argc, c_array<char*> argv), or even int main(int argc, c_array<c_string> argv), if one bothered to alias char* as well. I think it conveys the intent a lot more clearly than the standard char**.

With the above type aliases, the first example could redeclared as

function_ptr<c_array<char*>*(function_ptr<int(char*,char*)>)> foo

While still far from being clear, I do think that this is easier (and faster) to reason about than

char*(*(*foo)(int(*)(char*,char*)))[]

even though both foos have the exact same type.

Code should be optimized for reading (if possible) and in cases such as this, when there’s no performance overhead at all, I think it comes down to choosing one of the following:

int (*foo)(int) = &::isupper;
function_ptr<int(int)> bar = &::islower;
auto bax = &::isdigit; // only works if there are no overloads

I would choose auto when possible and function_ptr alias elsewhere.

Post a Comment

Your email is never published nor shared.