Conversion boilerplate

From time to time programmers need to deal with conversions. If written by hand they can get annoying. Consider this function:

void doStuff(const std::string& str) {
    double parsedNumber = std::atof(str.c_str());
    // do stuff with number
}

Now, someone might say we could make this function more generic by taking in C string.

There are a few solutions:

  • Refactor function to take in C string and refactor calls to this function.
  • Make another overload of this function which takes C string.

First one impacts a lot of stuff. Second makes us repeat the stuff we did with converted number. Of course, we could factor that out into another function and eliminate repeating but just for the sake of the function accepting two different, yet, semantically similar types we have to make two functions.

The only thing that varies is that we want to get double from some value in the end of the day. Can't we factor out the concept that varies?

Templatious library has a concept called a Match Functor. Let's create one and see what it does.

static auto toDoubleStr =
    SF::matchFunctor(
        SF::matchLoose<std::string>(
            [](const std::string& str) {
                return std::atof(str.c_str());
            }
        ),
        SF::matchLoose<const char*>(
            [](const char* str) {
                return std::atof(str);
            }
        )
    );

Might look quite complex at first glance. Basically, we're creating a functor that matches our functors we supplied according to the arguments. First match takes in std::string and returns a double. Second match takes in C string and returns a double. The whole point of creating a functor called toDouble is to pass in anything and get double. Let's see how it's used:

double d1 = toDoubleStr("7.7"); // d1 <- 7.7
double d2 = toDoubleStr(std::string("3.14")); // d2 <- 3.14

// nonsense - we didn't intend to match file handles,
// therefore, code doesn't compile.
double d3 = toDoubleStr(std::ofstream("out"));

So, basically, this functor works only with matches we have specified. The functions to call have to be resolved at compile time, not at runtime, so, essentially, there's no difference in performance, no matter if you write boilerplate to convert from double or you call this functor.

Now, you might say “wait a second, couldn't I achieve the same effect just by overloading () operator of some object to take in std::string and const char*“? You certainly could at this specific case. However, match functor is more flexible.

While the overloaded object has to rely on C++ overload resolution rules (about 30 pages in C++ standard - I'm too lazy to know all that stuff) match functor is completely linear. Match functor chooses from top to down until it finds a match, therefore, ambiguities of which function to call in match functor are impossible.

Match functor below is perfectly legal:

static auto toDoubleStr =
    SF::matchFunctor(
        SF::matchLoose<std::string>(
            [](const std::string& str) {
                return std::atof(str.c_str());
            }
        ),
        SF::matchLoose<const char*>(
            [](const char* str) {
                return std::atof(str);
            }
        ),
        SF::matchLoose<const char*>(
            // although legal, this function will never
            // be called, because match above comes
            // earlier and is identical to this match
            [](const char* str) {
                throw 7;
            }
        )
    );

So, we can simply solve the problem that we had earlier with our newly created match functor.

template <class T>
void doStuff(const T& str) {
    double parsedNumber = toDoubleStr(str);
    // do stuff with number
}

Although, we lose benefit of having this function precompiled as now it has to be in header to generate code, it does look more concise, is more generic, is less code and allows us to reuse our created match functor in any other context.

Now, the sweet part of match functor…

… is composition. Our functor is quite useful. For strings… And strings only… Maybe not that generic after all. What if we want to pass a numeric type? You mean to say that we can convert string to double but if we get an integer which is already numeric our hands are tied? How about having one functor to simply try to convert anything it gets to a double?

We could add to the existing match functor more overloads… But of course, this is not elegant and things can turn clunky quite quickly.

Let's create another match functor for converting from numerical types.

static auto toDoubleNum =
    SF::matchFunctor(
        SF::matchLoose<int>(
            [](int i) {
                return static_cast<double>(i);
            }
        ),
        SF::matchLoose<short>(
            [](short i) {
                return static_cast<double>(i);
            }
        )
    );

We made another functor that processes ints and shorts in the same manner to get double.

Now, we can combine the previously created functors into one generic toDouble functor:

static auto toDouble =
    SF::matchFunctor(
        toDoubleNum,toDoubleStr
    );

We just reused previously created match functors and combined them into one, even richer match functor. Now, this can deal with everything that toDoubleNum and toDoubleStr had to deal with separately:

double d1 = toDouble("3.14");
double d2 = toDouble(std::string("7"));
double d3 = toDouble(7);

// still nonsense, still illegal,
// still compile time error
double d4 = toDouble(std::ofstream("out"));

We can go back and tweak our original doStuff function to get the piece of the pie.

template <class T>
void doStuff(const T& anything) {
    double parsedNumber = toDouble(anything);
    // do stuff with number
}

Now our original function is even more generic. And it will become even MORE generic as we extend our match functor - without even modifying it. The worst case scenario is someone will try to use doStuff function and it won't compile, because it doesn't provide conversion to double (yet). And compile time errors are always better than hunting bugs at runtime.

But lets look at our toDoubleNum match functor. We have provided several matches for matching int and short. I mean, these types convert to double implicitly anyway, can't we generalize it a little bit?

Let's look at possible solutions in templatious library.
We can use typelist in loose match to use function on multiple matches.

static auto toDoubleNum =
    SF::matchFunctor(
        SF::matchLoose<
            templatious::TypeList< int, short >
        >(
            [](double i) {
                return i;
            }
        )
    );

Instead of a single type slot in matchLoose we passed a typelist - we are saying any of these types will do. There's an implicit conversion to double when we call lambda with an argument but that seems okay.

Of course, here it is obvious there's an implicit conversion. What if we're not dealing with primitive numericals but with some arbitrary types? Silent implicit conversions can be your best friend or the worst enemy. I'd rather be sure that code does what I expect it does!

So, our hands are tied? Oh ye of little faith… Templatious library has an extended match where we can supply our own function for testing arguments. First, let's write our argument matching metafunction.

We want to be 100% sure if something has an implicit conversion or not. We will need to provide two values in our function, whether
it matches the argument at the current point (there can be more than one argument) and number of arguments we expect for a function.

template <class Arg,int pos>
struct OurMatcher {
    static const bool isConvertible =
        std::is_convertible<Arg,double>::value;

    // does this argument match our conditions.
    // has to be true for all calls of this match
    // for every argument to use this match.
    static const bool does_match = isConvertible;
    // number of arguments expected.
    // pass 0 for any amount of arguments.
    static const int num_args = 1;
};

So, this little template structure has ability to tell whether value is convertible to double with a surgical precision. Here's our new match functor:

static auto toDoubleNum =
    SF::matchFunctor(
        SF::matchSpecial<OurMatcher>(
            [](double i) {
                return i;
            }
        )
    );

Looking sharp! This little code chunk just allowed us to implicitly convert from numerous primitives like short, char, unsigned char, float, long etc. And we know that only and ONLY values that can be implicitly converted to double will be converted.

There's one tiny downside to this approach… Our matcher function has no idea about the types in other arguments. What if we had to match three arguments and our match succeeds for first two, but on the third match we'd need to know the exact type of the first match to make an accurate decision?

Here it is, the last and most generic (although, less user friendly) match of match functor: matchSpecialExt. It takes one template parameter, which takes in templatious::TypeList which contains all the types at once and user is allowed to make razor sharp decision on whether to use this match with the given arguments. Here's the template function:

template <class TypeList>
struct OurExtendedMatcher {
    static const bool right_num_of_args =
        TypeList::size == 3;

    static const bool a_convertible =
        std::is_convertible<
            TypeList::template ByIndex<0>::type, double
        >::value;

    static const bool b_convertible =
        std::is_convertible<
            TypeList::template ByIndex<1>::type, float
        >::value;

    static const bool c_convertible =
        std::is_convertible<
            TypeList::template ByIndex<2>::type, char
        >::value;

    // the decision value
    static const bool does_match = right_num_of_args
        && a_convertible
        && b_convertible
        && c_convertible;
};

And the new match functor created:

static auto someAwesomeFunctor =
    SF::matchFunctor(
        SF::matchSpecialExt<OurExtendedMatcher>(
            [](double i,float f,char c) {
                // do awesome stuff with
                // these vars
            }
        )
    );

All for now folks!