Multi-Paradigm C++: Combining Object-Oriented and Functional Programming

WEBINAR: On-demand webcast

How to Boost Database Development Productivity on Linux, Docker, and Kubernetes with Microsoft SQL Server 2017 REGISTER >

By Satprem Pamudurthy

Introduction

C++ supports multiple programming paradigms—object-oriented, generic, and functional. Although object-oriented and generic programming paradigms are widely used, the applications and design practices for functional programming in C++ are still being discovered. Until the arrival of the C++ 11 standard, the most C++ had to offer in terms of functional programming was type-erased functors such as boost::function. However, with the addition of variadic templates, generic lambdas, perfect forwarding, and the ability to return lambdas from functions, C++'s functional credentials are stronger than ever. In this article, we will see how we can combine elements of objected-oriented and functional programming paradigms to create highly modular, extensible, and loosely coupled components. We will also see equivalent object-oriented components. The goal of this article is not to claim that one is better than the other, but rather to introduce another way of thinking about component design.

Units of Abstraction and Extension

The building block of objected-oriented programming in C++ is a class. A class encapsulates data and methods operating on that data. In other words, a class is an abstraction of an entity's attributes, state, and behaviors. A common operation in programming is the specialization or extension of an abstraction. In OOP, this is implemented by using inheritance. Derived classes define specialized behaviors or extend the base class's behaviors. Thus, the unit of abstraction and extension in OOP is a class. What this means is that we need to define a new class even if we only want to extend the behavior of a single method.

In functional languages, functions define operations on data and have no state. Instead, a function's arguments encapsulate all the data necessary for its operation. Function behaviors are extended through decorators. In contrast to OOP, we can refine or extend the behavior a single function. Thus, the unit of abstraction and extension in functional programming is a function.

Functional programming languages have certain core principles: functions as first-class citizens, pure functions (immutable state, no side-effects), and composable generic functions. C++ is not a pure functional language. For instance, the language cannot guarantee that a function will not have side effects. However, we still can use ideas from functional programming to achieve what might be called a functional style of programming. In this article, we will look at a hybrid design approach that combines OOP's ability to encapsulate data and behaviors, with functional programming's ability to define granular behavior extensions.

RuntimeBoundMethod

Let me begin by introducing the RuntimeBoundMethod class, which I am going to use later in the examples. It is a callable template class that stores a callable object (as an std::function), whose first parameter is a reference to the type containing the RuntimeBoundMethod (similar to the implicit this in member functions). It takes a reference to the containing object in its constructor and passes it along to the stored function. This allows us to call a RuntimeBoundMethod as we would a member function. We can also specify the const-ness of the bound method with respect to the object containing the RuntimeBoundMethod.

namespace detail
{
   template<typename ObjType, typename R, typename... Args>
   class ConstRuntimeBoundMethodImpl {
      public: typedef ObjType Type;
      explicit ConstRuntimeBoundMethodImpl(ObjType *pThis) :
      pThis_(pThis) {
         if (!pThis) {
            throw std::runtime_error("Null this pointer");
         }
      }
      ConstRuntimeBoundMethodImpl
         (const ConstRuntimeBoundMethodImpl&) = default;
      ConstRuntimeBoundMethodImpl& operator=
         (const ConstRuntimeBoundMethodImpl&) = default;
      ConstRuntimeBoundMethodImpl
         (ConstRuntimeBoundMethodImpl&&) = default;
      ConstRuntimeBoundMethodImpl& operator=
         (ConstRuntimeBoundMethodImpl&&) = default;
         template<typename Callable>
      ConstRuntimeBoundMethodImpl& operator=(Callable&& f)
      {
         f_ = std::forward<Callable>(f); return *this;
      }
      template<typename... X> R operator()(X&&... args)
      const
      {
         if (!isBound())
         {
            throw std::runtime_error("Unbound method");
         }
         return f_(*pThis_, std::forward<X>(args)...);
      }
      bool isBound() const
      {
         return !!f_;
      }
      private: ObjType *pThis_;
      std::function<R (ObjType&, Args...)> f_; };
      template<typename ObjType, typename R, typename... Args>
      class RuntimeBoundMethodImpl
      {
         public: typedef ObjType Type;
         explicit RuntimeBoundMethodImpl(ObjType *pThis) :
         pThis_(pThis)
         {
            if (!pThis)
            {
               throw std::runtime_error("Null this pointer");
            }
         }
         RuntimeBoundMethodImpl(const RuntimeBoundMethodImpl&) =
            default;
         RuntimeBoundMethodImpl& operator=
            (const RuntimeBoundMethodImpl&) = default;
         RuntimeBoundMethodImpl
            (RuntimeBoundMethodImpl&&) = default;
         RuntimeBoundMethodImpl& operator=
            (RuntimeBoundMethodImpl&&) = default;
            template<typename Callable>
         RuntimeBoundMethodImpl& operator=(Callable&& f)
         {
            f_ = std::forward<Callable>(f); return *this;
         }
         template<typename... X> R operator()(X&&... args)
         const
         {
            if (!isBound())
            {
               throw std::runtime_error("Unbound method");
            }
            return f_(*pThis_, std::forward<X>(args)...);
         }
         template<typename... X> R operator()(X&&... args)
         {
            if (!isBound())
            {
               throw std::runtime_error("Unbound method");
            }
            return f_(*pThis_, std::forward<X>(args)...);
        }
        bool isBound() const
        {
           return !!f_;
        }
        private: ObjType *pThis_;
        std::function<R(ObjType&, Args...)> f_;
      };
   }
   // Namespace detail
   template<typename ObjType, typename R, typename... Args>
   class RuntimeBoundMethod : public std::conditional
      <std::is_const<ObjType>::value,
      detail::ConstRuntimeBoundMethodImpl<ObjType,
      R, Args...>, detail::RuntimeBoundMethodImpl<ObjType,
      R, Args...> >::type
      {
         typedef typename std::conditional
            <std::is_const<ObjType>::value,
            detail::ConstRuntimeBoundMethodImpl<ObjType, R,
            Args...>, detail::RuntimeBoundMethodImpl
            <ObjType, R, Args...> >::type BaseType;
         public: using BaseType::operator=;
         using BaseType::operator();
         using BaseType::isBound; explicit RuntimeBoundMethod
            (ObjType *pThis) : BaseType(pThis)
      {
   }
};

Example 1: Abstractions

Let's look at an example of creating abstractions in the two design approaches. We'll start with OOP. In the following listing, ICircusAct is an interface for circus acts with two tricks. CircusAct1 implements ICircusAct, and involves a Dog doing a combination of jumps and flips. Defining additional circus acts with a different set of characters does not require modifications to the ICircusAct interface; OOP allows us to create abstractions that are open to extension and closed to modification (Open/Closed Principle).

class ICircusAct
{
   public: virtual ~ICircusAct()
   {
   }
   virtual void performTrick1() const = 0;
   virtual void performTrick2() const = 0; };
   class Dog
   {
      public: void jump() const
      {
         std::cout << "jump" << std::endl;
      }
      void flip() const
      {
         std::cout << "flip" << std::endl;
      }
   };
   class CircusAct1 : public ICircusAct
   {
      public: explicit CircusAct1(const Dog& dog) : dog_(dog)
      {
      }
      void performTrick1() const override
      {
         dog_.jump(); dog_.flip();
      }
      void performTrick2() const override
      {
         dog_.jump(); dog_.jump(); dog_.flip();
      }
      private: Dog dog_;
   };
   void testObjectOriented()
   {
      auto act = std::make_unique<CircusAct1>((Dog())
   );
   act->performTrick1(); act->performTrick2();
}

Although inheritance is a powerful tool, it has certain drawbacks. As I mentioned earlier, extending the behavior even of even one method requires the creation of a new class. Inheritance also entails strong coupling between the base and the derived classes, and amongst the derived classes. John Lakos has a great presentation on various types of inheritance and their implications.

Now, let's see what a hybrid design would look like.

class CircusAct
{
   public: RuntimeBoundMethod<const CircusAct, void>
   performTrick1
   {
      this
   };
   RuntimeBoundMethod<const CircusAct, void>
   performTrick2
   {
      this
   };
}; decltype(auto) getCircusAct1()
{
   auto dog = std::make_shared<Dog>();
   CircusAct act;
   act.performTrick1 = [dog](auto&& self)
   {
      dog->jump(); dog->flip();
   };
   act.performTrick2 = [dog](auto&& self)
   {
      dog->jump();
      dog->jump();
      dog->flip();
   };
   return act;
}
void testObjectFunctional()
{
   auto&& act = getCircusAct1();
   act.performTrick1(); act.performTrick2();
}

The getCircusAct1 function creates a CircusAct, and binds performTrick1 and performTrick2 to their definitions. The methods are bound to their definitions independently of each other; this means that the definitions can be loosely coupled. It also means that creating new circus acts does not require a proliferation of new classes. The self parameters in the definitions are references to the CircusAct object. However, unlike member functions, they can access only the object's public interface.

This approach is more flexible and gives us a lot of freedom in creating different combinations of behaviors. But it is not without its own drawbacks. For instance, the dependencies and the intended behaviors of a CircusAct object do not depend its type alone. Instead, we will need to understand how the object has been wired at and after construction. This requires some adjustment on the part of the programmer when it comes to code analysis and debugging. Also, if you forget to bind a method, you will not find out about it at compile-time. In some cases, you can solve this problem by binding to a default implementation, if available.

Example 2: Extensions

Now that we've seen an example of creating abstractions using the hybrid approach, we are ready to move on to extensions. Continuing with the characters from the earlier example, let's say we wanted to measure the time each performer takes for the first trick. In object-oriented programming, we would implement this using the decorator pattern. The decorator pattern is a common objected-oriented pattern for extending an object's behavior. A decorator is a special implementation of an interface that forwards calls to an inner implementation while executing code around the forwarded calls. The next listing shows a decorator class that implements ICircusAct and times invocations of performTrick1 on the inner act.

class CircusActTimingDecorator : public ICircusAct
{
   public: explicit CircusActTimingDecorator
      (std::unique_ptr<ICircusAct> inner) :
      inner_(std::move(inner))
   {
   }
   void performTrick1() const
   {
      using namespace std::chrono;
      auto start = steady_clock::now();
      inner_-&gt;performTrick1();
      auto diff = steady_clock::now() - start; std::cout <<
         "performTrick1 took " << duration<double,
            std::milli>(diff).count() << " ms"
            << std::endl; } void performTrick2() const
      {
         inner_->performTrick2();
      }
      private: std::unique_ptr<ICircusAct> inner_;
   };
   void testObjectOriented2()
   {
      auto inner = std::make_unique<CircusAct1>((Dog())
   );
   auto act = std::make_unique<CircusActTimingDecorator>
      (std::move(inner));
   act->performTrick1(); act->performTrick2();
}

Because the unit of extension in objected-oriented programming is a class, we are forced to create a whole new class that implements and delegates all the interface methods even if we only want to decorate a single method. In the preceding listing, we only wanted to time performTrick1, but the decorator still had to implement performTrick2. If we add a third trick to the interface, the decorator would have to implement that one as well.

Can a hybrid approach do better? Let's find out.

template<typename Method>
void decorateWithTimer(Method& method,
   const std::string& methodName)
{
   method = [=](auto& self, auto&& ...xs) ->
      decltype(auto)
   {
      using namespace std::chrono; struct Timer
      {
         Timer(const std::string& name) :
            start(steady_clock::now()) , methodName(name)
         {
         } ~Timer()
         {
            auto end = steady_clock::now();
            auto diff = end - start;
            std::cout << methodName <<
               " took " << duration<double,
            std::milli>(diff).count() <<
               " ms" << std::endl;
         }
         time_point<steady_clock>
         start;
         const std::string& methodName;
      };
      Timer timer(methodName);
      return method(std::forward<decltype(xs)>(xs)...);
   };
}
void testObjectFunctional2()
{
   auto&& act = getCircusAct1();
   decorateWithTimer(act.performTrick1, "performTrick1");
   act.performTrick1(); act.performTrick2();
}

The decorateWithTimer function takes a RuntimeBoundMethod and executes timing code around it, while perfectly forwarding its arguments and its return value. It allows us to decorate just the method or methods we care about without touching the rest of the methods. This is exactly the kind of granular extensibility that functional languages provide.

Final Thoughts

In this article, we've seen an approach to designing classes that combines aspects of objected-oriented and functional programming paradigms. As I mentioned at the beginning, the goal is not to claim that one is better than the other. As with any other design problem, we need to pick the right tool for the job.

C++ is not a pure functional language, but ultimately, programming paradigms are not so much about language features as they are ways of thinking about component design. Thinking functionally will allow us to build highly modular components that are easy to compose and extend. Object-oriented and functional programming can coexist and C++ allows us get the best of both worlds—we can use classes to encapsulate entities, and function objects to define and extend their behaviors.



Related Articles

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • As all sorts of data becomes available for storage, analysis and retrieval - so called 'Big Data' - there are potentially huge benefits, but equally huge challenges...
  • The agile organization needs knowledge to act on, quickly and effectively. Though many organizations are clamouring for "Big Data", not nearly as many know what to do with it...
  • Cloud-based integration solutions can be confusing. Adding to the confusion are the multiple ways IT departments can deliver such integration...

Most Popular Programming Stories

More for Developers

RSS Feeds

Thanks for your registration, follow us on our social networks to keep up-to-date