Giter Site home page Giter Site logo

modern_cpp_tutorials's Introduction

Table of contents

  1. Introduction
  2. Tutorial 1 - AOT
  3. Tutorial 2 - Type traits
  4. Tutorial 3 - Tuples
  5. Tutorial 4 - Functional programming
  6. Tutorial 5 - Dependency Injection
  7. Tutorial 6 - Static polymorphism (or three little pigs)
  8. Tutorial 7 - Type erasure

The remaining articles are available at: Documents

Introduction

These are some collection of working concepts that served me as teaching platform - for exploring the new features introduced by the latest C++ standards. Some of them were starting point in implementation of various utility classes and libraries in real projects, across different platforms (like socket library, logging mechanism, benchmarking, etc.). I’ve also used them to introduce the colleagues, as part of the internal discussions and knowledge transfer, the new approaches in realization of the well known topics, like concurrency and new memory model, the power of callable objects and functional programming, generic – template (meta)programming, chrono library, etc.

Please have in mind that using the code in commercial purposes is not permited (MIT licence), but on your own risk.
Primary, exposing it to the community is the way for getting the valuable feedback.
Thanks in advance.

Contact

Tutorial 1

AOT – Active Object Thread design pattern is introduced, as way to have asynchronous inter-thread communication, delegating the tasks to the background thread, enqueuing them into the tasks queue. The thread drains the queue and provides the execution context, a separate one from the thread(s) from which the tasks are sent, in which they will be executed sequentially, in order of arrival (FIFO). The caller thread can be also synchronized on result of task being executed, waiting on signaling the execution completion through the communication channel (future).

This concept is heavily used for asynchronous massage-based inter thread communication, especially for time consuming – blocking tasks that are delegated to the background thread in asynchronous way, where order of execution is preserved (first came, first served).
Typically, you would use this approach for

Tutorial 2

Type traits are small objects for inspecting the type (rather than value) at compile time. I’ve first encountered the type traits like constructs in “Modern C++ Design” introduced by A. Aleksandrescu and his Loki library. They are heavily used in template metaprogramming in various scenarios:

  • For SFINAE: imposing the template parameter substitution constraints: which types can be potentially consider as a valid for template parameter substitution (instantiation) both, as argument or return values.
  • For checking compile time condition that are type related (if constexpr)
  • For creating function overloading set (tag dispatching)
  • For any compile time type based decisions

This is small demonstration of using type traits with generic programming, where the type needs to be logged to logging medium either converted to a string (for numeric values), or expecting java-like user-defined types with public non-static member method “toString()”.
In pre C++17 times, this requirement couldn’t be done as elegant as with compile time if constexpr check

// Tag dispatching in action for C++14 compiler

using string_tag_v = enum class StringTagValues : uint8_t
{
   tag_string,
   tag_numeric,
   tag_enum,
   tag_invalid
};

using string_tag_t = std::integral_constant<string_tag_v , string_tag_v::tag_string>;
using numeric_tag_t = std::integral_constant<string_tag_v , string_tag_v::tag_numeric>;
using enum_tag_t = std::integral_constant<string_tag_v , string_tag_v::tag_enum>;
using invalid_tag_t = std::integral_constant<string_tag_v , string_tag_v::tag_invalid>;


template <typename T>
std::string conv2string(const T& t, invalid_tag_t)
{
    return {}; //type is not convertible to string
}

template <typename T>
std::string conv2string(const T& t, string_tag_t)
{
    return t;
}

template <typename T>
std::string conv2string(const T& t, numeric_tag_t)
{
    return std::to_string(t);
}

template <typename T>
std::string conv2string(const T& t, enum_tag_t)
{
    const auto e = static_cast<std::underlying_type_t<T>>(t);  

    /*
    * @note: for some compiler the sign promotion may happen
    * enum class : uint8_t
    * passing the scoped enum to std::to_string() may invoke singed promotion, i.e.
    * std::to_string(int) overloading will be called instead of expected std::to_string(unsigned)
    * In case that is building process set to fail on any compiler issues, additional 
    * cast to double is required
    * std::to_string(static_cast<double>(e));
    */
    return std::to_string(e); 
}

template <typename T>
struct is_string_compatible
{
  static constexpr bool value = std::is_same<std::decay_t<T>, std::string>::value ||
					std::is_constructible<std::string, T>::value ||
					std::is_convertible<T, std::string>::value;
};

template <typename T>
std::string conv2string(const T& t)
{
   return conv2string(t,
		   std::integral_constant<string_tag_v,
					is_string_compatible<T>::value ? string_tag_v::tag_string :
					std::is_arithmetic<std::decay_t<T>>::value ? string_tag_v::tag_numeric :
					std::is_enum<std::decay_t<T>>::value ? string_tag_v::tag_enum :  
					string_tag_v::tag_invalid>{});
}

Tutorial 3

Tuples

To be honest, I don't use tuples so frequently in everyday practice, although it's sometime inevitable, even recommended.
Therefore, I’ve decided to introduce some use-cases, where tuples can be quite useful.

First scenario would be if there is some repetitive assignment work, which is non-trivial, because you have some nullable, std::optional-like custom data type that may look like

template <typename T>
struct Optional
{
	bool valid;
	T value;
	...
};

Appealing characteristic of std::tuple, is that it's fixed-size heterogenous data structure – it can hold different data types.
We can write a generic class, with variadic number of parameters, stored into tuple as lvalue references.
To do that, we use std::tie() call

template <class T, class = void>
struct is_optional_t : std::false_type {};

template <class T>
struct is_optional_t<T, std::void_t<decltype(T::valid),	decltype(T::value)>> : std::true_type {};

template <class T>
static constexpr bool is_optional_v = is_optional_t<T>::value;

template <typename...Args>
class Setter
{
    public:

        explicit Setter(Args&...args) noexcept : m_values(std::tie(args...))
        {}
        explicit Setter(std::tuple<Args&...>& values) noexcept : m_values(values)
        {}

        template <std::size_t I, typename Arg>
        Setter& set(Arg&& arg)
        {

            /*
             * This is firstly meant for a non-trivial assignments, that are tedious to
             * write by hand
             *
             */
            auto& opt = std::get<I>(m_values);
            static_assert(is_optional_v<std::decay_t<decltype(opt)>>);
            
            if constexpr (is_optional_v<std::decay_t<Arg>>)
            {
                opt = std::forward<Arg>(arg);
            }
            else
            {
                opt.valid = true;
                opt.value = std::forward<Arg>(arg);
            }

            return *this;
        }

        template <typename...Values, typename Indices = std::index_sequence_for<Values...>>
        void setAll(Values&&...values)
        {
            static_assert(N >= sizeof...(Values), "Invalid number of arguments!");
            setAllImpl(Indices{}, std::forward<Values>(values)...);
        }

        template <std::size_t index>
        decltype(auto) get() const 
        {
            static_assert(N > index, "Index out of range!");
            return std::get<index>(m_values); 
        }
        
        template <typename Func>
        void for_each(Func func)
        {
            for_eachImpl(func, std::make_index_sequence<N>{});
        }

    private:

        template <std::size_t...Is, typename...Values>
        void setAllImpl(std::index_sequence<Is...>, Values&&...values)
        {
            if constexpr (sizeof...(Is) == 0) return;
            (set<Is>(std::forward<Values>(values)),...);
        }
	
        template <typename Func, std::size_t...Is>
        void for_eachImpl(Func func, std::index_sequence<Is...>)
        {
            (func(get<Is>()), ...);
        }

        
    private:

        std::tuple<Args&...> m_values; // Store arguments as lvalue references!
        static constexpr std::size_t N = sizeof...(Args);
};

Check the full example in the Compiler Explorer

Second use-case would be for user-defined types, where you want to provide class specific “less than” comparison operator, in order to be able to ascending sort the collection of this type using f.e. std::sort algorithm.

To accomplish this task, one can utilize on the std::tuple internal implementation of comparison operators, which is known as lexicographical (alphabetical) ordering. This way, you just wrap your type into std::tuple, instead of writing your own comparison logic, which can be tedious and sometime also error-prone task

Consider having simple class which represents the person

class Person
{
    public:

        Person(int _age, std::string _name, gender_t _gender) noexcept :
            age(_age)
            , name(std::move(_name))
            , gender(_gender)
        {}

        std::string getName() const { return name;}
        int getAge() const { return age;}
        gender_t getGender() const { return gender;}

    private:

        int age;
        std::string name;
        gender_t gender;
};

std::ostream& operator << (std::ostream& out, const Person& p)
{
    return out << "Name=" << p.getName()
               << ", Age=" << p.getAge()
               << ", gender=" << printEnum(p.getGender())
               ;
}

/*
* You can choose class scope - unary, or binary version of 
* the operator <
* The binary version can be implemented also as the friend function, and 
* wrap the private members as references within std::tie,
* instead of use the copied values with std::make_tuple
*/
bool operator < (const Person& p1, const Person& p2)
{
    /*
     * Lexicographic ordering
     *
     * It's relying on the std::tuple internal implementation of the
     * comparison operator ("less than"), instead of writing your own tedious,
     * and error-prone code
     *
     */
    return std::make_tuple(p1.getAge(), p1.getName(), p1.getGender()) <
           std::make_tuple(p2.getAge(), p2.getName(), p2.getGender());
}

On the other hand, there is no garantie of the semantical - logical correctness of the comparison.
Take for instance this example

struct Point { int x, y;};

Writing something like

bool operator < (const Point& p1, const Point& p2)
{
	return (p1.x < p2.x) && (p1.y < p2.y);
}

is wrong, in terms of the points in Descartes coordinate system.
Instead, the correct comparison would be

bool operator < (const Point& p1, const Point& p2)
{
	   // Distance from (0,0)
	   return (p1.x * p1.x + p1.y * p1.y) < (p2.x * p2.x + p2.y * p2.y);
}

If you want to rely on the lexicographical (alphabetical) ordering, you would need manually write, something like

bool operator < (const Point& p1, const Point& p2)
{
    if (p1.x < p2.x) return true;
    if (p1.y > p2.y) return false;
    
    return p1.x < p2.x;
}

which give you, once again, semantically an unsatisfactory result (doesn't concern the coordinates as absolute distance from (0,0)).

Check the source code for more details on this topic.

Tutorial 4

Functional programming

It's a common mistake thinking on C++ as a solely OOL - it's a hybrid language that supports the OOD paradigm, but It's also the functional language, especially having in mind that the std library itself is written in a way to follow the principles of the functional programming: having generic higher-order functions that take as argument another functions, or return another functions as result.

The basic idea is to have a generic code, highly customizable, that can be tailored to different kind of scenarios, by specifying the callable object that will be internally applied, usually on some iterable collection. This generic code introduces some abstraction, by hiding the implementation details - the complexity if you like, and providing the more expressive code - the code which is concise, compact, easy to read and understand, the code which is rather declarative, than imperative.

For those who are familiar with Java Streams, they embody these kind of principals: having chain of composable operations - monads, that transform the stream until they reach the terminal operation

List<Person> persons = …
persons.stream()
       .filter(person->person.getAge() >= age) //lambda
       .map(Person::getName) // non-static member method reference
       //.forEach(System.out::println) // static member method reference
      .collect(Collectors.toList());

Here, instead of being burden with implementation details ("how" the operators are implemented) - imperative programming, we are more focused on "what" should be done, as with declarative programming - expressing our intentions with callable objects on a chained operators. You can think of it as Strategy design pattern. Talking about desing patterns, there is ReactiveX library. It's basically an implementation of Publisher-Subscriber architectural pattern, that comes in different languages. Java implementation is konw as RxJava. The syntax is very similar with Java Streams, but that is where all other similarity seas. Instead of iterating over the collection, the reactive library is for having asynchronous event-based communication between observable (publisher) and observer (subscriber). We can even specify different thread contexts in which the observable will emit events/items, from the one in which the observer will receive these items. The library itself provides all the necessary infrastructure, so that we, once again, have declarative rather than imperative programming.

public Disposable test_mapToList()
{
    Button personsButton = (Button) app.findViewById(R.id.button);
    Observable<Unit> personsButtonPublisher = RxView.clicks(personsButton);

    return personsButtonPublisher
            .subscribeOn(Schedulers.io())
            .map(e->{
                List<Person> persons = new ArrayList<Person>();
                Collections.addAll(persons,
                        new Person("Alex", 7, Gender.MALE),
                        new Person("John", 45, Gender.MALE),
                        new Person("Marry", 47, Gender.FEMALE)
                );
                return persons;
            })
            .flatMap((Function<List<Person>, Observable<List<String>>>) persons-> {
                return Observable.fromIterable(persons)
                        .filter(person->person.getAge() > 18)
                        .map(Person::getName) // transformation function f: Person->String
                        .toList()
                        .toObservable();
            })
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(this::updateNames);
}

But, how C++ implementation may look like?

The first, traditional approach - using good-old loop

/*
 * Imperative way
 *
 * This is actually kind of lifting the generic filter function to
 * be used by the higher-order namesOf function, without having any
 * side-effects: changing the input data
 * So, it's more functional approach, but it uses loop instead of std algorithm, and
 * it's less composable as the genuine functional approach
 */

using persons_t = std::vector<Person>;

template <typename FilterFunc>
std::vector<std::string> namesOf(const persons_t& persons, FilterFunc filter)
{
    std::vector<std::string> names;
    names.reserve(persons.size());

    for (const auto& person : persons)
    {
        if (filter(person))
        {
            names.push_back(std::move(person.getName()));
        }
    }

    return names;
}

This would be an imperative - or halfway declarative approach. To turn it into truly declarative way, so that the code is more expressive and reusable, we need to make separation of concerns. But this flexibility doesn't come without costs. The main problem is that std algorithms are not composable. The reason for that is that they use iterators instead of the concreate collections, which requires the auxiliary memory space: if we want to have a pure function which doesn't alter input collection

Filter function

template <typename UnaryPredicate> 
persons_t filterPersons(const persons_t& persons, UnaryPredicate p)
{
    persons_t filteredPersons; // auxiliary memory space
    filteredPersons.reserve(persons.size());

    std::copy_if(persons.cbegin(), persons.cend(), std::back_inserter(filteredPersons), p);

    return filteredPersons;
}

Transformation function would be

template <typename Func, typename R = std::invoke_result_t<Func, const Person&>>
auto mapPersons(const persons_t& persons, Func func)
{
    std::vector<R> result; // auxiliary memory space
    result.reserve(persons.size());

    std::transform(persons.cbegin(), persons.cend(), std::back_inserter(result), func);

    return result;
}

Eventually, we can write something like

print(mapToNames(adults(persons)); // from inner to outher call

Note For more details please visit: Turorial 4

There is (starting with C++20) range library, which simplified the syntax using '|' operator

auto adultsNames(const persons_t& persons) 
{
    return persons | filter(is_adult)| transform(to_name); // from left to right
}

As we can see, this is the most resemble to the Java Streams solution, without additional penalties in terms of introducing the auxiliary memory space.

Functors and monads

Functor: the function which takes as input argument class template of one instance F, and return the transformed instance of the same class template F:

template <typename T1, typename Func>
auto map(F<T1> t1, Func f)->decltype(F(f(T1))) 
{
   ...
}

where f: T1->T2 is transformation function. 

Typical example of a functor is std::optional

template <typename T, typename Func>
auto map(const std::optional<T>& t, Func f) -> decltype(std::make_optional(f(*t)))
{
    if (t) return std::make_optional(f(*t));
    return {};
}

Let assume we have this two transformation functions

std::string f1 (const std::string& s); // first transformation function
std::string f2 (const std::string& s); // second transformation function

Now we can write

map(map(s, f1), f2); //pay attention that map(s, f1) wraps the result into std::optional<std::string>

The problem arises when transformation functions return std::optional itself, to indicate the outcome of operation, since map(s, f1) would then return std::optional<std::optionalstd::string>. This is where monads come in rescue, by slightly redefine the transformation helper function

template <typename T, typename Func>
auto map(const std::optional<T>& t, Func f) -> decltype(f(*t))
{
    if (t) return f(*t);
    return {};
}

One interesting monad is also std::future which is used to wait on result of the asynchronous task. We would usually call std::future::get method, which is blocking call. In order to be able to combine the results of asyncrhonous tasks in a functional way, we need helper function that converts blocking get call into non-blocking one.

@see Herb Sutter's talk

/*
 * std::future<T> as monad.
 *
 * To compose the futures (or better say, asynchronous tasks),
 * we need to convert the blocking std::future::get call into
 * non-blocking one, by spawning another asynchronous task that will wait
 * on previous one being signaled - result being returned
 */
template <typename T, typename Func>
auto then(std::future<T>&& f, Func func)
{
    return std::async(std::launch::async, [f = std::move(f), func]() mutable
            {
                return func(f.get());
            });
}

Since this is presentation from the second hand, for this and many other concepts of functional programming, I would highly recommend to read this exelent book "Functional Programming in C++" by Ivan Čukić

Tutorial 5

DIP - Dependency Inversion Principle is one in collection of well-known SOLID principles, which in short advises that in order to loosely couple on dependency, we should depend on the abstraction rather than on the concreate implementation. This way, we decouple the behavioral aspect of the interface, from its concrete implementation. In other words, the Client shouldn't be aware of implementation details - nor it should directly instantiate the dependency object, the Service. This should be responsibility of another component, Injector - that would inject the dependency at client side either through

  • Constructor of the client
  • Setter method
  • Interface method
template <typename Service>
struct ServiceSetter
{
    virtual void set(std::shared_ptr<Service> service) = 0;
};

template <typename...Services>
class Client : public ServiceSetter<Services>...
{
};

In some languages (like Java), there are even DI Frameworks that can be used for introducing the highly configurable runtime dependency injection (Spring, Dagger, etc.).

But, what would be benefit of this?
One obviously is for having flexible code, that can be easily configured for different kind of dependency implementations, without paying the price of refactoring - changing the code, since the interfaces remaining the same.
Another benefit would be simplified test scenarios, where the real implementation is replaced with the mocking one.

Implementation

There is another, more generic term: IOC-Inversion Of Control, where DI is just one variation of it.
We usually talk about the IOC container - a centralized component that holds all dependency modules across the process. Internally, it holds two associative arrays, since the DO (Dependency Object) can be either obtained as a shared (already created) instance, or the client can require new instance of DO, which will be then factored - by invoking the matching factory. This means, that we need to store the arbitrary number of arguments required by the factory, for being able to create on demand, a new DO instance - on the same argument list. We could use, once again, tuple as a storage for these arguments

template <typename DIServiceInterface, typename DIService, typename...Args>
class DIFactory final : IFactory<DIServiceInterface>
{
    ...

    /**
     * For binding the arguments of dependency object - service creation,
     * with factory method
     *
     * <p>
     * The service factory will be placed into IOC container.
     * In case that expectation is that each call of the @see DIFactory#create
     * creates the new instance of the service implementation, to accomplish that,
     * the arguments for the factory function need to be stored into tuple
     *
     * @param args  Service implementation construction arguments
     */
     explicit DIFactory(Args&&...args) noexcept :
        m_args(std::make_tuple(std::forward<Args>(args)...))
    {}

private:
    std::tuple<std::decay_t<Args>...> m_args; // store the arguments
};

To produce a new instance of DO, we call eventually std::apply on that tuple

/**
 * Factory method: creates the dependency object, a
 * Service that will be injected at client side.
 *
 * @return  Reference to the service concrete implementation upcasted (RVO)
 *          to the matching interface
 *
 * @note template <class T> class A{ public: A(T* t);};
 * There is no hierarchy "is a" relationship between A<Base> and A<Derived> instances
 * (only between template parameters!)
 */
std::unique_ptr<DIServiceInterface> create() override
{
    return std::apply(
        [](auto&&...args)
        {
            return factory<DIService>(std::forward<decltype(args)>(args)...);
        }
       , m_args);
}

In order to be able to store in homogenous collections objects of a different type - we need some kind of universal type-container, that could hold virtually any type (type erasure).

Hint: We don't want to use void *.

Since C++17, std library offers exactly what we need - std::any.
Taking into account constraint, that contained type needs to be copy-constructible, we will use std::shared_ptr to
reference both, not only DO, but also the fatory object.

using factories_map = std::unordered_map<std::type_index, std::any>;
using instances_map = std::unordered_map<std::type_index, std::any>;

/**
* Adding to container the factory method for creating the matching
* dependencies
*
* @tparam DIService        Service interface
* @tparam DIServiceImpl    Service concrete implementation
* @tparam Args             Arbitrary argument types
* @param  args             Arguments to construct the concrete service implementation
*/
template <class DIService, class DIServiceImpl, typename...Args>
inline void DIContainer::add(Args&&...args)
{
    using namespace std;

    const auto id = type_index(typeid(DIService)); //C++ "reflection": service interface type index as a key

    const auto it = m_diFactories.find(id);
    if (it != m_diFactories.end()) throw std::runtime_error("DI: Service factory already specified!");

    auto factory = make_factory<DIService, DIServiceImpl>(std::forward<Args>(args)...);
    if (!factory) throw std::runtime_error("DI: Service factory not created!");

    // Save instance - for the case that shared instance is required

    m_diServices[id] = std::any(std::shared_ptr<DIService>(factory->create()));

    // Save service factory - for the case that new instance is required

    m_diFactories[id] = std::any(std::shared_ptr<IFactory<DIService>>(std::move(factory)));
}

At client side, we are only aware of the interface - not the concrete implementation.
To achieve that, to inject a proper implementation, we rely on the C++ "reflection", mapping the index of the service interface type (std::type_index) to the associated counterpart

/**
 * Retrieving at client side the concrete service implementation, based on
 * the given service interface type - its std::type_index
 *
 * @tparam DIService    Service interface
 * @param shared        Whether to retrieve the shared reference to the service, or
 *                      create a new instance
 * @return              The reference to the concrete service implementation
 */
template <class DIService>
inline std::optional<std::shared_ptr<DIService>> get(bool shared)
{
     using namespace std;
     // Service interface as key to find the associated implementation
     const auto id = type_index(typeid(DIService)); 
     /*
      * If the shared instance of the service implementation is required,
      * grab from the container the matching one
      */
     if (shared)
     {
         if (const auto it = m_diServices.find(id); it != m_diServices.end())
         {
             return std::any_cast<std::shared_ptr<DIService>>(it->second);
         }
         else
         {
             cerr << "DI: Service implementation not found!\n";
             return nullopt;
         }
     }

     // Otherwise, return the new instance of the service implementation

     if (const auto it = m_diFactories.find(id); it != m_diFactories.end())
     {
         const auto& factory = std::any_cast<const std::shared_ptr<IFactory<DIService>>&>(it->second);
         return factory->create();
     }
     else
     {
         cerr << "DI: Service factory not found!\n";
         return nullopt;
     }
}

Additionally, we can write a generic client as well, that stores the dependencies into type-safe unions std::variant,
and use the visitor pattern along with the function object whose overloading resolution set covers all dependencies.
Usually, we want to target the single dependency, rather than visit all of them at once.
For that, we will call std::holds_alternative explicitly, to check whether DO is held by the union

/**
* Provide for the particular service type
* the callable object that will be invoked
* on the injected service implementation
*
* @param func  Function object that will be invoked on the injected
*              service implementation
* @return      The return value of invocation, for existing service: otherwise, exception will be thrown
*/
template <typename Service, typename Function>
decltype(auto) call(Function func)
{
    if (auto it = std::find_if(m_services.begin(), m_services.end()
    , [](const auto& service)
    {
        return std::holds_alternative<Service>(service);
    }
    ); it != m_services.end())
    {
        return func(std::get<Service>(*it));
    }
    else
    {
        throw std::logic_error("<DI> Non-existing service required!");
    }
}

The complete source code with examples is available at: Tutorial 5

Tutorial 6 - Static polymorphism (or three little pigs)

Static polymorphism is a collection of the template-based technics to have configurable code, where all dependencies are resolved at the compile-time, through the template instantiation.
One can understand this as a static dependency injection

Three little pigs

Img.1 Three little pigs

I've addressed this topic metaphorically "Three little pigs". As in the story, they are as any siblings resembled,
but yet essentially different. And it takes some time (practice), to recognize these differences.
I'll not reveal who is my favorite one, but I'll do introduce them - so that you can choose on your own preferences.

Mixin (Pig#1)

Mixin class (or just mixin) is in essence the parameterized inheritance, it's the way to add additional value - functionality
to the templetized base class, similar to the Decorator design pattern.
Difference is that there is no "is a" relationship between the mixin - host class, and the base class - they
don't share the same interface, but rather they encapsulate different - orthogonal features.
It's also known as collaborator-based (or role-based) design, where each mixin class has a distinguished role - that can be easily combined with other roles - into resulting type which embeds all of these roles.

There are two issues that should be considered, when we use mixin technic in our design:

Scalability

Problem: With linear mixing: Mixin_1<...Mixin_n<A>...>, the resulting type can become quite complex and eventually unmanageable.
For overcoming this issue, so called Mixin Layer[1] is introduced.
Each layer represents a single collaboration, capturing the related roles in form of inner mixins

template <typename AnotherLayer>
class ThisLayer : public AnotherLayer
{
    public:
        // Inner mixin classes
        class Mixin1 : public AnotherLayer::Mixin1{...};
        class Mixin2 : public AnotherLayer::Mixin2{...};
};

This way we relax the resulting syntax and have more scalable way of exercising this technic

Passing the data - constructing the mixins

Constructing the result type by passing innermost non-mixin class constructor overload set
to all mixin subclasses, including the outermost one: using Super::Super
This works for the case where the mixin classes only contribute with additional functionality (are stateless),
and therefore being default constructible

template <typename Super>
class ConsoleLogger : public Super
{
    public:
        /* 
         *  Inherits super class c-tor overload resolution set!
         *  It can be constructed in the same way, as base class.
         */
        using Super::Super; 
    ...
};

If this is not the case, if there is a mixin class in chain that is stateful: non-default constructable,
we need to employ variadic templates

template <typename Super>
class TimeStamp : public Super
{
    /**
     * Mixin class which is not default-constructible
     *
     * @param format    The desirable timestamp format
     * @param args      Arbitrary argument list for constructing the base class
     */
    template <typename...Args>
    explicit TimeStamp(const std::string& format, Args&&...args) noexcept :
                         Super(std::forward<Args>(args)...)
                         , m_format(format)
    {}
    ...
};

At the time when the article[1] was published, both of these features were not part of the std library.

The source code which demonstrates using of this technic: mixin

Policy-based design (Pig#2)

Apparently, the term is first used in the book[2] which had the major impact on the future development of the entire language.
It's another aspect of the parameterized inheritance, where this time the functionality of the template class (policy) is plugged-in into the interface of the derived - host class, through inheritance (public or private).
It can be seen as strategy design pattern at compile-time, where at client side a different policy implementations can be introduced, which makes the code highly configurable - especially for the library writers.
When we talk about policies, we talk about the different (postponed) design decisions that customize the generic code at client side to fulfill certain requirements, as for

  • locking (whether it will be used in single-thread, or multi-thread environment)
  • allocation (on the heap, or stack)
  • logging (on console, file system, or data base)

etc.

A basic pattern for using the policy-based designed

template <typename LoggingPolicy>
class Host : private LoggingPolicy
{
    public:
        template <typename…Args>
        void f(Args&&…args)
        {
            LoggingPolicy::log(std::forward<Args>(args)…);
            doSomething(std::forward<Args>(args)…);
            ...
        }
};

You can even combine this with Template method pattern

template <typename LoggingPolicy>
class Base : private LoggingPolicy
{
    public:
        void f(const std::string& s)
        {
            LoggingPolicy::log(s);
            doSomething(s);
        }
    protected:
       virtual void doSomething(const std::string& s) = 0; // "template" method - customization point
};

template <typename LoggingPolicy>
class Derived : public Base<LoggingPolicy>
{
    private:
        void doSomething(const std::string& s) override {...}
};

The comprehensive example of using the locking (threading) policy, can be found at locking policy.
The inspiration was the famous Loki library[3].

CRTP - Curiously Recurring Template Pattern (Pig#3)

For a change, let start with a reference code snippet that will lead us to the definition

template <typename Implementation>
class Base : public Implementation
{
    // C-tor is private
    Base() {}
    friend Implementation;

    public:

        // Factory method
        template <typename...Args>
        static std::unique_ptr<Base> create(Args&&...args) noexcept
        {
            if constexpr ((std::is_constructible_v<Implementation, Args&&> && ...)) // unary right fold expression
            {
                return std::make_unique<Implementation>(std::forward<Args>(args)...);
            }
            else
            {
                return nullptr;
            }
        }


        template <typename...Args>
        decltype(auto) f(Args&&...args)
        {
            return impl().f_impl(std::forward<Args>(args)...); // for prevent shadowing the names
        }
        ...
    private:
        Implementation& impl()
        {
            return static_cast<Implementation&>(*this);
        }
};

The reason to make the constructor of the base class private, is to prevent mismatch[4] in specifying the template parameter:

class A :public Base<A>{};// OK
class B :public Base<A>{};// compiles, but is mismatch!

We need to provide the derived-specific implementation of the base interface

class A : public Base<A>
{
    public:
        explicit A(int i) noexcept;
        void f_impl(std::string_view s){...};// Derived class implementation - A::f_impl()
        ...
};

At the client side, we can write this pseudo code, to demonstrate the technic

	
class Client
{
    public:
        explicit Client(int i) noexcept : m_ptrA(Base<A>::create(i))
        {}
        void foo(std::string_view s, int i)
        {
            if (!m_ptrA) throw std::runtime_error("Invalid pointer.");
            m_ptrA->f(s);// Base class interface - Base<A>::f()
            ...
            
        }

    private:
       std::unique_ptr<Base<A>> m_ptrA;
};

As you could see, the CRTP is the way to have derived-specific implementation that will be invoked through the base class interface, whereby the derived class has no "is a" relationship with base class, nor
inherits the interface of the base class - it just provides the implementation behind the base class interface.
In practice, that usually means having platform-specific implementation of the base interface (f.e. Audio Driver), which is
exactly the main idea of the static polymorphism - having the variations of the base interface implementations, resolvable at compile-time.

Final thoughts: when you master these valuable technics, with help of your imagination and creativity, you can
easily turn the "three little pigs" into "three musketeers"

Three musketeers

Img.2 Upgraded version of my initial sketch - Alex, my 6 years old son

The complete source code with examples is available at: Tutorial 6

References

[1]: Mixin-Based Programming in C++, Yannis Smaragdakis, Don Batory
[2]: Modern C++ desing, Andrei Alexandrescu
[3]: Loki library
[4]: Fluent C++

Tutorial 7- Type Erasure

Usually, similar tutorials start with explaining different meanings of the type erasure in different languages.
I'll pursuit the same approach here - starting with type erasure in Java.
Working on Android platform, beside of native code in C++, we also heavily use Java(Kotlin) as well.
Secondly, I wanted to make an homage to one of my favorite authors, Bruce Eckel1.

Java type erasure

In Java, templates were not originally included into language core.
Instead, being inspired with C++, generics are for the first time introduced with Java SE5.
For the reason of migration compatibility with older non-generic code, instead of reification
(retaining the type info at run-time: converting the parameterized type into concreate one through specialization),
the generics are implemented using type erasure 2.

What does it actually mean?

Having code like this in Java

public static <T> List<T> filter(T[] in, Predicate<T> p)
{
    List<T> out = new ArrayList<>();
    for (T el : in) {
        if (p.test(el)) { // filter criteria
            out.add(el);
        }
    }
    
    return out;
}

will result in parameterized type T being internally substituted with root base Object type: T->Object
This doesn't give us a lot flexibility. We can't write, as with C++, something like

template <typename T>
class A 
{
  public:
        explicit A(T obj) : m_obj(std::move(obj))
        {}
        void foo()
        {
           m_obj.doSomething();
        }
              
  private:
        T m_obj;
};

since Object interface doesn't have "doSomething" method.
We can't use any run-time constructs like new, instanceof, or any reflection indeed,
since we don't have type information at run-time: it's stripped away, treating any parameterized type as Object.

What is the benefit of that - why don't we use the Object directly instead?

There are (at least) two reasons:

  • We express with T the intention - generic type (code)
  • Type-safety: we still have compile-time type check

Consider this code

public static void printAnyList(List<?> list) {
    for (Object el : list) {
        System.out.println(el);
    }
}

and

public static void printObjectList(List<Object> list) {
    for (Object el : list) {
        System.out.println(el);
    }
}

where the body of these two methods is the same, but only the first one (argument) is truly generic

List<Integer> list = Arrays.asList(new Integer[]{1,2,3});
printAnyList(list); // OK: List<Integer> is subtype of List<?>
printObjectList(list); // Error: List<Integer> and List<Object> are not related!

We can make this more useful, by bounding the type erasure with some more concreate type than Object.
We can bound type erasure to the upper bound: <? extends T> (T and all subtypes of T)

public static void startVehicles(List<? extends Vehicle> vehicles) {
    for (Vehicle vehicle : vehicles) {
        vehicle.drive();
        …
    }
}

or we can bound it to the lower bound: <? super T> (T and all super-types of T)

public static void addCars(List<? super Car> vehicles, Car[] cars) {
    // Collections.addAll(vehicles, cars);
    for (Car car : cars) {
       vehicles.add(car);
    }
}

C++ type erasure

In C++ type erasure has nothing to do with compiler - it's all about design.
It's powerful design technic.
As Klaus Iglberger3 pointed out in his excellent talk, these are three main
pillars of this technic:

  • External polymorphism (and some other patterns)
  • templated constructor (of the enclosing class)
  • non-virtual interface (of the enclosing class)

External Polymorphism

It's design pattern that enables treating (in terms of algorithms) by inheritance unrelated - different types
as they polymorphically same: as they have a common interface.
As stated in accompanied article4, one of the reasons for using this technic would be
in case that you have classes originated from different 3rd party libraries for which you can't introduce
common ancestor in the inheritance tree, and then lately in code upcast them to this common base pointer
(dynamic polymorphism).

Instead, we introduce desired adaptation interface.

Let assume that we want to have a way for logging these classes on some logging medium.
We specify following adaptation interface, that will be used as a gateway for all these unrelated types

struct Logging
{
    virtual ~Logging() = default;
    virtual void log() const = 0;
    
    protected:
        Logging() = default;
};

Now we introduce the implementation of the interface that is "type agnostic"

template <typename T, typename Logger>
class LoggingImpl final : public Logging
{
    public:
        LoggingImpl(const T& type, const Logger& logger) noexcept :
              m_obj(type)
            , m_logger(logger)
        {}

        ~LoggingImpl() override = default;

        void log() const override
        {
              // m_logger.log(dump<T>(m_obj)); // template helper function
               m_logger.log(dump(m_obj)); // free function
        }

    private:
        const T& m_obj; // unmutable object to log
        const Logger& m_logger; // unmutable logger itself
};

Actually, any type candidate, would need eventually to adapt its dumping signature, in order to be invoked
uniformly - in polymorphic way.
This is here done with the dump() call, as a "signature adapter".
This can be either a free function, or a template helper function

template <typename T>
std::string dump(const T& t)
{
    return t.toString();  // this would assume that most of the classes have already the same "java-like" signature
}

and for those types who don't share the same signature - we have overloading (full) specialization

std::string dump<A>(const A& a)
{
    return a.print();
}

Follow the link to see the complete source code.

Let's go one step back - to the behavioral aspect of the (erased) type.
We specify through the interface, the behavioral affordances as a gateway for parameterized
type specific implementation that needs to cope with it, without imposing any relationship
between types - through inheritance.
It's also known as duck typing - what behaves as a duck, will be considered as a duck.
This allows us to keep the interface and its parameterized type based implementation private (pimpl idiom).
For that, we need an enclosing - wrapper type

class Vehicle final
{
    private:
      // Interface
      struct VehicleConcept
      {
          virtual ~VehicleConcept() = default;
          virtual void drive(drive_type type) const = 0;
          virtual void configure() = 0;
        protected:
          VehicleConcept() = default;
      };

      // Internal parameterized type based implementation (pimpl idiom)
      template <typename VehicleType, typename Configurator>
      class VehicleConceptImpl final : public VehicleConcept
      {
        public:
           using vehicle_type = VehicleType;
           using configurator_type = Configurator;

           VehicleConceptImpl(VehicleType vehicle, Configurator configurator) noexcept
               : m_vehicle(std::move(vehicle)) // take the ownership over the type
               , m_configurator(std::move(configurator))
           {}

           // Interface implementation
           void drive(drive_type type) const override
           {
               m_vehicle.drive(type);
           }

           void configure() override
           {
               m_configurator.configure(m_vehicle);
           }

         private:
           vehicle_type m_vehicle;
           configurator_type m_configurator;
       };

       // the rest of code as non-virtual public interface
};

and therefore the appropriate - templated constructor : as an entry point for customization

class Vehicle
{
  public:
    template <typename VehicleType, typename Configurator>
    Vehicle(VehicleType&& vehicle, Configurator&& configurator)
        : m_vehicle(std::make_unique<VehicleConceptImpl>(
              std::forward<VehicleType>(vehicle)
            , std::forward<Configurator>(configurator)))
    {}
  private:
    mutable std::unique_ptr<VehicleConcept> m_vehicle; 
}

In order to use our enclosing type with "value semantics" - to design the user-defined type
as an built-in type, that can be created on the stack, copied/moved, etc., we need to extend the
initial interface with so called "virtual copy-constructor"5

// Interface
struct VehicleConcept
{
    virtual std::unique_ptr<VehicleConcept> clone() const = 0;   
};

// Internal interface implementation
template <typename VehicleType, typename Configurator>
class VehicleConceptImpl : public VehicleConcept
{
  public:
    std::unique_ptr<VehicleConcept> clone() const override
    {
        return std::make_unique<VehicleConceptImpl>(*this);
    }
};

This allows us to write the missing copy functions

class Vehicle
{
  public:
    // Copy functions - to support value semantics
    Vehicle(const Vehicle& other) : m_vehicle(other.m_vehicle->clone())
    {}
	
    Vehicle& operator = (const Vehicle& other)
    {
        m_vehicle = other.m_vehicle->clone();
	return *this;
    }
};

This is also known as Prototype pattern.
The complete code is available at following link

To summarize.

Pros

  • It outperforms the classical polymorphism in terms of
    • Tamed inheritance hierarchy, which is handled internally only at the single level (place)
    • Easily extendable (OCP), since the compiler will fabricate for each compatible type a new variant
    • Clear separation of concerns (SRP)
  • It outperforms the Dependency Injection as well in terms of performance, since introduces the value semantics
    and reduce the number of small allocations as result of injecting dependent object(s) Dependency Injection

Cons

  • It requires a lot of boilerplate code, by specifying the behavioral affordances through interface, that will be internally
    implemented by simple wrapping it around the parameterized type implementation, and non-virtual interface of the enclosing class for forwarding the calls to the private implementation (pimpl idiom).
  • It still employs virtual dispatching, and therefore is not such efficient as static polymorphism

References

Footnotes

  1. Thinking in Java 4th edition, Bruce Eckel

  2. Java generics, Oracle

  3. Type Erasure, Klaus Iglberger

  4. External Polymorphism, Chris Cleeland

  5. Type Erasure, Johnatan Müller

modern_cpp_tutorials's People

Contributors

damirlj avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.