QtPromise source code analysis CPP template meta programming

catalogue

Promise concept

QtPromise open source template library

Design patterns used in QtPromise template library

  one   Build patterns in QtPromise

  two   Decoration mode in QtPromise

Metaprogramming skills used in QtPromise template library

1. Template meta function

two   Full specialization & partial specialization to construct if then else

three   Type Traits

4. SFINAE

QtPromise source code analysis and implementation idea

1. UML sequence diagram of qtpromise

2. QtPromise source code analysis

The mixture of design patterns and meta programming

one   Metaprogramming and singleton pattern sampleCode

two   Metaprogramming and other modes

Promise concept

Promise is an asynchronous programming solution  

Promises is an alternative to callbacks used to pass asynchronous calculation results

QtPromise open source template library

If friends using Qt framework have requirements for asynchronous programming, it is recommended to use this template library to handle asynchronous operations.

github address:   GitHub - simonbrunel/qtpromise: Promises/A+ implementation for Qt/C++

Address of user manual:   Getting Started | QtPromise

In the following, the source code of QtPromise template library is analyzed and some CPP skills used are discussed.

This article will not teach you how to better use QPromise, but share the ideas and processes of its internal implementation.

Design patterns used in QtPromise template library

  one   Build patterns in QtPromise

  a.   In order to simplify initialization when constructing complex objects, we usually split multiple member properties into multiple set/with member methods to construct objects.

  b.   Similarly, in order to make the call of QPromise more in line with Promise style and achieve the purpose of chain call. QPromise class external interfaces are also variants based on this mode.

  QPromise parent class QPromiseBase source code example:

// Partial member function declaration from QPromise class
template<typename T>
class QPromiseBase {
    template<typename TFulfilled, typename TRejected>
    inline typename QtPromisePrivate::PromiseHandler<T, TFulfilled>::Promise
    then(const TFulfilled& fulfilled, const TRejected& rejected) const;

    template<typename TFulfilled>
    inline typename QtPromisePrivate::PromiseHandler<T, TFulfilled>::Promise
    then(TFulfilled&& fulfilled) const;

    template<typename TRejected>
    inline typename QtPromisePrivate::PromiseHandler<T, std::nullptr_t>::Promise
    fail(TRejected&& rejected) const;

    template<typename THandler>
    inline QPromise<T> finally(THandler handler) const;

    template<typename THandler>
    inline QPromise<T> tap(THandler handler) const;

    template<typename THandler>
    inline QPromise<T> tapFail(THandler handler) const;

    template<typename E = QPromiseTimeoutException>
    inline QPromise<T> timeout(int msec, E&& error = E{}) const;

    template<typename E = QPromiseTimeoutException>
    inline QPromise<T> timeout(std::chrono::milliseconds msec, E&& error = E{}) const;

    inline QPromise<T> delay(int msec) const;
    inline QPromise<T> delay(std::chrono::milliseconds msec) const;

    inline QPromise<T> wait() const;
};

We can see that most QPromiseBase functions return QPromiseBase instance objects, which conforms to the idea of construction mode, but there are some differences from construction mode. The setting method of construction mode acts on the same object and returns the same object. However, a new Promise instance object is returned each time in QPromiseBase.

  two   Decoration mode in QtPromise

a.   Business logic should be implemented in combination-- From microservice architecture design patterns

Benefits of combination mode:

  • Weaker coupling between types
  • The runtime is more flexible and can replace composite objects at any time according to business needs
  • More friendly to customer code

b. Decoration based on promise resolver

Let's first look at the source code of the PromiseResolver class

template<typename T>
class PromiseResolver
{
public:
    PromiseResolver(QtPromise::QPromise<T> promise) : m_d{new Data{}}
    {
        m_d->promise = new QtPromise::QPromise<T>{std::move(promise)};
    }

    template<typename E>
    void reject(E&& error)
    {
        auto promise = m_d->promise;
        if (promise) {
            Q_ASSERT(promise->isPending());
            promise->m_d->reject(std::forward<E>(error));
            promise->m_d->dispatch();
            release();
        }
    }

    void reject()
    {
        auto promise = m_d->promise;
        if (promise) {
            Q_ASSERT(promise->isPending());
            promise->m_d->reject(QtPromise::QPromiseUndefinedException{});
            promise->m_d->dispatch();
            release();
        }
    }

    template<typename V>
    void resolve(V&& value)
    {
        auto promise = m_d->promise;
        if (promise) {
            Q_ASSERT(promise->isPending());
            promise->m_d->resolve(std::forward<V>(value));
            promise->m_d->dispatch();
            release();
        }
    }

    void resolve()
    {
        auto promise = m_d->promise;
        if (promise) {
            Q_ASSERT(promise->isPending());
            promise->m_d->resolve();
            promise->m_d->dispatch();
            release();
        }
    }

private:
    struct Data : public QSharedData
    {
        QtPromise::QPromise<T>* promise = nullptr;
    };

    QExplicitlySharedDataPointer<Data> m_d;

    void release()
    {
        Q_ASSERT(m_d->promise);
        Q_ASSERT(!m_d->promise->isPending());
        delete m_d->promise;
        m_d->promise = nullptr;
    }
};

//

template<class T>
class QPromiseResolve
{
public:
    QPromiseResolve(QtPromisePrivate::PromiseResolver<T> resolver) : m_resolver{std::move(resolver)}
    {
        qDebug() << "QPromiseResolve";
    }


    ~QPromiseResolve() {
        qDebug() << "~QPromiseResolve";
    }

    template<typename V>
    void operator()(V&& value) const
    {
        m_resolver.resolve(std::forward<V>(value));
    }

    void operator()() const { m_resolver.resolve(); }

private:
    mutable QtPromisePrivate::PromiseResolver<T> m_resolver;
};

template<class T>
class QPromiseReject
{
public:
    QPromiseReject(QtPromisePrivate::PromiseResolver<T> resolver) : m_resolver{std::move(resolver)}
    { }

    template<typename E>
    void operator()(E&& error) const
    {
        m_resolver.reject(std::forward<E>(error));
    }

    void operator()() const { m_resolver.reject(); }

private:
    mutable QtPromisePrivate::PromiseResolver<T> m_resolver;
};

From the source code analysis, we know that the function of PromiseResolver is relatively simple and single. It holds a QPromise instance, provides methods for successful and failed operations to the customer code, and then forwards the external operation results to the held promise instance for internal processing.

QPromise continues to define qpromiseresolve & qpromisereject classes to handle success and failure separately in order to achieve a simpler and easier way (more in line with modern functional programming paradigm) and more in line with Promise style semantically. We can see that these three classes are not the standard decoration mode in textbooks, but in fact, the implementation of qpromiseresolve & qpromisereject class combines the PromiseResolver instance, overloads the call () operator (making it a callable object), and internally forwards the call to PromiseResolver, which basically conforms to the idea of decoration mode, but lacks the existence of base class interface, There is no unified calling interface, but the overload of () operator makes up for this defect.

Promiseresolver & qpromiseresolve & qpromisereject UML class diagram

 

From the usage of Promise, for the customer code, you only need to care about calling the operation for qpromiseresolve & qpromisereject when processing the result. Because the qpromiseresolve & qpromisereject class combines the PromiseResolver, it holds the QPromise object and exists in the whole QPromise execution chain. I refer to PromiseResolver & qpromiseresolve & qpromisereject as QPromise context class.

Metaprogramming skills used in QtPromise template library

As QtPromise is a lightweight template library, a large number of template meta technologies are used internally. Therefore, before analyzing the source code of QtPromise, we need to introduce the concept of CPP template meta skills used in QtPromise

1. Template meta function

    a. Characteristics & definitions of metafunctions

  • Metafunction is not a traditional language function. It is usually a struct or class
  • Usually returns one or more types
  • Metafunctions are not part of the language, and there is no formal language to support them
  • They exist as idiomatic usages of existing language functions
  • Their use is not enforced by the language

    b. Value meta function & type meta function

      Value metafunctions usually return a value      

template<class T>
class ValueMetaFunction {
    static constexpr int value = 100;
};

     We declare & define a metafunction named ValueMetaFunction, which returns a value of int

     The plus version of the value meta function

template<auto T>
class ValueMetaFunction {
    static constexpr auto value = 100;
};

template<class T, T value>
class ValueMetaFunction {
    static constexpr T value = value
};

  Type metafunctions usually return a type

template<class T>
class TypeMetaFunction {
    using type = T;
};

Declare & define a metafunction named TypeMetaFunction that returns a type of type T

c.   Call of meta function

For the above example code, let's now get the return value of the metafunction

ValueMetaFunction<int>::value
ValueMetaFunction<int, 100>::value
TypeMetaFunction<int>::type  --> typename TypeMetaFunction<int>::type

When we call the return value of meta function, the writing is generally long and the reading is not friendly. CPP provides the using keyword, which can be used to define a more convenient calling method.

Use the using keyword to define aliases for templates (metafunctions).

Usually, the value meta function uses_ Variable template at the end of v

template<typename T>
using value_v = ValueMetaFunction<T>::value

Typically, the type meta function uses_ Variable template at the end of t

template<typename T>
using type_t = typename ValueMetaFunction<T>::type

Convenient calling method

value_v<int>
type_t<int>

d. A useful meta function

// STRUCT TEMPLATE integral_constant
template <class _Ty, _Ty _Val>
struct integral_constant {
    static constexpr _Ty value = _Val;

    using value_type = _Ty;
    using type       = integral_constant;

    constexpr operator value_type() const noexcept {
        return value;
    }

    _NODISCARD constexpr value_type operator()() const noexcept {
        return value;
    }
};

// ---From MSVC xtr1commom file

integral_constant is used to judge the number of bits in the system

static const std::uint64_t default_buffer_size = 
std::conditional<sizeof(void *) == 8,  
                 std::integral_constant<std::uint64_t, 100 * 1024 * 1024>,   
                 std::integral_constant<std::uint64_t, 1024 * 1024 * 1024>
                >::type::value

Advantages of such coding:

  1. integral_constant, sizeof(void *) == 8 values are all calculated at compile time and do not occupy runtime overhead.
  2. default_buffer_size can be optimized into a compilation constant at compile time, and the control is allocated at compile time to improve the efficiency of program runtime.

two   Full specialization & partial specialization to construct if then else

The full specialization and partial specialization of the template construct the condition judgment in metaprogramming

Ex amp le of partial specialization & full specialization of template:

// Standard template
template<typename T, typename U>
struct Sample {};

// For T is the partial specialization of the pointer
template<typename T, typename U>
struct Sample<T*, U> {};

// For partial specialization where T is int
template<typename U>
struct Sample<int, U> {};

// For full specialization with T & U of type int 
template<>
struct Sample<int, int> {};

Declare if then else:

template< bool CONDITION, class THEN, class ELSE > struct IF {};

template<class THEN, class ELSE> struct IF< false, THEN, ELSE > {typedef ELSE TEST;};

template<class THEN, class ELSE> struct IF< true, THEN, ELSE > {typedef THEN TEST;};

if-then-else SampleCode:

template< bool Condition >
class IF {
public:
    static inline void EXEC(){std::cout << "Statement is true";}
};

// Fully specialized IF template. When the template parameter is false, the version is matched
class IF< false > {
public:
    static inline void EXEC(){std::cout << "Statement is false";}
};

std::cout << IF<sizeof(void*) == 8>::EXEC()<< std::endl;

three   Type Traits

a. What is type traits?

  • It is one of the pillars of C + + generic programming
  • Check and convert the properties of the type
  • It is usually a simple template structure

b. Type in MSVC_ A large number of common type feature templates are defined in the traits file

  The following figure is taken from <type_traits> - C++ Reference part

 

  You can click the link to view the specific instructions and usage. The above types of feature templates are often used in common template design programs.

c. call_traits

Example

// CLASS TEMPLATE binder1st
template <class _Fn>
class binder1st : public unary_function<typename _Fn::second_argument_type,
                      typename _Fn::result_type> { // functor adapter _Func(stored, right)
public:
    using _Base         = unary_function<typename _Fn::second_argument_type, typename _Fn::result_type>;
    using argument_type = typename _Base::argument_type;
    using result_type   = typename _Base::result_type;

    binder1st(const _Fn& _Func, const typename _Fn::first_argument_type& _Left) : op(_Func), value(_Left) {}

    // @1
    result_type operator()(const argument_type& _Right) const {
        return op(value, _Right);
    }

    result_type operator()(argument_type& _Right) const {
        return op(value, _Right);
    }

protected:
    _Fn op;
    typename _Fn::first_argument_type value; // the left operand
};


// --From MSVC functional header file

The binder1st structure implemented by MSVC exists in the code segment @ 1   argument_ If type is a reference type, the binder1st overload () calling operator has a reference problem. This is not allowed in C + +

call_traits tool  -- From boost library

call_ The purpose of traits is to ensure that problems like "reference" never happen and that parameters are passed in the most efficient way.

template <typename T, bool small_>
struct ct_imp2 {
    typedef const T& param_type;
};

template <typename T>
struct ct_imp2<T, true> {
    typedef const T param_type;
};

template <typename T, bool isp, bool b1, bool b2>
struct ct_imp {
    typedef const T& param_type;
};

template <typename T, bool isp, bool b2>
struct ct_imp<T, isp, true, b2> {
    typedef typename ct_imp2<T, sizeof(T) <= sizeof(void*)>::param_type param_type;
};

template <typename T, bool isp, bool b1>
struct ct_imp<T, isp, b1, true> {
    typedef typename ct_imp2<T, sizeof(T) <= sizeof(void*)>::param_type param_type;
};

template <typename T, bool b1, bool b2>
struct ct_imp<T, true, b1, b2> {
    typedef const T param_type;
};


template <typename T>
struct call_traits
{
public:
    typedef T value_type;
    typedef T& reference;
    typedef const T& const_reference;
    typedef typename ct_imp<
        T,
        ::boost::is_pointer<T>::value,
        ::boost::is_arithmetic<T>::value,
        ::boost::is_enum<T>::value
    >::param_type param_type;
};

template <typename T>
struct call_traits<T&>
{
    typedef T& value_type;
    typedef T& reference;
    typedef const T& const_reference;
    typedef T& param_type;  // hh removed const
};
template <typename T>
struct call_traits<T& const>
{
    typedef T& value_type;
    typedef T& reference;
    typedef const T& const_reference;
    typedef T& param_type;  // hh removed const
};


// From boost library call_traits.hpp section

binder1st's overloading of the calling operator can eventually be modified to

result_type operator()(typename call_traits<argument_type>::param_type _Right) const {
    return op(value, _Right);
}

b. Application of type properties in qtpromise

The argtraits meta function is defined in QtPromise to extract the meta information of the formal parameter list of the function.

template<typename... Args>
struct ArgsTraits
{
    using types = std::tuple<Args...>;
    // Returns the type of the first formal parameter in the function parameter list
    using first = typename std::tuple_element<0, types>::type;
    // Returns the number of formal parameters in the function parameter list
    static const size_t count = std::tuple_size<types>::value;
};

// The full specialization parameter list is empty
template<>
struct ArgsTraits<>
{
    using types = std::tuple<>;
    using first = void;
    static const size_t count = 0;
};


// --From qpromiseglobal.h header file

    Define the ArgsOf metafunction of the extended class of ArgsTraits. According to the declaration of ArgsTraits, the ArgsTraits metafunction receives a type parameter package. The extension class ArgsOf is used to collect function list type information.

// Standard ArgsOf type declaration definition
template<typename T, typename Enabled = void>  struct ArgsOf : public ArgsTraits<> { };

// Yes, nullptr_ Partial specialization of T
template<> struct ArgsOf<std::nullptr_t> : public ArgsTraits<>{ };

// Partial specialization of operator() member function pointers
template<typename T> 
struct ArgsOf<T, typename std::enable_if<HasCallOperator<T>::value>::type>: public ArgsOf<decltype(&T::operator())> { };

// Partial specialization of reference types
template<typename T> struct ArgsOf<T&> : public ArgsOf<T> { };
// Partial specialization of right value types
template<typename T> struct ArgsOf<T&&> : public ArgsOf<T> { };

// Partial specialization of callable types
template<typename R, typename... Args> struct ArgsOf<R(Args...)> : public ArgsTraits<Args...> { };

// Partial specialization of function pointers
template<typename R, typename... Args> struct ArgsOf<R (*)(Args...)> : public ArgsTraits<Args...> { };

// Partial specialization of member function pointers
template<typename R, typename T, typename... Args> struct ArgsOf<R (T::*)(Args...)> : public ArgsTraits<Args...> { };

// Partial specialization of const's member function pointer @7
template<typename R, typename T, typename... Args> struct ArgsOf<R (T::*)(Args...) const> : public ArgsTraits<Args...> { };

// Pointer specialization of volatile member functions @8
template<typename R, typename T, typename... Args> struct ArgsOf<R (T::*)(Args...) volatile> : public ArgsTraits<Args...> { };

// Member function pointer with partial specialization type T const volatile @9
template<typename R, typename T, typename... Args> struct ArgsOf<R(T::*)(Args...) const volatile> : public ArgsTraits<Args...> { };

// --From qpromiseglobal.h header file

We can see that ArgsOf makes partial specialization for most function pointer types, so as to extract the parameter package of a specific function parameter list and pass it to its parent class. Complete the extraction of function parameter list information.

In fact, we found that ArgsOf is used to extract the formal parameter list type of the function. As for whether the member function of the class is   Const, volatile, const volatile and other types are not concerned. However, in order to meet the extraction of all types of member functions of a class, the ArgsOf versions of all member function types must be partially specialized as far as possible. See code segments @7& 8 & 9. Careful readers may find that ArgsOf's meta information extraction of class member function parameter list types is incomplete, because ArgsOf does not specialize all member function types. For example, const &, const & &, const volatile &, const volatile & &, volatile &, volatile & & member function types are not partial specialization. For member function types of this type, the compiler will match the standard ArgsOf version when instantiating ArgsOf, so the function parameter list will be treated as empty.

If your project has the non partial specialization member function mentioned above, there will be an error when using QPromise. How can we optimize the following ArgsOf?

In project development, we can provide a basic large and comprehensive type_traits, used to extract raw function types. Whether you use the QtPromise template library or not,   At least it can also exist as a basic class in other requirements of the project.

Provide an is_const_or_volatile_member_function member function type feature tool class. The declaration is defined as follows:

template <typename T>
struct is_const_or_volatile_member_function : std::false_type { };

template <typename R, typename T, typename... Args>
struct is_const_or_volatile_member_function<R(T::*)(Args...) const> : std::true_type{
    using type = R(T::*)(Args...);
};
template <typename R, typename T,  typename... Args>
struct is_const_or_volatile_member_function<R(T::*)(Args...) const&> : std::true_type {
    using type = R(T::*)(Args...);
};
template <typename R, typename T, typename... Args>
struct is_const_or_volatile_member_function<R(T::*)(Args...) const&&> : std::true_type {
    using type = R(T::*)(Args...);
};
template <typename R, typename T, typename... Args>
struct is_const_or_volatile_member_function<R(T::*)(Args...) const volatile> : std::true_type {
    using type = R(T::*)(Args...);
};
template <typename R, typename T, typename... Args>
struct is_const_or_volatile_member_function<R(T::*)(Args...) const volatile&> : std::true_type {
    using type = R(T::*)(Args...);
};
template <typename R, typename T, typename... Args>
struct is_const_or_volatile_member_function<R(T::*)(Args...) const volatile&&> : std::true_type {
    using type = R(T::*)(Args...);
};
template <typename R, typename T, typename... Args>
struct is_const_or_volatile_member_function<R(T::*)(Args...) volatile> : std::true_type {
    using type = R(T::*)(Args...);
};
template <typename R, typename T, typename... Args>
struct is_const_or_volatile_member_function<R(T::*)(Args...) volatile&> : std::true_type {
    using type = R(T::*)(Args...);
};
template <typename R, typename T, typename... Args>
struct is_const_or_volatile_member_function<R(T::*)(Args...) volatile&&> : std::true_type {
    using type = R(T::*)(Args...);
};

is_const_or_volatile_member_function is partial specialization of all types of member functions, and inherits from std::false_type or std::true_type is used to give a bool value to judge whether the type passed in is a member function type. Finally, a bare member function pointer type is returned (here, bare member functions are non const, const &, const & &, const volatile, const volatile &, const volatile & &, volatile, volatile &, volatile & & types)

is_const_or_volatile_member_function use:

Here, the code segments @ 7, 8 and 9 defined by ArgsOf can be uniformly modified to the code segment @ 2 in the following figure. This can be done by is_ const_ or_ volatile_ member_ The function meta function extracts all non bare member function types, which also saves the number of partial specialization versions of ArgsOf.

// @1
// Partial specialization of pointer member function types
template<typename R, typename T, typename... Args>
struct ArgsOf<R(T::*)(Args...)> : public ArgsTraits<Args...>
{
    static constexpr char* value = "R(T::*)(Args...)";
};

// @2
// For const, const &, const & &, const volatile, const volatile &
// Const volatile & &, volatile, volatile &, volatile & & pointer specialization of member functions
template<typename T>
struct ArgsOf <T, std::enable_if_t<is_const_or_volatile_member_function<T>::value> >: 
    public ArgsOf<typename is_const_or_volatile_member_function<T>::type>
{
    static constexpr char* value = "R(*)(Args...) const or volatile";
};

  Here we will verify the use of optimized ArgsOf

use sample code: 

qDebug() << ArgsOf<int(TestClass::*)(int, int)>::value;
qDebug() << ArgsOf<int(TestClass::*)(int, int) const>::value;
qDebug() << ArgsOf<int(TestClass::*)(int, int) const&>::value;
qDebug() << ArgsOf<int(TestClass::*)(int, int) volatile>::value;
qDebug() << ArgsOf<int(TestClass::*)(int, int) volatile&>::value;
qDebug() << ArgsOf<int(TestClass::*)(int, int) const volatile&>::value;

  Output:

R(T::*)(Args...)
R(*)(Args...) const or volatile
R(*)(Args...) const or volatile
R(*)(Args...) const or volatile
R(*)(Args...) const or volatile
R(*)(Args...) const or volatile

  From the output, we can analyze that the pointer type of the non bare member function matches the ArgsOf partial specialization version of the above code segment @ 2, and the bare member function type matches the ArgsOf partial specialization version of the code segment @ 1. So far, we have completed the optimization of the insufficient member function types of the partial specialization class of ArgsOf in the QtPromise template library.

4. SFINAE

a.   What is SFINAE?

  • Substitution failure is not an error
  • If you overload a function and call it, as long as one overload is valid. Other overloads can fail by default and select the best match in all overloaded functions.
  • It is very useful for overloading multiple functions

b.   What can SFINAE do?

  • Check whether a class contains the member XXMember
  • Check whether the function XXFunction is defined in a structure
  • What can a type do
  • Compile time Boolean check / conditional compilation  

  c. Application of sfinae in QtPromise

  Check whether a function member function exists in a class

template<typename T>
struct HasCallOperator 
{
    template<typename U>
    static char check(decltype(&U::operator(), char(0)));

    template<typename U>
    static char (&check(...))[2];
    static const bool value = (sizeof(check<T>(0)) == 1);
};

// --From qpromiseglobal.h file

  QPromiseBase constructor overload

template<typename F, typename std::enable_if<QtPromisePrivate::ArgsOf<F>::count == 1, int>::type = 0>
inline QPromiseBase(F resolver);
template<typename F,typename std::enable_if<QtPromisePrivate::ArgsOf<F>::count != 1, int>::type = 0>
inline QPromiseBase(F resolver);

The compiler generates the best constructor according to the number of formal parameter lists in the argument type.  

QtPromise source code analysis and implementation idea

1. UML sequence diagram of qtpromise

 

 

Next, I will analyze the main flow of QtPromise source code in combination with the time sequence diagram

2. QtPromise source code analysis

a.   Construction & life cycle of QPromise

Let's start with a basic QPromise Sample Code:

{
    QPromise<std::string> promise = QPromise<int>([](QPromiseResolve<int> resolve, QPromiseReject<int> reject) {
        // @1
        resolve(100)
    }).then([](int) -> std::string {
        return "100";
    });
}

  The above code segment constructs a basic promise chain call and returns the last QPromise instance of the chain. I wonder if you have such a question when using QPromise, that is, after the execution of the above code segment is completed, the promise instances are destroyed. How does it ensure that there is a then method function body under asynchronous execution??? As we analyze the structure of QPromise objects, we will slowly solve this problem.

QPromise constructor analysis

template<typename T>
class QPromiseBase
{
public:
    using Type = T;

    // @1
    template<typename F,
             typename std::enable_if<QtPromisePrivate::ArgsOf<F>::count == 1, int>::type = 0>
    inline QPromiseBase(F resolver) :  m_d{new QtPromisePrivate::PromiseData<T>{}} 
    {
        // @6
        QtPromisePrivate::PromiseResolver<T> resolver{*this};

        try {
            // @4
            callback(QPromiseResolve<T>(resolver));
        } catch (...) {
            resolver.reject(std::current_exception());
        }
    }

    // @2
    template<typename F,
             typename std::enable_if<QtPromisePrivate::ArgsOf<F>::count != 1, int>::type = 0>
    inline QPromiseBase(F resolver) : m_d{new QtPromisePrivate::PromiseData<T>{}} 
    {
        // @7
        QtPromisePrivate::PromiseResolver<T> resolver{*this};

        try {
            // @5
            callback(QPromiseResolve<T>(resolver), QPromiseReject<T>(resolver));
        } catch (...) {
            resolver.reject(std::current_exception());
        }
    }
};

template<typename T>
class QPromise : public QPromiseBase<T>
{
public:
    // @3
    template<typename F>
    QPromise(F&& resolver) : QPromiseBase<T>(std::forward<F>(resolver)) {
    }
};

  1. QPromise constructor receives an arbitrary argument (the parameter type is universal reference type), but it is usually a callable object (function pointer, member function, Lambda expression, class overloaded with () operator, std::function). It is not difficult to find this rule from the code segment @4&5 in the figure above, and the number of formal parameters of the calling object must be 1 or 2, When it is 1, the parameter type must be QPromiseResolve, and when it is 2, the parameter type must be QPromiseResolve & qpromisereject.
  2. QPromise forwards the universal reference formal parameter resolver as an argument to the constructor of the parent class QPromiseBase. The above code segment @3 forwards the universal reference type with std::forward (refer to effective modern C + +).
  3. QPromiseBase has two overloaded versions of template function in code segment @1&2 (refer to sfina above). Which version is finally selected, the parameter list quantity of type F is extracted according to the ArgsOf type attribute template described above. If it is 1, code segment @ 1 is matched, otherwise code segment @ 2 is matched.  
  4. QPromiseBase function body implementation: code snippet @6&7   Construct a local object of the context class PromiseResolver and hand over its control to the PromiseResolver object. Construct QPromiseResolve or QPromiseResolve based on the PromiseResolver object   QPromiseResolve & qpromisereject (determined by the number of formal parameters of arguments (callable types) when constructing QPromise objects).
  5. During the construction of QPromise, the construction arguments will be called back immediately, and then enter the function stack of the customer code. When the client code calls parameter 1 or 2, it is equivalent to submitting the operation result to the QPromise context class.
  6. Here we only discuss the success of the operation. Other processes are basically the same. We comb the process in combination with qpromise sample code (the example at the beginning of this section)  
  7.   When the customer code notifies the QPromiseResolve object of the operation result 100, resolve forwards the result, and then calls the resolve function of the decoration object QPromiseResolver. Personally, I don'T think this method needs to be a template method, and the formal parameter type is T.
        template<typename V>
        void resolve(V&& value)
        {
            auto promise = m_d->promise;
            if (promise) {
                Q_ASSERT(promise->isPending());
                promise->m_d->resolve(std::forward<V>(value));
                promise->m_d->dispatch();
                release();
            }
        }

  8. promise->m_d->resolve(std::forward<V>(value)); Store the operation result of the customer code into the member attribute m_d
  9.   promise->m_ d->dispatch();   Distribute the current results asynchronously to the next QPromise object to be processed
  10. release(); Release the QPromise heap object held by the current context, so when calling back the user code in the QPromise constructor, the user code should call resolve or reject to avoid memory leakage.

  I believe we know how QtPromise ensures its life cycle.

  • Thanks to the encapsulation of QPromise by the PromiseResolver context, its copy has been saved in the context class, and the length of its life cycle depends on the customer code's calls to resolve and reject.
  • Ensuring the execution of subsequent methods depends on the Qt event runloop mechanism. Event encapsulates callable objects (lambda internally) and implements asynchronous chain calls based on asynchronous events.
  • Please ensure that the callable objects of resolve and reject must be executed, otherwise there will be memory leakage.

QPromise then core function analysis

then provides a function entry to receive the next step (asynchronous) processing (hereinafter referred to as the nextPromise object)

then function body source code

template<typename T>
template<typename TFulfilled, typename TRejected>
inline typename QtPromisePrivate::PromiseHandler<T, TFulfilled>::Promise
QPromiseBase<T>::then(const TFulfilled& fulfilled, const TRejected& rejected) const
{
    using namespace QtPromisePrivate;
    // @1
    using PromiseType = typename PromiseHandler<T, TFulfilled>::Promise;
    // @2
    PromiseType next([&](const QPromiseResolve<typename PromiseType::Type>& resolve,
                         const QPromiseReject<typename PromiseType::Type>& reject) {
        // @3
        m_d->addHandler(PromiseHandler<T, TFulfilled>::create(fulfilled, resolve, reject));
        m_d->addCatcher(PromiseCatcher<T, TRejected>::create(rejected, resolve, reject));
    });

    if (!m_d->isPending()) {
        m_d->dispatch();
    }

    return next;
}

template<typename T>
template<typename TFulfilled>
inline typename QtPromisePrivate::PromiseHandler<T, TFulfilled>::Promise
QPromiseBase<T>::then(TFulfilled&& fulfilled) const
{
    return then(std::forward<TFulfilled>(fulfilled), nullptr);
}
  1.   then function parameters usually receive a callable object
  2. Code segment @1 extracts the template type and return type of Promise to be returned through the type feature of Promise handler   , Refer to the QPromise Sample Code example. The PromiseType is qpromise < STD:: String >.
  3. Construct the local object QPromise and immediately execute the argument Lambda expression.
  4. Create a callable object (std::function) through PromiseHandler, and save the formal parameters of the current then and the context under nextPromise in the callable object
    // PromiseHandler Standard Edition
    template<typename T, typename THandler, typename TArg = typename ArgsOf<THandler>::first>
    struct PromiseHandler
    {
        using ResType = typename invoke_result<THandler, T>::type;
        using Promise = typename PromiseDeduce<ResType>::Type;
    
        template<typename TResolve, typename TReject>
        static std::function<void(const T&)>
        create(const THandler& handler, const TResolve& resolve, const TReject& reject)
        {
            return [=](const T& value) {
                PromiseDispatch<ResType>::call(resolve, reject, handler, value);
            };
        }
    };
  5. Save the callable object built by PromiseHandler in the current member property QPromiseData

  6. Return the Promise object to continue to receive the next handler in a chained manner

c. QPromiseData structure & Promise chained memory model

The QPromise class inherits a unique data property member m from its parent class PromiseBase_ d.

QExplicitlySharedDataPointer<QtPromisePrivate::PromiseData<T>> m_d;

QPromiseData structure. The source code in the figure below only gives Promise member attributes,

template<typename T, typename F>
class PromiseDataBase : public QSharedData {
public:
    using Handler = std::pair<QPointer<QThread>, std::function<F>>;
    using Catcher = std::pair<QPointer<QThread>, std::function<void(const PromiseError&)>>;
protected:
    mutable QReadWriteLock m_lock;
    
private:
    bool m_settled = false;
    // After the asynchronous event is completed, the next step is to call the callable object collection (and the encapsulation of nextPromise) 
    QVector<Handler> m_handlers;
    // Error handling
    QVector<Catcher> m_catchers;
    PromiseError m_error;

};


template<typename T>
class PromiseData : public PromiseDataBase<T, void(const T&)> {
    using Handler = typename PromiseDataBase<T, void(const T&)>::Handler;
private:
    
    // The value currently processed (returned) by Promise is handed over to Promise value to automatically manage its life cycle
    PromiseValue<T> m_value;
};

// Fully specialized QPromise returns a case with a value type of void
template<>
class PromiseData<void> : public PromiseDataBase<void, void()>
{
    using Handler = PromiseDataBase<void, void()>::Handler;
}


// Use RAII method to manage the life cycle of data T
template<typename T>
class PromiseValue
{
public:
    PromiseValue() { }
    PromiseValue(const T& data) : m_data(QSharedPointer<T>::create(data)) { }
    PromiseValue(T&& data) : m_data(QSharedPointer<T>::create(std::forward<T>(data))) { }
    bool isNull() const { return m_data.isNull(); }
    const T& data() const { return *m_data; }

private:
    QSharedPointer<T> m_data;
};

From the structure of QPromiseData, the attribute member of Promise class, we can give its memory model  

 

Qpromiseresolve & qpromisereject is the decoration object of PromiseResolver.

Promise resolver is a context that carries QPromise objects, that is, the member attributes resolve and reject decoration objects in the handler and catcher label objects point to the QPromiseData object respectively, forming the QPromise chained memory mode.  

d.   Role of QPromise context class

  • Encapsulates the QtPromise chained header object, focusing on the first result.
  • Isolate the association between the customer code and QPromise to make the customer code call easier. You only need to care about the result.
  • The QPromise object life cycle is transferred, and the QPromise life cycle is managed by the customer code.
  • Simplify customer business code, separate success and failure logic, and rely on Qt display sharing technology.

e.   Implement Promise non blocking call with Qt message loop

template<typename F>
static void qtpromise_defer(F&& f, const QPointer<QThread>& thread)
{
    using FType = typename std::decay<F>::type;

    struct Event : public QEvent
    {
        Event(FType&& f) : QEvent{QEvent::None}, m_f{std::move(f)} { }
        Event(const FType& f) : QEvent{QEvent::None}, m_f{f} { }
        ~Event() override { m_f(); }
        FType m_f;
    };

    if (!thread || thread->isFinished()) {
        // Make sure to not call `f` if the captured thread doesn't exist anymore,
        // which would potentially result in dispatching to the wrong thread (ie.
        // nullptr == current thread). Since the target thread is gone, it should
        // be safe to simply skip that notification.
        return;
    }

    QObject* target = QAbstractEventDispatcher::instance(thread);
    if (!target && QCoreApplication::closingDown()) {
        // When the app is shutting down, the even loop is not anymore available
        // so we don't have any way to dispatch `f`. This case can happen when a
        // promise is resolved after the app is requested to close, in which case
        // we should not trigger any error and skip that notification.
        return;
    }

    Q_ASSERT_X(target, "postMetaCall", "Target thread must have an event loop");
    QCoreApplication::postEvent(target, new Event{std::forward<F>(f)});
}

Promise calls the final function body asynchronously and relies on Qt asynchronous message mechanism. The next callable object to be processed (Lambda expression) encapsulates the context object of nextPromise.

F: HandlerCallable

Event: Custom QEvent carries Callable and asynchronously calls nextPromise based on message loop

Thread: ensure thread singleness

 

The mixture of design patterns and meta programming

For reading and writing friends who like meta programming, it is recommended to read the source code of Loki library, which involves in-depth skills of meta programming. Loki mainly uses template meta programming to implement general design patterns. Let's see what kind of sparks will collide when template meta programming and design patterns are combined together. (#^.^#)

one   Metaprogramming and singleton pattern sampleCode

Definition of policy structure

Object to create a policy structure

template <class T> struct CreateUsingNew {
    static T* Create() {
        return new T;
    }
    static void Destroy(T* p) {
        delete p;
    }
};

Object lifecycle policy structure

template <class T>
struct DefaultLifetime {
    static void ScheduleDestruction(T*, atexit_pfn_t pFun) {
        std::atexit(pFun);
    }
    static void OnDeadReference() {
        throw std::logic_error("Dead Reference Detected");
    }
};

Definition of singleton construction template class

  The template singleton class receives two default creation objects and lifecycle management types. When instantiated, the user code can re extend the policy type.

  The above content is extracted from the Loki library. For a simple example, it is necessary to simplify the template class constructed by the singleton pattern.

two   Metaprogramming and other modes

    The Loki template Library also implements the following mixing of template elements and design patterns

  •   Abstract factory pattern
  • Factory mode
  • Visitor mode

For specific explanations, you can read the Book modern C++ programming, which is written based on Loki library.

ending

1. Read the excellent framework source code for better use and modification.

2. Learn excellent coding ideas to make yourself grow.

Tags: C++ Qt source code

Posted on Tue, 28 Sep 2021 15:11:21 -0400 by trp