June 16th, 2018 (back)

C++ Named Parameters using Operator Suffixes and Variadic Templates

UPDATE

This is a repost from my previous blog.

Redditor inter1965 managed to modify this code to use constexpr hashing for compile-time keys: This guarantees you compile-time errors and so on: check it out here. The additional template expressions that you need do make it a bit uglier, but once -fstring-template-parameters comes around then this would be much better.

Introduction

If you didn't know: C++ does not have any language constructs to define named or keyword parameter solutions. There are idioms to support alternatives such as the named parameter idiom, or libraries that which utilize macros such as the Boost parameter library. This post aims to provide an alternative solution using modern C++ without the immediate need of macros or external libraries.

The Idea

We will use variadic templates, operator suffixes, proxy types, template casting, and dynamic_cast to provide a succinct solution. If you're purely interested in the code, then feel free to jump to the bottom of the post. The rest of the post describes the idea behind the code and the implementation details.

What we want

Let's start off with what the ideal case would look like:

greet(user="Justin") // -> "Hello, Justin!"

Now, we know with our basic language constructs this is not simply possible. However there is an alternative.  In the end we will have something that looks like:

greet("user"kw="Justin") // -> "Hello, Justin!"

Operator Suffixes

In C++ we can define new literals through the suffix notation. This allows us to define literals such as:

struct KG { long double n; }
KG operator"" kg(long double x)  { return KG{x}; }
KG mass = 10.0kg;

(technically all non _ prefixed suffixes are reserved for future use, but let's ignore that for the time being)

Operator suffixes are also available to character literals. So, we can also define

struct Male { std::string name; }
Male operator"" m(const char* s, size_t n)  { return Male{s}; }
Male justin = "justin"m;

So, we can define an alternative solution to our keyword argument problem now.

Now what do we have?

greet("user"kw="Justin") // -> "Hello, Justin!"
struct kwarg {std::string name;}
kwarg operator"" kw(const char* name, size_t x)  { return kwarg{name}; }

We now have this concept of a keyword argument literal expression. However, we have no way to use it or assign to it. That's a problem! We need a way to arbitrarily define the right-hand-side of the expression. It should be fairly obvious at this point that we will need to use templates. We will want to have a way to contain the type internally, so we can provide a mapping of keys to values without needing to be explicit about the type.

Defining our kwarg type

class kwarg {
public:
    kwarg() = default
    kwarg(const char* name)
        : name(name) { }

    struct kwarg_proxy_base { virtual ~kwarg_proxy_base() {}; };

    template <typename T>
    struct kwarg_proxy : kwarg_proxy_base {
        kwarg_proxy() = default;
        kwarg_proxy(const T& t)
            : t(t) { }
        typedef T type;
        const T& t;
    };

We first define our constructor to take a name of the keyword argument. This is the left-hand-side and defines how we will do our lookup. Now, we said that we don't want to expose the type directly. We are effectively defining an any type. That is where the kwarg_proxy_base and kwarg_proxy objects come in. We will store our default type as a kwarg_proxy_base and dynamic_cast to the templated kwarg_proxy on request. Next part!

    const std::string name;
    std::string char_value;
    std::shared_ptr<kwarg_proxy_base> value;

We will access our proxy'd object through a shared_ptr. We do store a char_value, but this is for other reasons later explained. Now for the brunt of it.

    kwarg& operator=(kwarg& rhs)
    {
        value = rhs.value;
        return *this;
    }

A typical operator for copying values from the rhs over. Remember, we are using a shared_ptr. Now, for the real fun part. We need to take any value on the right hand side and store it in our proxy container.

    template <class T>
    kwarg& operator=(const T& rhs)
    {
        value = std::shared_ptr<kwarg_proxy_base>(new kwarg_proxy<T>(rhs));
        return *this;
    }

    kwarg& operator=(const char* rhs)
    {
        char_value = std::string(rhs);
        value = std::shared_ptr<kwarg_proxy_base>(
                new kwarg_proxy<std::string>(char_value));
        return *this;
    }

We do this by taking a const reference (this intentionally has implications on the lifespan of our objects) and casting it to the templated type as our internal kwarg_proxy object. Now, how do we access it?

  template<typename T>
    operator const T&() const {
        if (!value) return T();
        return dynamic_cast<const kwarg_proxy<T> &>(*value).t;
    }

    template <typename T>
    operator T*() const {
        if (!value) return nullptr;
        return &dynamic_cast<const kwarg_proxy<T> &>(*value).t;
    }

    template <typename T>
    operator T() const {
        if (!value) return T();
        return dynamic_cast<const kwarg_proxy<T> &>(*value).t;
    }

We provide const& T, T*, and T access to the kwarg. We can not make any guarantees about the lifetime of the parameters, so these are our best bets. You either get a copy, a const-reference, or a pointer to the object. As mentioned before, we will need to dynamic_cast it. If it fails then an exception will be thrown, otherwise we get a default initialized value.  Finally, we have the actual literal:

kwarg operator "" kw (const char* literal, size_t n)
{
    return kwarg(literal);
}

We subclass std::map and add get() semantics to be a bit more Pythonic. You could use only the map that we subclass to reference the kwargs as well.

class kwargs : public std::map<std::string, kwarg> {
public:
    template <typename T>
    const T& get(const std::string& key, const T& default_parameter = T()) const
    {
        auto it = this->find(key);
        if (it == this->end()) 
            return default_parameter;
        if (it->second.value) 
            return it->second;
        return default_parameter;
    }

    template <typename T>
    T get(const std::string& key, T default_parameter = T())
    {
        auto it = this->find(key);
        if (it == this->end()) 
            return default_parameter;
        return it->second;
    }
};

Variadic Templates

Our issue now is how do we arbitrarily add kwargs to a function. The C standard already defined variadic functions with our favorite va_start, va_end, and so on. However, it is a much more limiting implementation. There are restrictions on any type that does not provide a means of trivial copy construction. As apparent in our code above, we do not have a trivial copy constructor. The lack of type promotion and the type safety guarantee are additional reasons.

The basic structure

Let's briefly look at the structure of a variadic template. We will use the classic printf as an example:

void printf(const char *s)
{
    while (*s) {
        if (*s == '%') {
            if (*(s + 1) == '%') {
                ++s;
            }
            else {
                throw std::runtime_error("invalid format string: missing arguments");
            }
        }
        std::cout << *s++;
    }
}
 
template<typename T, typename... Args>
void printf(const char *s, T value, Args... args)
{
    while (*s) {
        if (*s == '%') {
            if (*(s + 1) == '%') {
                ++s;
            }
            else {
                std::cout << value;
                printf(s + 1, args...); // call even when *s == 0 to detect extra arguments
                return;
            }
        }
        std::cout << *s++;
    }
    throw std::logic_error("extra arguments provided to printf");
}

The base case

Our base case is the function that we will (ideally) fall to. The constructs of the recursive templates will define the final call to this function. Now, the base case printf is just a simple means of printing a string

void printf(const char *s)
{
    while (*s) {
        if (*s == '%') {
            if (*(s + 1) == '%') {
                ++s;
            }
            else {
                throw std::runtime_error("invalid format string: missing arguments");
            }
        }
        std::cout << *s++;
    }
}

Our recursive case is the interesting bit:

template<typename T, typename... Args>
void printf(const char *s, T value, Args... args)
{
    while (*s) {
        if (*s == '%') {
            if (*(s + 1) == '%') {
                ++s;
            }
            else {
                std::cout << value;
                printf(s + 1, args...); // call even when *s == 0 to detect extra arguments
                return;
            }
        }
        std::cout << *s++;
    }
    throw std::logic_error("extra arguments provided to printf");
}

The first two lines define our variadic template. The ... operator defines our remaining types, while typename T defines our current type.

Let's consider:

printf("Hello, %s %s. You are %d years old.", "Justin", "Van Horne", 23);

The functional call graph will look like this:

printf(const char* s, const char* first, [Args... args {const char* last, int age}])

printf(const char* s, const char* last, [Args... args {int age}])

printf(const char* s, int age, [Args... args {empty}])

printf(const char* s)

kwargs situation

We need a way to construct the kwargs map while allowing kwarg values. We can easily do this with a few variadic templates.

void greet(const kwargs& kw = kwargs())
{
    std::string name = kw.get("user", std::string("somebody"));
    std::cout << "Hello, " << name << "!" << std::endl;
}

template <typename... Args>
void greet(kwargs kw, kwarg k, Args... args)
{
    kw[k.name] = k;
    greet(kw, args...);
}

template <typename... Args>
void greet(kwarg k, Args... args)
{
    kwargs kw;
    kw[k.name] = k;
    greet(kw, args...);
}

int main()
{
    greet("user"kw="Justin");
    greet();
}

The whole code

#include <string>
#include <map>
#include <memory>
#include <iostream>

class kwarg {
public:
    kwarg() = default;
    kwarg(const char* name)
        : name(name) { }

    struct kwarg_proxy_base { virtual ~kwarg_proxy_base() {}; };

    template <typename T>
    struct kwarg_proxy : kwarg_proxy_base {
        kwarg_proxy() = default;
        kwarg_proxy(const T& t)
            : t(t) { }
        typedef T type;
        const T& t;
    };

    kwarg& operator=(kwarg& rhs)
    {
        value = rhs.value;
        return *this;
    }

    template <class T>
    kwarg& operator=(const T& rhs)
    {
        value = std::shared_ptr<kwarg_proxy_base>(new kwarg_proxy<T>(rhs));
        return *this;
    }

    kwarg& operator=(const char* rhs)
    {
        char_value = std::string(rhs);
        value = std::shared_ptr<kwarg_proxy_base>(
                new kwarg_proxy<std::string>(char_value));
        return *this;
    }

    template<typename T>
    operator const T&() const {
        if (!value) return T();
        return dynamic_cast<const kwarg_proxy<T> &>(*value).t;
    }

    template <typename T>
    operator T*() const {
        if (!value) return nullptr;
        return &dynamic_cast<const kwarg_proxy<T> &>(*value).t;
    }

    template <typename T>
    operator T() const {
        if (!value) return T();
        return dynamic_cast<const kwarg_proxy<T> &>(*value).t;
    }

    const std::string name;
    std::string char_value;
    std::shared_ptr<kwarg_proxy_base> value;
};

kwarg operator "" kw (const char* literal, size_t n)
{
    return kwarg(literal);
}

class kwargs : public std::map<std::string, kwarg> {
public:
    template <typename T>
    const T& get(const std::string& key, const T& default_parameter = T()) const
    {
        auto it = this->find(key);
        if (it == this->end()) 
            return default_parameter;
        if (it->second.value) 
            return it->second;
        return default_parameter;
    }

    template <typename T>
    T get(const std::string& key, T default_parameter = T())
    {
        auto it = this->find(key);
        if (it == this->end()) 
            return default_parameter;
        return it->second;
    }
};

void greet(const kwargs& kw = kwargs())
{
    std::string name = kw.get("user", std::string("somebody"));
    std::cout << "Hello, " << name << "!" << std::endl;
}

template <typename... Args>
void greet(kwargs kw, kwarg k, Args... args)
{
    kw[k.name] = k;
    greet(kw, args...);
}

template <typename... Args>
void greet(kwarg k, Args... args)
{
    kwargs kw;
    kw[k.name] = k;
    greet(kw, args...);
}

int main()
{
    greet("user"kw="Justin");
    greet();
}

And there we have it. A pure C++ solution to keyword arguments. This is something I put together in a relatively short time period, so it is not a perfect solution by any means.