This will seem like one hell of a rambling, I’m still not used to writing things in this format…

Today, I randomly remembered the std::execution policies and how they are used as “tags” to select between different overloads of functions such as std::copy. I wondered to myself; “Hey, can’t I just switch between all functions with this method?”, the answer to which is a definite yes.

Before anything, let’s define our tags. For this, the approach is pretty straightforward, we first need a container to hold our strings:

template<size_t N>
struct Tag {
    char name[N];
};

Simple enough, now we need to be able to use the tags to specialise.. things. Let’s go with fully specialising a simple function first:

template<Tag>
void foo();

template<>
void foo<"bar">() {}

But what’s that? we get a compiler error. Why of course we do, how would the compiler know what N to use? One way to tell the compiler how to deduce the N parameter is by deduction guides:

template<size_t N>
Tag(char const(&)[N]) -> Tag<N>; 

But this brings about a mild annoyance, we can’t implicitly construct the Tag objects in template template specialisations anymore, so we would have to write the foo specialization of "bar" as such:

template<>
void foo<Tag{"bar"}>() {}

We can do better than this with a constructor while losing the aggregate-ness of the Tag type, which is not an issue as this type is only intended to be used during compile time:

template<size_t N>
struct Tag {
    char name[N];
    constexpr Tag(char const(&str)[N]) { std::copy(str, str + N, this->str); } 
};

Now we can make use of the implicit conversion rules and use string literals directly in our full specialisation:

template<>
void foo<"bar">() {}

And we can call the function like so:

foo<"bar">();

Well ain’t that sweet?
Actually, no, it is not very sweet. Why? Because we are still making use of function specialisations which cannot have their return types and arguments differ from the original function. Both of those issues can be solved via small hacks but there’s a far easier approach, function objects:

template<Tag>
struct Foo;

template<>
struct Foo<"bar"> {
    void operator()() {};
};

And we can then call the function like so:

Foo<"bar">{}();

Only 2 extra characters! (Instead of explicitly specifying the return type ._.)
But with this we run into a small and easy to fix issue: Recursing looks far uglier with this new method of using function objects…

template<>
struct Foo<"fibonacci"> {
    int operator()(int n) {
        if (n <= 2)
            return 1;
        return Foo<"fibonacci">{}(n-1) + Foo<"fibonacci">{}(n-2);
    }
}

But actually, I have lied to you, well, not entirely. It can look a little bit better:

template<>
struct Foo<"fibonacci"> {
    int operator()(int n) {
        if (n <= 2)
            return 1;
        return (*this)(n-1) + (*this)(n-2);
    }
}

And now let’s see all that was written here in action:

#include <algorithm>
#include <cstdio>

template<size_t N> struct Tag {
    char str[N];
    constexpr Tag(char const(&str)[N]) { std::copy(str, str + N, this->str); }
};

// bonus! not really needed...
template<size_t N>
Tag(char const(&)[N]) -> Tag<N>;

template<Tag G>
constexpr auto operator""_tag() {
    return G;
}

template<Tag T> struct Thing;

template<> struct Thing<"fib1"> {
    constexpr int operator()(int n) {
        if (n <= 2)
            return 1;
        return Thing<"fib1">{}(n - 1) + Thing<"fib1">{}(n-2);
    }
};

template<> struct Thing<"fib2"> {
    constexpr int operator()(int n) {
        if (n <= 2)
            return 1;
        return (*this)(n - 1) + (*this)(n-2);
    }
};

template<> struct Thing<"printf"> {
    template<typename... Ts>
    int operator()(const char* fmt, Ts... args) {
        return printf(fmt, args...);
    }
};

int main() {
    static constexpr auto demo = "printf"_tag;
    Thing<demo>{}("%d\n", Thing<"fib1">{}(10));
    Thing<demo>{}("%d\n", Thing<"fib2">{}(10));
}

One would expect the compiled output for this program to be horrendous (at least I did…) but with -O3, gcc seems to be able to do pretty much everything in compile time and emit out only a handful of AMD64 assembly. The same cannot be said for Clang, disappointingly. You can see the results for yourself here