C++ Objectified Arguments

Last year, I started experimenting with strong types for C++ arguments. After a year of working trying various approaches, I have found the sweet spot for creating APIs that are

  • strongly typed,
  • easy to use,
  • and compile efficiently.

Here is a quick reminder of what a strongly typed API looks like.

// see the strong type argument post linked above for how to define Argument
using SourceFilePath = Argument<const char *, struct SourceFileTag>; 
using DestinationFilePath = Argument<const char *, struct DestinationFilePathTag>; 

int copy_file_weak(
    const char * source,
    const char * destination

int copy_file_strong(
    SourceFilePath source,
    DestinationFilePath destination

//then calling method looks like this
//order of the argument is obvious

//hard to know which is the source/dest without checking the declartion

The example above illustrates some of the pros and cons of using strongly typed arguments.

  • Code is super easy to read 👍
  • Argument order is obvious when then looking at the implementation 👍
  • APIs are cumbersome to code 👎

An alternative approach is to define a lightweight class that packages up the arguments and provides setters and getters to access arguments. Consider the following example:

class CopyOptions {

    CopyOptions& set_source(const char * value){
        m_source = value;
        return *this;

    CopyOptions& set_destination(const char * value){
        m_destination = value;
        return *this;

    const char * source() const {
        return m_source;

    const char * destination() const {
        return m_destination;

    const char * m_source = nullptr;
    const char * m_destination = nullptr; //set to the default
static void copy_file_strong(const CopyOptions & options);
static void copy_file_weak(const char *  source, const char *  destination);

//strong call
//very obvious what the arguments are
//added bonus -- arguments an be in any order
//default argument values can be define in CopyOptions

//same problems as before

On the implementation side, this approach works much better. Code completion is especially nice because it picks up right away what options can be set. 👍

But we introduced a pretty cumbersome problem with the setters and getters 👎. To solve that we create a getter/setter macro that looks something like this.

#define API_ACCESS_FUNDAMENTAL(c, t, v, iv) \
    public: \
    t v() const { return m_##v; } \
    c& set_##v(t value){ m_##v = value; return *this; } \
    private: \
    t m_##v = iv

class CopyOptions {
    API_ACCESS_FUNDAMENTAL(CopyOptions,const char *,source,nullptr);
    API_ACCESS_FUNDAMENTAL(CopyOptions,const char *,destination,nullptr);

Now that is much better.

So the next question is whether we are still efficient (❓). To get an idea of the performance hit from this approach, I compiled a simple HelloWorld program (cross-compiled to the ARM Cortex M architecture) and checked the binary output size.

#include <cstdio>
#include <sapi/api/ApiObject.hpp>

#define PASS_BY_OBJECT 1

class ExecuteOptions {
    API_ACCESS_FUNDAMENTAL(ExecuteOptions,const char *,hello,nullptr);
    API_ACCESS_FUNDAMENTAL(ExecuteOptions,const char *,world,nullptr);
static void execute(const ExecuteOptions & options);
static void execute(const char *  hello, const char *  world);

int main(int argc, char * argv[]){
    execute("Hello", "World");
    return 0;

void execute(const ExecuteOptions & options){
    printf("%s %s\n", options.hello(), options.world());
void execute(const char *  hello, const char *  world){
    printf("%s %s\n", hello, world);

To my surprise, the programs were compiled to the same size (with optimization on, of course).

  • PASS_BY_OBJECT=1 code size: 548
  • PASS_BY_OBJECT=0 code size: 548

This example is particularly simple. If you are highly concerned about performance and code size, you will want to experiment. Nonetheless, this approach is a great way to write complex APIs with clear code that enforces strong argument types.

Last thing, using the Argument class for a strong bool argument is still worthwhile. Consider this code:

remove(true); //hmmm, what is true? what does that mean?
remove(IsRecursive(true)); //much better, recursive remove

//header looks like this
using IsRecursive = Argument<bool, struct IsRecursiveTag>; 
int remove(IsRecursive is_recursive);

There are some other places where Argument works well, but if it isn’t used sparingly, the code becomes quite cumbersome.