C++ :: Mapping User Input to Actions

Writing programs to perform some tasks is useful but writing programs that respond to user input are infinitely more useful. We will run through some common ways to respond to user input with varying degrees of flexibility and maintainability.

User input is usually given to us as text so these examples respond to user input as char. They could easily be modified to respond to std::string from using std::getline or enum from using an event based library like SDL2 or flatbuffers.

Function Pointer

This technique uses plain C function pointers. It’s fairly easy to implement and we can even move the functions to separate files to speed up compilation. If we don’t need to move the function pointers around too much, this will get the job done.

#include <iostream>
#include <unordered_map>

void A()
{
  std::cout << "A()\n";
}

void B()
{
  std::cout << "B()\n";
}

void C()
{
  std::cout << "C()\n";
}

int main()
{
  std::unordered_map<char, void (*)(void)> m
  {
    { 'A', A },
    { 'B', B },
    { 'C', C },
  };

  m.at( 'A' )();
  m.at( 'B' )();
  m.at( 'C' )();
}

/* output

A()
B()
C()

*/

Function Pointer to Lambda

When C++11 came onto the scene, we gained lambdas to make writing small routines easy at the site of the call rather than having to write the functionality externally. In this implementation, we replace the function pointers with lambdas. We’ve gained a very terse implementation and haven’t poisoned our namespace but lost the ability to compile each action separately.

#include <iostream>
#include <unordered_map>

int main()
{
  std::unordered_map<char, void (*)(void)> m
  {
    { 'A', [](){ std::cout << "A::lambda\n"; } },
    { 'B', [](){ std::cout << "B::lambda\n"; } },
    { 'C', [](){ std::cout << "C::lambda\n"; } },
  };

  m.at( 'A' )();
  m.at( 'B' )();
  m.at( 'C' )();
}

/* output

A::lambda
B::lambda
C::lambda

*/

Function Pointer to Lambda with Local State

Imagine you have some State object that your program passes around. You want to be able to perform some action based on the state but don’t want to modify the state. We can adapt our last implementation easily using function parameters. This is easily implemented using plain C function pointers as well using our first implementation above.

#include <iostream>
#include <unordered_map>

struct State
{
  int val1;
  bool val2;
};

int main()
{
  std::unordered_map<char, void (*)(State s)> m
  {
    { 'A', []( State s ){
      std::cout << std::boolalpha << "A::lambda " << s.val1 << " " << s.val2 << "\n"; } },

    { 'B', []( State s ){
      std::cout << std::boolalpha << "B::lambda " << s.val1 << " " << s.val2 << "\n"; } },

    { 'C', []( State s ){
      std::cout << std::boolalpha << "C::lambda " << s.val1 << " " << s.val2 << "\n"; } },
  };

  m.at( 'A' )( State{ 1, true } );
  m.at( 'B' )( State{ 2, false } );
  m.at( 'C' )( State{ 3, true } );
}

/* output

A::lambda 1 true
B::lambda 2 false
C::lambda 3 true

*/

OOP Interface

Now our program has grown quite large and we have too many actions to fit in a single file using our lambda approach above. We can use a trusty Object Oriented Programming Interface. This is easy to maintain but now we incur a VTable lookup at runtime due to the polymorphism used to hold the objects in a container. You will need a C++17 compliant compiler to use the structured binding.

#include <iostream>
#include <unordered_map>

struct Base
{
  virtual void run() = 0;

  virtual ~Base() {}
};

struct A : public Base
{
  void run()
  {
    std::cout << "A::run()\n";
  }
};

struct B : public Base
{
  void run()
  {
    std::cout << "B::run()\n";
  }
};

struct C : public Base
{
  void run()
  {
    std::cout << "C::run()\n";
  }
};

int main()
{
  std::unordered_map<char, Base*> m
  {
    { 'A', new A{} },
    { 'B', new B{} },
    { 'C', new C{} },
  };

  m.at( 'A' )->run();
  m.at( 'B' )->run();
  m.at( 'C' )->run();

  for ( auto& [k, v] : m )
  {
    delete v;
  }
}

/* output

A::run()
B::run()
C::run()

*/

Refactor the call to new

Let’s use std::unique_ptr to manage our memory for us in the map. It’s clear there will only be one instance of these objects for the lifetime of the program. For this, we will need a c++14 compliant compiler.

#include <iostream>
#include <unordered_map>
#include <memory>

struct Base
{
  virtual void run() = 0;

  virtual ~Base() {}
};

struct A : public Base
{
  void run()
  {
    std::cout << "A::run()\n";
  }
};

struct B : public Base
{
  void run()
  {
    std::cout << "B::run()\n";
  }
};

struct C : public Base
{
  void run()
  {
    std::cout << "C::run()\n";
  }
};

int main()
{
  std::unordered_map<char, std::unique_ptr<Base>> m{};
  m.emplace( 'A', std::make_unique<A>() );
  m.emplace( 'B', std::make_unique<B>() );
  m.emplace( 'C', std::make_unique<C>() );

  m.at( 'A' )->run();
  m.at( 'B' )->run();
  m.at( 'C' )->run();
}

/* output

A::run()
B::run()
C::run()

*/

OOP Interface with Local State

From the lambda example above, we can react to State passed to our actions using function parameters.

#include <iostream>
#include <unordered_map>
#include <memory>

struct State
{
  int val1;
  bool val2;
};

struct Base
{
  virtual void run( State ) = 0;

  ~Base() {}
};

struct A : public Base
{
  void run( State s )
  {
    std::cout << std::boolalpha
      << "A::run() " << s.val1 << " " << s.val2 << "\n";
  }
};

struct B : public Base
{
  void run( State s )
  {
    std::cout << std::boolalpha
      << "B::run() " << s.val1 << " " << s.val2 << "\n";
  }
};

struct C : public Base
{
  void run( State s )
  {
    std::cout << std::boolalpha
      << "C::run() " << s.val1 << " " << s.val2 << "\n";
  }
};

int main()
{
  std::unordered_map<char, std::unique_ptr<Base>> m{};
  m.emplace( 'A', std::make_unique<A>() );
  m.emplace( 'B', std::make_unique<B>() );
  m.emplace( 'C', std::make_unique<C>() );

  m.at( 'A' )->run( State{ 1, true } );
  m.at( 'B' )->run( State{ 2, false } );
  m.at( 'C' )->run( State{ 3, true } );

}

/* output

A::run() 1 true
B::run() 2 false
C::run() 3 true

*/

OOP Interface with Object and Local State

Now imagine that we want our actions to each perform differently based on some configuration we give them. We can instantiate each action using any type of constructor at runtime and still pass in our local State at runtime.

#include <iostream>
#include <unordered_map>
#include <memory>

struct State
{
  int val1;
  bool val2;
};

struct Base
{
  virtual void run( State ) = 0;

  ~Base() {}
};

struct A : public Base
{
  int init_val;

  A( int x ) : init_val{x} {} 

  void run( State s )
  {
    std::cout << "A::init_val " << init_val << "\n";
    std::cout << std::boolalpha
      << "A::run() " << s.val1 << " " << s.val2 << "\n\n";
  }
};

struct B : public Base
{
  bool init_val;

  B( bool x ) : init_val{x} {}

  void run( State s )
  {
    std::cout << std::boolalpha << "B::init_val " << init_val << "\n";
    std::cout << std::boolalpha
      << "B::run() " << s.val1 << " " << s.val2 << "\n\n";
  }
};

struct C : public Base
{
  double init_val;

  C( double x ) : init_val{x} {}

  void run( State s )
  {
    std::cout << "C::init_val " << init_val << "\n";
    std::cout << std::boolalpha
      << "C::run() " << s.val1 << " " << s.val2 << "\n";
  }
};

int main()
{
  std::unordered_map<char, std::unique_ptr<Base>> m{};
  m.emplace( 'A', std::make_unique<A>( 11 ) );
  m.emplace( 'B', std::make_unique<B>( false ) );
  m.emplace( 'C', std::make_unique<C>( 3.33 ) );

  m.at( 'A' )->run( State{ 1, true } );
  m.at( 'B' )->run( State{ 2, false } );
  m.at( 'C' )->run( State{ 3, true } );
}

/* output

A::init_val 11
A::run() 1 true

B::init_val false
B::run() 2 false

C::init_val 3.33
C::run() 3 true

*/

OOP Interface with Compile Time Object State and Runtime Local State

If you want to pass the configuration of your action at compile time, you can make primitive types template parameters. Unfortunately we are limited in the types that we can pass as template arguments see cppreference.com for more information.

#include <iostream>
#include <unordered_map>
#include <memory>

struct State
{
  int val1;
  bool val2;
};

struct Base
{
  virtual void run( State ) = 0;

  ~Base() {}
};

template <int val>
struct A : public Base
{
  int init_val = val;

  void run( State s )
  {
    std::cout << "A::init_val " << init_val << "\n";
    std::cout << std::boolalpha
      << "A::run() " << s.val1 << " " << s.val2 << "\n\n";
  }
};

template <bool val>
struct B : public Base
{
  bool init_val = val;

  void run( State s )
  {
    std::cout << std::boolalpha << "B::init_val " << init_val << "\n";
    std::cout << std::boolalpha
      << "B::run() " << s.val1 << " " << s.val2 << "\n\n";
  }
};

template <char val>
struct C : public Base
{
  char init_val = val;

  void run( State s )
  {
    std::cout << "C::init_val " << init_val << "\n";
    std::cout << std::boolalpha
      << "C::run() " << s.val1 << " " << s.val2 << "\n";
  }
};

int main()
{
  std::unordered_map<char, std::unique_ptr<Base>> m{};
  m.emplace( 'A', std::make_unique<A<11>>() );
  m.emplace( 'B', std::make_unique<B<false>>() );
  m.emplace( 'C', std::make_unique<C<'c'>>() );

  m.at( 'A' )->run( State{ 1, true } );
  m.at( 'B' )->run( State{ 2, false } );
  m.at( 'C' )->run( State{ 3, true } );

}

/* output

A::init_val 11
A::run() 1 true

B::init_val false
B::run() 2 false

C::init_val c
C::run() 3 true

*/

OOP Interface with Object State, Local State, and Boilerplate

What happens if we need to perform some boilerplate before and/or after each action? To limit the amount of overhead, we can use the Curiously Recurring Template Pattern. This still incurs a single VTable lookup from storage in a container but we only need to instantiate each object once rather than use a pointer to a function that holds the boilerplate that then instantiates our object. This is much more maintanable and flexible as we can have any number of Parent classes for different behavior.

#include <iostream>
#include <unordered_map>
#include <memory>

struct State
{
  int val1;
  bool val2;
};

struct Base
{
  virtual void run( State ) = 0;
  virtual ~Base() {}
};

template <typename Child>
struct Parent : public Base
{
  void run( State s )
  {
    std::cout << "Base::run() before\n";
    static_cast<Child*>( this )->run_impl( s );
    std::cout << "Base::run() after\n\n";
  }
};

struct A : public Parent<A>
{
  int init_val;

  A( int x ) : init_val{x} {} 

  void run_impl( State s )
  {
    std::cout << "A::init_val " << init_val << "\n";
    std::cout << std::boolalpha
      << "A::run() " << s.val1 << " " << s.val2 << "\n";
  }
};

struct B : public Parent<B>
{
  bool init_val;

  B( bool x ) : init_val{x} {}

  void run_impl( State s )
  {
    std::cout << std::boolalpha << "B::init_val " << init_val << "\n";
    std::cout << std::boolalpha
      << "B::run() " << s.val1 << " " << s.val2 << "\n";
  }
};

struct C : public Parent<C>
{
  double init_val;

  C( double x ) : init_val{x} {}

  void run_impl( State s )
  {
    std::cout << "C::init_val " << init_val << "\n";
    std::cout << std::boolalpha
      << "C::run() " << s.val1 << " " << s.val2 << "\n";
  }
};


int main()
{
  std::unordered_map<char, std::unique_ptr<Base>> m{};
  m.emplace( 'A', std::make_unique<A>( 11 ) );
  m.emplace( 'B', std::make_unique<B>( false ) );
  m.emplace( 'C', std::make_unique<C>( 3.33 ) );

  m.at( 'A' )->run( State{ 1, true } );
  m.at( 'B' )->run( State{ 2, false } );
  m.at( 'C' )->run( State{ 3, true } );
}

/* output

Base::run() before
A::init_val 11
A::run() 1 true
Base::run() after

Base::run() before
B::init_val false
B::run() 2 false
Base::run() after

Base::run() before
C::init_val 3.33
C::run() 3 true
Base::run() after

*/

OOP Interface with Local State and Variadic Templates for Chaining

Now imagine we have to perform many actions in response to user input and those actions need to be reused for different user input. We could go back to using pointers to lambdas that describe the multiple actions we will take but this is cumbersome.

Instead, we can use template parameter packs to have the compiler write this code for us while giving us a simple interface to chain these actions in a well-ordered fashion. See cppreference.com for more information.

This example passes the local State as a parameter at runtime but we’ve used references to indicate that the state will be changed by these actions. In a threaded environment where we perform these actions based on user input that comes from multiple locations (i.e. sockets and command line), we will need to lock and unlock an std::mutex associated with our State during the boilerplate in Exec.

#include <iostream>
#include <unordered_map>
#include <memory>

struct State
{
  int val;
};

struct Base
{
  virtual void operator()( State&& ) = 0;
  virtual ~Base() {}
};

template <typename... Middleware>
struct Exec : public Base
{
  void operator()( State&& s )
  {
    std::cout << "Exec::operator() before, lock mutex\n";
    ( Middleware{ s }, ... );
    std::cout << "Exec::operator() after, unlock mutex\n\n";
  }
};

struct A
{
  A( State& s )
  {
    std::cout << "A()\n";
    std::cout << "  State before " << s.val << "\n";
    s.val = 1;
    std::cout << "  State after  " << s.val << "\n";
  }
};

struct B
{
  B( State& s )
  {
    std::cout << "B()\n";
    std::cout << "  State before " << s.val << "\n";
    s.val = 2;
    std::cout << "  State after  " << s.val << "\n";
  }
};

struct C
{
  C( State& s )
  {
    std::cout << "C()\n";
    std::cout << "  State before " << s.val << "\n";
    s.val = 3;
    std::cout << "  State after  " << s.val << "\n";
  }
};

int main()
{
  std::unordered_map<char, std::unique_ptr<Base>> m{};
  m.emplace( 'A', std::make_unique<Exec<A>>() );
  m.emplace( 'B', std::make_unique<Exec<A,B>>() );
  m.emplace( 'C', std::make_unique<Exec<A,B,C>>() );

  (*m.at( 'A' ))( State{ 0 } );
  (*m.at( 'B' ))( State{ 0 } );
  (*m.at( 'C' ))( State{ 0 } );
}

/* output

Exec::operator() before, lock mutex
A()
  State before 0
  State after  1
Exec::operator() after, unlock mutex

Exec::operator() before, lock mutex
A()
  State before 0
  State after  1
B()
  State before 1
  State after  2
Exec::operator() after, unlock mutex

Exec::operator() before, lock mutex
A()
  State before 0
  State after  1
B()
  State before 1
  State after  2
C()
  State before 2
  State after  3
Exec::operator() after, unlock mutex

*/

Replacing the map

Lookup in an std::unordered_map takes constant time on average and linear time at the worst. Even with a much more efficient map implementation, we will still get amortized constant time in the worst case due to the chaining implementation required to mitigate hash collisions. We can fix this using enum and std::vector or std::array to obtain constant time lookup in the worst case.

We will go back to the first lambda example to showcase the std::array and enum use.

Also, to use an enum class, you will need to cast it to its underlying type. See Stack Overflow for more information.

#include <iostream>
#include <array>

enum Input
{
  A = 0, B = 1, C = 2
};

int main()
{
  std::array<void (*)(void), 3> m{
    [](){ std::cout << "A::lambda\n"; },
    [](){ std::cout << "B::lambda\n"; },
    [](){ std::cout << "C::lambda\n"; },
  };

  m.at( Input::A )();
  m.at( Input::B )();
  m.at( Input::C )();
}

/* output

A::lambda
B::lambda
C::lambda

*/