C++/WinRT Runtime Component in Win32 Applications

Part 2



Templates and variadic templates.

Abbiamo visto com'è strutturata (in partenza) un'interfaccia ed una runtime class in C++/WinRT, ed a questo proposito ho mostrato un esempio di CRTP.

Prima di implementare le interfacce e le classi (WinRT style) nella nostra app è bene riassumere brevemente, magari con degli esempi, qualche concetto C++, nello specifico:

Nota
In tutti gli esempi troverete come primo include il file Windows.h per il semplice fatto che, dato che sto usando un compilatore Microsoft, includendo quel file viene definito (tra le altre cose) il tipo interface.
Ciò non è strettamente necessario, infatti gli esempi sono scritti in C++ standard 17 e possono essere compilati con un qualunque compilatore C++ 17, sotto Windows, Linux etc...
Pertanto se state usando un compilatore non Microsoft e la keyword interface non è definita, aggiungete la seguente direttiva:

#include <basetyps.h>



Per iniziare, perchè mai dovrebbero essere usate queste tecniche? In quali casi? Qual'è lo scopo?
La risposta a queste domande è legata ad una delle pietre miliari del linguaggio C++ ( e di altri linguaggi OOP): Inheritance (ereditarietà).
Come ben sappiamo, con l'inheritance, è possibile estendere le funzionalità degli oggetti, sovrascrivendo le esistenti (override) od aggiungendone di nuove; in altre parole a partire da un un oggetto (classe) base, deriveremo uno o più oggetti (classi); i nuovi oggetti a loro volta potranno essere classi base per altri ulteriori oggetti che derivano da essi e così via.
Questo tipo di relazione tra oggetti è definita Is-a, ovvero una classe che discende da un'altra è sia un oggetto del tipo classe derivata che base (e questo per tutta la catena).

Ora, sebbene l'ereditarietà sia utile in molti frangenti, spesso introduce dei grossi problemi, in taluni casi la multiple inheritance innesca ambiguità che possono rendere impossibile la compilazione.

Un esempio tipico è il Dreaded Diamond Problem (o diamond problem) che introduce ambiguità quando una classe eredita da altre due classi le quali a loro volta hanno una classe base in comune.

Dreaded Diamond Problem


Per ovviare al problema, in tali casi, (oltre che rivedere seriamente la struttura dei nostri progetti), sarà necessario usare il risolutore del campo d'azione ::, o ereditare le classi base come virtual.


//Diamond problem, sample and fix

#pragma once
#include <Windows.h>
#include <iostream>

    class Base
    {
    public:

    Base() = default;
    virtual ~Base() = default;

    void Print() { std::wcout << L"Print base" << std::endl; }
    int GetVal() { return m_val; }

    private:
    int m_val = 100;
    };

    //Decomment virtual to fix the problem    
    class Derived_1 : /*virtual*/ public Base
    {
    public:
    Derived_1() = default;
    virtual ~Derived_1() = default;

    void Print() { std::wcout << L"Print Derived_1" << std::endl; }
    };

    
    //Decomment virtual to fix the problem
    class Derived_2 : /*virtual*/ public Base
    {

    public:
    Derived_2() = default;
    virtual ~Derived_2() = default;

    void Print() { std::wcout << L"Print Derived_2" << std::endl; }

    };


    class Derived_3 : public Derived_1, public Derived_2
    {
    public:
    Derived_3() = default;
    virtual ~Derived_3() = default;

    void Print() { std::wcout << L"Print Derived_3" << std::endl; }

    };


    //====================================

    // Main

    int Main()
    {
    
    //Ambiguous cast and access sample. 
    //Compilation fails if Base is not declared as virtual
    //in Derived_1 and Derived_2
    
    std::wcout << L" = = = = = = = = = = = = = = " << std::endl;
    std::wcout << L"Ambiguous access sample:" << std::endl;

    auto pDv = std::make_unique<Derived_3>();
   
    Base* pBase = dynamic_cast<Base *>(pDv.get()); //<-- Not compile or warning
    pBase->Print();
    int val = pBase->GetVal();


    Derived_1* pDv1 = dynamic_cast<Derived_1*>(pDv.get());
    pDv1->Print();
    val = pDv1->GetVal();

    Derived_2* pDv2 = dynamic_cast<Derived_2*>(pDv.get());
    pDv2->Print();
    val = pDv2->GetVal();
    

    pDv->Print();
    val = pDv->GetVal(); //<-- Not compile
       
    return 0;
    }
                               
// = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
//Compiler Output:
//warning C4540: dynamic_cast used to convert to inaccessible or ambiguous base; run-time test will fail ('Derived_3 *' to 'Base *')                              
//error C2385: ambiguous access of 'GetVal'
//message : could be the 'GetVal' in base 'Base'

Decommentando la keyword virtual nelle definizioni di Derived_1 e Derived_2 il problema sarà risolto, in quanto verrà implementata una sola copia di Base.

//Output:

 = = = = = = = = = = = = = =
Ambiguous access sample:
Print base
Print Derived_1
Print Derived_2
Print Derived_3
    


Come abbiamo appena visto quindi la multiple inheritance è potenzialmente pericolosa. Ma allora, per ridurre gli effetti collaterali dell'ereditarietà multipla, come possiamo fare? Beh, abbiamo a disposizione varie tecniche:

Nota
Composition
Con composition si intende la creazione di oggetti complessi composti da uno o più oggetti più semplici, il classico esempio di un'automobile che viene considerata un oggetto unico, mentre in realtà è composta da vari oggetti: il motore, il volante, le ruote, le viti etc..
La relazione nel caso di composition è di tipo Has-a.
L'oggetto complesso che contiene tutti gli altri dovrà gestire gli oggetti più semplici, in altre parole gli oggetti contenuti non possono esistere se non in funzione dell'oggetto che li contiene.

Aggregation
Con aggregation si intende sempre la creazione di oggetti complessi composti da uno o più oggetti più semplici, ma in questo caso l'oggetto complesso non gestisce gli oggetti aggregati che invece vivono indipendentemente dall'oggetto che li contiene.
Anche in questo caso la relazione tra oggetti è di tipo Has-a.

CRTP
CRTP si usa allo scopo di aggiungere generiche funzionalità ad una classe.
Abbiamo già visto un'introduzione a CRTP nella parte 1.

Mixin
Anche Mixin viene usato allo scopo di aggiungere generiche funzionalità ad una classe.
Una classe Mixin però deriva dal tipo di oggetto a cui noi vogliamo che le funzionalità della classe mixin vadano aggiunte

CRTP e Mixin, spesso, vengono utilizzati sottoforma di variadic templates.

Nota
Variadic template è una classe od una funzione template che accetta un arbitrario numero di argomenti.
Abbiamo visto un'introduzione a variadic templates nella prima parte di questo tutorial a proposito delle runtime classes WinRT implementate (per noi) da Visual Studio 2019.


Nell' esempio CRTP vs Mixin qui di seguito vedremo come alla fine, in output, il risultato finale sarà identico sia seguendo il pattern CRTP che Mixin. Teniamo presente che lo scopo, in entrambi i casi, è quello di estendere una data classe aggiungendo delle nuove funzionalità, cercando di evitare le trappole dell'ereditarietà multipla.
Doteremo di una funzione ReversePrint() una classe che già possiede una sua funzione di stampa PrintData().
La classe Reverse in cui il metodo ReversePrint() è implementato è una classe template che aderisce all'idioma CRTP nel namespace UseCRTP ed all'idioma Mixin nel namespace UseMixin.

A seguire vedremo più in dettaglio le differenze tra i due patterns.


CRTP vs Mixin sample

#pragma once
#include <Windows.h>
#include <iostream>

//CRTP
namespace UseCRTP
{
  template <typename T>
  class Reverse 
  {
    public:		
		
    Reverse() = default;
    virtual ~Reverse() = default;
	
    void ReversePrint() 
    {			

    static_cast<T&>(*this).PrintData();
		
    std::wstring fName = static_cast<T&>(*this).GetFirstName();
    std::wstring lName = static_cast<T&>(*this).GetLastName();
    std::wstring alias = static_cast<T&>(*this).GetAlias();

			
    std::wcout << L"Reverse Print (CRTP):" 
	       << std::endl
		   << alias << L" :"
		   << lName << L", " 
		   << fName  
		   << std::endl << std::endl;
    }
		
  };
	


class SuperHero :public Reverse<SuperHero>
{
    public:

    SuperHero() = default;
    virtual ~SuperHero() = default;

    SuperHero(std::wstring fName, std::wstring lName, std::wstring alias)
	{
		m_FirstName = fName;
		m_LastName = lName;
		m_Alias = alias;
	}

	void PrintData() { std::wcout <<L"Regular Print:" << std::endl
			                      << m_FirstName  << L", " 
			                      << m_LastName   << L" :" 
			                      << m_Alias      << std::endl
			                      << std::endl; }
		
	std::wstring GetFirstName() { return m_FirstName;}
	std::wstring GetLastName()  { return m_LastName;}
	std::wstring GetAlias()     { return m_Alias;}
    
        void SetFirstName(std::wstring const& fName) {  m_FirstName = fName; }
	void SetLastName(std::wstring const& lName)  { m_LastName = lName; }
	void SetAlias(std::wstring const& alias)     { m_Alias = alias; }

	private:
		std::wstring m_FirstName = L"";
		std::wstring m_LastName  = L"";
		std::wstring m_Alias     = L"";
	};

}




//========================================



namespace UseMixin
{

class SuperHero
{
    public:

    SuperHero() = default;
    virtual ~SuperHero() = default;

    SuperHero(std::wstring fName, std::wstring lName, std::wstring alias)
	{
		m_FirstName = fName;
		m_LastName = lName;
		m_Alias = alias;
	}

	void PrintData() { std::wcout <<L"Regular Print:" << std::endl
			                      << m_FirstName  << L", " 
			                      << m_LastName   << L" :" 
			                      << m_Alias      << std::endl
			                      << std::endl; }
		
	std::wstring GetFirstName() { return m_FirstName;}
	std::wstring GetLastName()  { return m_LastName;}
	std::wstring GetAlias()     { return m_Alias;}
    
        void SetFirstName(std::wstring const& fName) {  m_FirstName = fName; }
	void SetLastName(std::wstring const& lName)  { m_LastName = lName; }
	void SetAlias(std::wstring const& alias)     { m_Alias = alias; }

	private:
		std::wstring m_FirstName = L"";
		std::wstring m_LastName  = L"";
		std::wstring m_Alias     = L"";
	};

}


	
template<typename T>
class Reverse :public T
 {
		
public:
    Reverse() = default;
    virtual ~Reverse() = default;

    Reverse<T>(std::wstring fName, std::wstring lName, std::wstring alias )
    {

      T::SetFirstName(fName);
      T::SetLastName(lName);
      T::SetAlias(alias);
    }
		
    void ReversePrint() 
    {
      T::PrintData();			
			
      std::wcout << L"Reverse Print (Mixin):"
      << std::endl
      << T::GetAlias() << L" :"
      << T::GetLastName() << L", "
      << T::GetFirstName()
      << std::endl << std::endl;				
    }
	
 };

}


//========================================

//Main

int Main()
{
    //CRTP
    {
        using namespace UseCRTP;

        //We can use smart pointers        
        auto pshCRTP = std::make_unique<SuperHero>(L"Oliver",L"Queen",L"Green Arrow");
        pshCRTP->ReversePrint();

        //Or plain old 
        SuperHero shCRTP {L"Barry",L"Allen",L"The Flash"};
        shCRTP.ReversePrint();
    }



    //Mixin
    {
       
        using namespace UseMixin;
       
        //We can use smart pointers
        auto pshMixin = std::make_unique<Reverse<SuperHero>>(L"Tony",L"Stark",L"Iron Man");
        pshMixin->ReversePrint();

        //Or plain old
        using CMixin = Reverse<SuperHero>
        CMixin shMixin {L"Miles",L"Morales",L"Ultimate Spider-Man"};
        shMixin.ReversePrint();

    }


    return 0;
}


//Output:

Regular Print:
Oliver, Queen :Green Arrow

Reverse Print (CRTP):
Green Arrow :Queen, Oliver

Regular Print:
Barry, Allen :The Flash

Reverse Print (CRTP):
The Flash :Allen, Barry

Regular Print:
Tony, Stark :Iron Man

Reverse Print (Mixin):
Iron Man :Stark, Tony

Regular Print:
Miles, Morales :Ultimate Spider-Man

Reverse Print (Mixin):
Ultimate Spider-Man :Morales, Miles



Principali differenze tra CRTP e Mixin nell'esempio


Quando usare CRTP e quando Mixin allora?
Useremo CRTP quando vorremo implementare nuove funzionalità generiche estendendo una data classe per mezzo dell'ereditarietà. La classe da estendere dovrà infatti derivare dalla classe template e la classe template deve avere come suo argomento la classe derivata stessa.
Useremo Mixin quando vorremo dotare una data classe di funzionalità aggiuntive ma vogliamo che la classi coinvolte rimangano indipendenti. La classe da estendere non dovrà derivare dalla classe template; la classe template dovrà derivare dalla classe del proprio argomento.

All together now.

Concludiamo il breve riassunto su alcune funzionalità del C++ con un esempio sulle variadic template classes e functions.
Di seguito vedremo come, utilizzando un misto di CRTP, Mixin e variadic template è possibile estendere delle classi a nostro piacimento in maniera alternativa rispetto alla "classica" inheritance.
I più attenti avranno a questo punto già colto il controsenso intrinseco:
per limitare i problemi dell'inheritance useremo... l'inheritance (anche se in modo differente).


Variadic template class and function sample.

Ho in precedenza già introdotto variadic templates; come detto si tratta di classi o funzioni templates che supportano un qualunque numero di argomenti.
Questa caratteristica introduce una grande flessibilità nello sviluppare applicazioni anche molto complesse.


App di esempio: prima versione.
(il cliente ha sempre ragione...forse)

Supponiamo di essere stati incaricati da un nostro committente di creare un'applicazione che manda delle emails ai suoi clienti.
Le funzionalità richieste in sede di progetto sono:

Supponiamo anche che noi, dopo aver accettato l'incarico, abbiamo sollevato le giuste obiezioni in merito a come un corretta progettazione dell'applicazione necessiterebbe di molte altre funzionalità: ipotizziamo un database da cui prelevare i dati dei destinatari, un sistema di salvataggio e visualizzazione delle stesse etc...
Supponiamo infine che il nostro committente non abbia voluto sentire ragioni ed abbia insistito (e pagato) per avere solo le due funzionalità sopradescritte. Nient'altro.

Stando così le cose, scriviamo la nostra applicazione così come la desidera, (d'altronde, come si dice? Il cliente ha sempre ragione...) e gliela diamo in prova.

Qui sotto il codice della nostra applicazione (prima versione).
#pragma once
#include <Windows.h>
#include <iostream>
#include <vector>


namespace Messages_vers_01
{

	interface IMessages
	{
		struct __tagMESSAGES
		{
			std::wstring to;
			std::wstring subject;
			std::wstring text;
		};
		using Message = __tagMESSAGES;		
		virtual void Compose(const Message& msg) = 0;
		virtual void Send() = 0;
		virtual std::vector<Message> Read() = 0;

    };

    using Messages = IMessages::Message;

    interface IDisplay
    {
        virtual void Display() = 0;
    };


    class CEmail :public IMessages, public IDisplay
    {

    public:
    CEmail() = default;
    virtual ~CEmail() = default;

    //IMessages
    virtual void Compose(const Message& msg) { m_email.push_back(msg); };
    virtual void Send() { std::wcout << std::endl << L"  Email sent! " << std::endl; };
    virtual std::vector<Message> Read() { return m_email; };

    //IDisplay
    void Display()
    {

    std::vector<Messages>emails_sent = Read();

    std::wcout << std::endl;
    std::wcout  << L" * * * * * * * * * * *"  << std::endl;

    for (size_t i = 0; i < emails_sent.size(); i++)
    {
    std::wcout  << L"\n\r";
    std::wcout  << L"  # "  << (i + 1)  << std::endl;
    std::wcout  << L"  To:     \t"  << emails_sent[i].to.c_str()  << std::endl;
    std::wcout  << L"  Subject:\t"  << emails_sent[i].subject.c_str()  << std::endl;
    std::wcout  << L"  Text:   \t"  << emails_sent[i].text.c_str()  << std::endl;
    std::wcout  << L"\n\r";
    }

    std::wcout  << L" * * * * * * * * * * *"  << std::endl;


    }

    private:
    std::vector<Message> m_email;

    };
  }


    //=====================================

    //Main
    int main()
    {

    {
    using namespace Messages_vers_01;


    Messages email_msg;
    CEmail em;


    //#1
    email_msg.to = L"my_best_client@myclient.com";
    email_msg.subject = L"Hey, how you doing?";
    email_msg.text = L"Hello my best client! Greetings.";

    em.Compose(email_msg);
    em.Send();

    //#2
    email_msg.to = L"next_client@myclient.com";
    email_msg.subject = L"A word to my NEXT best client!";
    email_msg.text = L"Hello my next best client! Do you know we are in your zone?.";

    em.Compose(email_msg);
    em.Send();

    em.Display();


    return 0;
    }

//Output

    Email sent!

    Email sent!

 * * * * * * * * * * *

  # 1
  To:           my_best_client@myclient.com
  Subject:      Hey, how you doing?
  Text:         Hello my best client! Greetings.


  # 2
  To:           next_client@myclient.com
  Subject:      A word to my NEXT best client!
  Text:         Hello my next best client! Do you know we are in your zone?

 * * * * * * * * * * *



Niente di spettacolare, la classe CEmail implementa le interfacce IMessagese ed IDisplay.
Nota
Nelle interfacce IMessages e IDisplay sono dichiarate delle funzioni virtuali pure; per chi non ricordasse, una funzione virtuale pura DEVE essere obbligatoriamente implementata nelle classi che derivano da classi astratte e/o che implementano le interfacce in cui le funzioni virtuali pure son dichiarate.

Il committente è contento e dopo qualche tempo ci incarica di aggiungere all'applicazione la possibilità di mandare, oltre le emails, dei messaggi SMS.

Noi eseguiamo ed implementiamo la nuova funzionalità... nel modo errato.
Nota
L'applicazione sicuramente funziona, ma ci sono errori di progettazione.


Qui sotto il codice della nostra applicazione (seconda versione).

#pragma once
#include <Windows.h>
#include <iostream>
#include <vector>


namespace Messages_vers_02
{

interface IMessages
	{
		struct __tagMESSAGES
		{
			std::wstring to;
			std::wstring subject;
			std::wstring text;
		};
		using Message = __tagMESSAGES;		
		virtual void Compose(const Message& msg) = 0;
		virtual void Send() = 0;
		virtual std::vector<Message> Read() = 0;

	};
	
	using Messages = IMessages::Message;


	interface IDisplay
	{
		virtual void Display() = 0;
	};


	class CSMS :public IMessages, public IDisplay
	{
	
	 public:
		CSMS() = default;
		virtual ~CSMS() = default;


		//IMessages
		virtual void Compose(const Message& msg) { m_sms.push_back(msg); };
		virtual void Send() { std::wcout << std::endl << L"   SMS sent! " << std::endl; };
		virtual std::vector<Message> Read() { return m_sms; };

		//IDisplay
		void Display()
		{

			std::wcout << std::endl;
			std::wcout << L" * * * * * * * * * * *" << std::endl;

			for (size_t i = 0; i < m_sms.size(); i++)
			{
				std::wcout << L"\n\r";
				std::wcout << L"  # " << (i + 1) << std::endl;
				std::wcout << L"  To:     \t" << m_sms[i].to.c_str() << std::endl;
				std::wcout << L"  Subject:\t" << m_sms[i].subject.c_str() << std::endl;
				std::wcout << L"  Text:   \t" << m_sms[i].text.c_str() << std::endl;
				std::wcout << L"\n\r";
			}

			std::wcout << L" * * * * * * * * * * *" << std::endl;


		}

	private:
		std::vector<Message> m_sms;

	};


	class CEmail :public IMessages, IDisplay
	{

	public:
		CEmail() = default;
		virtual ~CEmail() = default;


		//IMessages
		virtual void Compose(const Message& msg) { m_email.push_back(msg); };
		virtual void Send() { std::wcout << std::endl << L"  Email sent! " << std::endl; };
		virtual std::vector<Message> Read() { return m_email; };


		//IDisplay
		void Display()
		{
			

			std::wcout << std::endl;
			std::wcout << L" * * * * * * * * * * *" << std::endl;

			for (size_t i = 0; i < m_email.size(); i++)
			{
				std::wcout << L"\n\r";
				std::wcout << L"  # " << (i + 1) << std::endl;
				std::wcout << L"  To:     \t" << m_email[i].to.c_str() << std::endl;
				std::wcout << L"  Subject:\t" << m_email[i].subject.c_str() << std::endl;
				std::wcout << L"  Text:   \t" << m_email[i].text.c_str() << std::endl;
				std::wcout << L"\n\r";
			}

			std::wcout << L" * * * * * * * * * * *" << std::endl;

		}

	private:
		std::vector<Message> m_email;

	};

}


//Main

int main()
{

    {
           using namespace Messages_vers_02;


           //Emails section

           Messages email_msg;
           CEmail em;


           //#1          
           email_msg.to = L"my_best_client@myclient.com";
           email_msg.subject = L"Hey, how you doing?";
           email_msg.text = L"Hello my best client! Greetings.";

           em.Compose(email_msg);
           em.Send();

           //#2           
           email_msg.to = L"next_client@myclient.com";
           email_msg.subject = L"A word to my NEXT best client!";
           email_msg.text = L"Hello my next best client! Do you know we are in your zone?";

           em.Compose(email_msg);
           em.Send();
           
           
           em.Display();

           //=============================================

           //SMSs section

           Messages sms_msg;
           CSMS sms;


           //#1          
           sms_msg.to = L"My wife (+39)123.123.123";           
           sms_msg.text = L"I'm still at work. See you later.";

           sms.Compose(sms_msg);
           sms.Send();

           //#2           
           sms_msg.to = L"My new girlfriend (+39)345.345.345";          
           sms_msg.text = L"Hi, I'm coming to you right now.";

           sms.Compose(sms_msg);
           sms.Send();
           
           
           sms.Display();
          
       }


return 0;
}

//Output

  Email sent!

  Email sent!

 * * * * * * * * * * *

  # 1
  To:           my_best_client@myclient.com
  Subject:      Hey, how you doing?
  Text:         Hello my best client! Greetings.


  # 2
  To:           next_client@myclient.com
  Subject:      A word to my NEXT best client!
  Text:         Hello my next best client! Do you know we are in your zone?

 * * * * * * * * * * *

   SMS sent!

   SMS sent!

 * * * * * * * * * * *

  # 1
  To:           My wife (+39)123.123.123
  Text:         I'm still at work. See you later.


  # 2
  To:           My new girlfriend (+39)345.345.345
  Text:         Hi, I'm coming to you right now.

 * * * * * * * * * * *




Anche qui nulla di che, abbiamo aggiunto al progetto un nuova classe ed in questa abbiamo implementato la solite interfacce IMessages ed IDisplay.
Nel farlo abbiamo seguito lo stesso schema progettuale iniziale.
Purtroppo però nell'aggiungere la nuova funzionaltà abbiamo fatto ciò che non si dovrebbe mai fare: abbiamo duplicato il nostro codice , infatti le classi CSMS e CEmail sono identiche ed anche il metodo Display contiene praticamente lo stesso codice in entrambe le classi.

Seguendo questo schema, quando dovremo aggiungere nuove funzionalità, per ognuna di esse andremo a riscrivere lo stesso codice ancora ed ancora.
In breve tempo il nostro codice diventerà ciclopico, in pratica non mantenibile.

Nel nostro progetto qualcosa non va.

Nel frattempo la nostra ipotetica applicazione ha venduto un bel pò di copie ed un altro committente ci incarica di estenderne le funzionalità aggiungendo la possibilità di inviare non solo emails ed SMS ma anche messaggi vocali ed immagini.

Studiando il caso, notiamo che le due nuove funzionalità non sono altro che due ulteriori tipi di messaggi.
Ci serve, a questo punto, un meccanismo generico che implementi qualunque tipo di messaggio senza ogni volta dover riprtere lo stesso codice.

Questi sono i tipi di problemi che vengono risolti con classi e funzioni templates.

A questo punto però rendendoci conto che il progetto originario non supporta alcun template, non possiamo fare altro che rivedere da zero la struttura della nostra applicazione.

Stavolta progetteremo l'app affidandoci a classi e funzioni templates.

Nel nuovo progetto vogliamo che ogni tipo (classe) di messaggio rimanga indipendente rispetto agli altri, ma che tutti i tipi di messaggi abbiano un gestore comune per le funzioni analoghe (Send, Read etc.) che si occupi di delegare a funzioni più specializzate, per ogni classe di messaggio, il lavoro che intendiamo fargli fare.

Per fare un esempio, ogni messaggio dovrà necessariamente gestire una funzione di invio differente.
Questo è naturale, il codice per inviare emails sarà differente da quello di invio di SMS.
Pertanto il gestore generico (una classe template) metterà a disposizione un metodo generico di invio ma potrà anche chiamare il metodo specifico di invio, a seconda che si tratti di emails o SMSs (ed altri tipi in futuro).


Riprogettiamo e riscriviamo l'applicazione secondo il seguente schema UML.
Messages UML


Come vediamo nello schema, implementiamo l'interfaccia IMessages solo nella classe template (base) MessagesT; quest'ultima è una classe template che oltre ad implementare IMessages eredita la classe del suo argomento (T).
L'argomento (T) di essa (da progetto) è la classe variadic template MessageHelper. Come possiamo vedere nel codice (più sotto), la classe MessageHelper eredita la classe del suo argomento template (D).
Possiamo anche vedere la sintassi per definire una classe variadic template (typename...).
La classe MessageHelper si occuperà di fornire le funzioni comuni a tutti i tipi di messaggi: Compose(), Send() e Read().
Tuttavia, come detto prima, se la funzione Send della classe generica MessageHelper può essere invocata quando è necessaria un'ipotetica funzione Send comune a più tipi di messaggi, avremo sicuramente anche la necessità di chiamare la versione specializzata di Send specifica per ogni classe di messaggio.
Questo scopo lo otteniamo aggiungendo alla classe MessageHelper una funzione variadic template il cui nome è SendSpecialized.
Possiamo notare la sintassi della variadic template function SendSpecialized:

template <typename I, typename... Args>
void SendSpecialized(I i, Args... args)
    {			
     I::Send(args...);
    }
    //......
    //......
Stiamo semplicemente dichiarando una funzione template con più argomenti di cui il primo è I e l'altro (Args...) (o per meglio dire gli altri) sono arbitrari.
Args..., nella definizione della funzione, è il parameter pack e può accettare un qualunque numero di argomenti

Nel corpo della funzione ritroviamo l'argomento args della funzione con l'operatore tripli punti (...) che rappresenta il parameter pack expansion.
Tale operatore (...) unpacks/expands il parameter pack in argomenti separati da virgole e quindi per ogni argomento chiama la funzione Send(args...) ricorsivamente.

Vediamo che come nel caso delle classi variadic template anche le funzioni variadiche utilizzano l'operatore (...) detto ellipsis.

Applichiamo la stessa logica alla classe variadic template Displayable che doterà le varie classi di messaggi oltre che della funzione Display specifica, di una funzione Display() generica.

Fatto ciò dichiariamo le classi per i primi due tipi di messaggi: CEmail e CSMS.

Di seguito il codice della nostra applicazione (terza versione).

#pragma once
#include <Windows.h>
#include <iostream>
#include <vector>


namespace Messages_vers_03
{
	
	interface IMessages
	{

		struct __tagMESSAGES
		{
			std::wstring to;
			std::wstring subject;
			std::wstring text;
		};

		using Message = IMessages::__tagMESSAGES;

		virtual void Compose(const Message& msg) = 0;
		virtual void Send() = 0;
		virtual std::vector<Message< Read() = 0;
	};


	using Message = IMessages::Message;

    //Mixin template class	
    template <typename T>
	class MessagesT : public IMessages, public T
	{
	public:

		//default ctor/dtor
		MessagesT() = default;
		virtual ~MessagesT() = default;

		//IMessages
		virtual void Compose(const Message& msg) override { T::Compose(msg); }
		virtual void Send()  override { T::Send(); }
		virtual std::vector<Message> Read() override { return T::Read(); }

	};

    //Mixin variadic template class
	template<typename... D>
	class MessagesHelper :public D...
	{
	public:

		MessagesHelper() = default;
		virtual ~MessagesHelper() = default;

		//IMessages
		void Compose(const Message& msg) { m_msg.push_back(msg); }		
		void Send() { std::wcout << std::endl; std::wcout << L"--> Generalized send method executed..." << std::endl; }
		std::vector<Message> Read() { return m_msg; }

		//variadic template function
		template <typename I, typename... Args>
		void SendSpecialized(I i, Args... args)
		{
			
			I::Send(args...);
		}


		

	private:
		std::vector<Message> m_msg;

	};


	interface IDisplay
	{
		virtual void Display() = 0;
		virtual void Display(const std::vector<Message>& msg) = 0;
	};


    //Mixin variadic template class
	template <typename... J>
	class Displayable : public IDisplay, public J...
	{
	public:
		Displayable() = default;
		virtual ~Displayable() = default;

		virtual void Display() override { std::wcout << std::endl;  std::wcout << L"--> Generalized Display method executed" << std::endl; }
		
		virtual void Display(const std::vector<Message>& msg) override { std::wcout << std::endl;  std::wcout << L"--> Generalized Display method executed" << std::endl; }

		//variadic template function
        template <typename V,typename...Args>
		void Display(V v, Args... args)
		{
			
			std::wcout << std::endl;
			std::wcout << L" * * * * * * * * * * *" << std::endl;
			
			V::Display(args...);
			

			std::wcout << L" * * * * * * * * * * *" << std::endl;
			std::wcout << std::endl;
		}

	};

	class CEmail
	{
	public:

		//default ctor/dtor
		CEmail() = default;
		virtual ~CEmail() = default;

		void Send() { std::wcout << std::endl; std::wcout << L"--> Specialized email send method executed..." << std::endl; }

		void Display(const std::vector<Message>& msg) 
		{ 
			std::wcout << std::endl;
			std::wcout << L"--> Specialized email display method executed..." << std::endl; 

			for (size_t i = 0; i < msg.size(); i++)
			{
				std::wcout << L"\n\r";
				std::wcout << L"  # " << (i + 1) << std::endl;
				std::wcout << L"  To:     \t" << msg[i].to.c_str() << std::endl;
				std::wcout << L"  Subject:     \t" << msg[i].subject.c_str() << std::endl;
				std::wcout << L"  Text:   \t" << msg[i].text.c_str() << std::endl;
				std::wcout << L"\n\r";
			}
		}

	};


	class CSMS 
	{
	public:

		//default ctor/dtor
		CSMS() = default;
		virtual ~CSMS() = default;

		void Send() { std::wcout << std::endl; std::wcout << L"--> Specialized sms send method executed..." << std::endl; }

		void Display(const std::vector<Message>& msg) 
		{ 
			std::wcout << std::endl;
			std::wcout << L"--> Specialized sms display method executed..." << std::endl; 

			for (size_t i = 0; i < msg.size(); i++)
			{
				std::wcout << L"\n\r";
				std::wcout << L"  # " << (i + 1) << std::endl;
				std::wcout << L"  To:     \t" << msg[i].to.c_str() << std::endl;
				std::wcout << L"  Text:   \t" << msg[i].text.c_str() << std::endl;
				std::wcout << L"\n\r";
			}

		}

		
	};

    //New message types declaration
    using TextMessage = MessagesT<MessagesHelper<Displayable<CSMS>>>;
    using EmailMessage = MessagesT<MessagesHelper<Displayable<CEmail>>>;

}


//Main

int main()
{


 {

    using namespace Messages_vers_03;
 
    Message msg;
    
    TextMessage smsType;
    EmailMessage emType;
   
    
    //Email message type
    
    //# 1 email type  message
    msg.to = L"my_best_client@myclient.com";
    msg.subject = L"Hey, how you doing?";
    msg.text = L"Hello my best client! Greetings.";

    emType.Compose(msg);

    //# 2 email type message
    msg.to = L"next_client@myclient.com";
    msg.subject = L"A word to my NEXT best client!";
    msg.text = L"Hello my next best client! Do you know we are in your zone?";
          
    emType.Compose(msg);
   
   //performs generic send method,  optional to specialized method,
   //it is not necessary here
    emType.Send();
    
    emType.SendSpecialized<CEmail>(emType);

   //performs generic display method,  optional to specialized method,
   //it is not necessary here   
    emType.Display();
    
    emType.Display<CEmail>(emType, emType.Read());
    
 //==========================================

    //Text message type

     //# 1 text type  message
    msg.to = L"My wife (+39)123.123.123";
    msg.text = L"I'm still at work. See you later.";
    

    smsType.Compose(msg);

    //# 2 text type message
    msg.to = L"My new girlfriend (+39)345.345.345"; 
    msg.text = L"Hi, I'm coming to you right now.";

    smsType.Compose(msg);

   //performs generic send method,  optional to specialized method,
   //it is not necessary here    
    smsType.Send();

    smsType.SendSpecialized<CSMS>(smsType);

    //performs generic display method,  optional to specialized method,
    //it is not necessary here
    smsType.Display();


    smsType.Display<CSMS>(smsType, smsType.Read());


 }


return 0;
}

//Output:


    --> Generalized send method executed...

    --> Specialized email send method executed...

    --> Generalized Display method executed

    * * * * * * * * * * *

    --> Specialized email display method executed...

    # 1
    To:           my_best_client@myclient.com
    Subject:      Hey, how you doing?
    Text:         Hello my best client! Greetings.


    # 2
    To:           next_client@myclient.com
    Subject:      A word to my NEXT best client!
    Text:         Hello my next best client! Do you know we are in your zone?

    * * * * * * * * * * *


    --> Generalized send method executed...

    --> Specialized sms send method executed...

    --> Generalized Display method executed

    * * * * * * * * * * *

    --> Specialized sms display method executed...

    # 1
    To:           My wife (+39)123.123.123
    Text:         I'm still at work. See you later.


    # 2
    To:           My new girlfriend (+39)345.345.345
    Text:         Hi, I'm coming to you right now.

    * * * * * * * * * * *





Possiamo vedere come in questa nuova versione dell'app abbiamo, da una parte, evitato il problema della multiple inheritance, pur mantenendola in altra forma e come alcune funzioni siano comunque polimorfiche.
Inoltre l'introduzione nel progetto dei variadic templates rende più semplice l'enstensione ad eventuali nuovi tipi di messaggi nel tempo.

In effetti, prima abbiamo detto che era necessario riscrivere l'app (di fantasia) proprio perchè ci era stato chiesto di aggiungere altri tipi di messaggi (vocali e immagini).

Stavolta però aggiungere le nuove funzionalità è un lavoro di 5 minuti.

Aggiungiamo altre due classi al progetto:


//.........
//.........

class CVocal
	{

	public:
		
		//default ctor/dtor
		CVocal() = default;
		virtual ~CVocal() = default;


		void Send() { std::wcout << std::endl; std::wcout << L"--> Specialized vocal send method executed..." << std::endl; }

		void Display(const std::vector<Message>& msg)
		{
			std::wcout << std::endl;
			std::wcout << L"--> Specialized vocal display method executed..." << std::endl;

			for (size_t i = 0; i < msg.size(); i++)
			{
				std::wcout << L"\n\r";
				std::wcout << L"  # " << (i + 1) << std::endl;
				std::wcout << L"  To:     \t" << msg[i].to.c_str() << std::endl;
				std::wcout << L"  Audio:  \t" < msg[i].audio_video_image.c_str() << std::endl;
				std::wcout << L"\n\r";
			}

		}

	};



	class CVideoOrImage
	{

	public:

		//default ctor/dtor
		CVideoOrImage() = default;
		virtual ~CVideoOrImage() = default;


		void Send() { std::wcout << std::endl; std::wcout << L"--> Specialized image or video send method executed..." << std::endl; }

		void Display(const std::vector<Message>& msg)
        {
           std::wcout << std::endl;
           std::wcout << L"--> Specialized image or video display method executed..." << std::endl;

           for (size_t i = 0; i < msg.size(); i++)
           {
                 std::wcout << L"\n\r";
                 std::wcout <<L"  # " << (i + 1) << std::endl;
                 std::wcout << L"  To:     \t" << msg[i].to.c_str() << std::endl;
                 std::wcout << L"  Image:  \t" << msg[i].audio_video_image.c_str() << std::endl;
                  std::wcout << L"\n\r";
            }

         }

    };

    //.....
    //.....
     using VocalMessage = MessagesT<MessagesHelper<Displayable<CVocal>>>;
     using VideoOrImage = MessagesT<MessagesHelper<Displayable<CVideoOrImage>>>;

    //.....
    //.....


//Main
int main()

//.....
//.....

 //Vocal Messages

    VocalMessage vmType;
    msg.to = L"(+39)123.123.123";
    msg.audio_video_image = L"vocal.mp4";
    
    vmType.Compose(msg);

    vmType.SendSpecialized<CVocal>(vmType);

    vmType.Display<CVocal>(vmType, vmType.Read());



    //Image Messages

    VideoOrImage vd_imType;
    msg.to = L"(+39)123.123.123";
    msg.audio_video_image = L"image.png";

    vd_imType.Compose(msg);

    vd_imType.SendSpecialized<CVideoOrImage>(vd_imType);

    vd_imType.Display<CVideoOrImage>(vd_imType, vd_imType.Read());

//.....
//.....

//Output:


--> Specialized vocal send method executed...

 * * * * * * * * * * *

--> Specialized vocal display method executed...

  # 1
  To:           (+39)123.123.123
  Audio:        vocal.mp4

 * * * * * * * * * * *


--> Specialized image or video send method executed...

 * * * * * * * * * * *

--> Specialized image or video display method executed...

  # 1
  To:           (+39)123.123.123
  Image:        image.png

 * * * * * * * * * * *



Bene, ora la nostra applicazione, grazie al codice riusabile è decisamente adattabile ai più svariati tipi di messaggi.
Abbiamo visto, inoltre, come e perchè usare classi/funzioni templates e variadic templates; possiamo pertanto considerare concluso questo breve recap di alcune importanti funzionalità del C++ e continuare l'applicazione UWP/WinRT/Win32 di esempio.




Giuseppe Pischedda 2020


Se il post ti è utile puoi fare una donazione all'autore, l'importo è a tua libera scelta.

Grazie