Monthly Archives: December 2018

Programmez – My Articles from Oct 2017 to Jan 2019

Here are my articles for french Magazine Programmez:

  • N°225: Rookit key Logger – PDF
  • N°224: Space Invaders 1978 en C/C++ avec SFML – PDF
  • N°223: Windows Le Multithreading en C/C++ – PDF
  • N°222: Linux Le Multithreading en C++ – PDF
  • N°221: Au coeur d’un Service Windows NoSQL – PDF
  • N°220: Créer un service Windows – PDF
  • N°218: Migrer son code C/C++ en 64 bits – PDF
  • N°217: Les Tests en C++ – PDF
  • N°216: La Programmation Orientée Objet en C++ – PDF
  • N°215: Utiliser shared_ptr<T> en C++ pour la gestion des ressources – PDF
  • N°214: Développez un IDE en C++ Partie II – PDF
  • N°213: Développez un IDE en C++ Partie I – PDF
  • N°212: Un serveur REST Web API en C++ – PDF
  • N°211: Pourquoi C++ en 2017 ? – PDF

Surface Go feedback

After two weeks of using the Surface Go, I am very satisfied. I have installed Office suite 2019 for documents and messaging stuff.

The Microsoft multimedia keyboard is easy to use… The Pentium processor is enough to run Office, VS Code and Visual C++. I compile my teaching projects very fast and it works well. The memory of 4GB is just a problem if you open 20 Chrome windows but if you close and open at regular basis, it works perfectly well. This is definitively a good buy.

C++ developers need few resources…

I have installed Visual Studio 15.9.4 on my Surface Go with only Visual C++ features.

As a C++ developer, I only rely on small features:

  • an IDE for managing files, projects and the editor
  • an ISO C++ compiler

Surface Go comes with a Pentium processor and fast hard drive so it’s cool for C++ stuff.

Surface Go for developers !

My gift for Christmas is a Surface Go with a Microsoft Media Keyboard and two 128GB SanDisk MicroSD cards.

I have installed all the necessary tools:

  • 7zip
  • Acrobat Reader
  • VLC
  • Winamp
  • Notepad++
  • Chrome
  • TechSmith Camtasia 2018
  • SysInternals Suite

I have installed the following Microsoft products:

  • Office 2019
  • Visual Studio 2017 15.9.2 with Visual C++ only tools -> 10 GB

After that, remaining space is 10GB.

On SD cards, I put data, ebooks and multimedia files (mp3 and movies).

Search for files & folders

My boss wanted a tool for searching files and folders from a dedicated path… He used to write it using Powershell but took hours to run… Here is why system programming is important. No runtime, no framwork, no virtual machine, it runs direct on the metal. Fast !

#include <string>
#include <iostream>
#include <atlstr.h> 
#include <tchar.h>
#include <Windows.h>
#include <Shlwapi.h>

#pragma comment(lib, "Shlwapi.lib")


CString SearchDrive(const CString& strFile, const CString& strFilePath, const bool& bRecursive, const bool& bStopWhenFound) 
{
       CString strFoundFilePath;
       WIN32_FIND_DATA file;

       CString strPathToSearch = strFilePath;
       strPathToSearch += _T("\\");

       HANDLE hFile = FindFirstFile((strPathToSearch + "*"), &file);
       if (hFile != INVALID_HANDLE_VALUE)
       {
             do
             {
                    CString strTheNameOfTheFile = file.cFileName;

                    // It could be a directory we are looking at
                    // if so look into that dir
                    if (file.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
                    {
                           if ((strTheNameOfTheFile != ".") && (strTheNameOfTheFile != "..") && (bRecursive))
                           {
                                  strFoundFilePath = SearchDrive(strFile, strPathToSearch + strTheNameOfTheFile, bRecursive, bStopWhenFound);

                                  if (!strFoundFilePath.IsEmpty() && bStopWhenFound)
                                        break;
                           }
                    }
                    else
                    {
                           strFoundFilePath = strPathToSearch + strTheNameOfTheFile; //strFile;

                           long size = (file.nFileSizeHigh * (MAXDWORD + 1)) + file.nFileSizeLow;

                           TCHAR szPath[10240];
                           memset(szPath, 0, 10240);
                           _tcscpy_s(szPath, (LPCTSTR)strFoundFilePath);
                           ::PathRemoveFileSpec(szPath);
                           _tprintf(_T("%s;%s;%ld\n"), szPath, (LPCTSTR)strTheNameOfTheFile, size);


                           if (bStopWhenFound)
                                  break;
                    }
             } while (FindNextFile(hFile, &file));

             FindClose(hFile);
       }

       return strFoundFilePath;
}


int _tmain(int argc, TCHAR *argv[])
{
       if (argc != 2)
       {
             std::wcout << _T("Help") << std::endl;
             std::wcout << _T("usage: scanfiles <path>") << std::endl;
             return 0;
       }

       TCHAR strPath[1024];
       wcscpy_s(strPath, argv[1]);

       TCHAR strWildcard[255];
       wcscpy_s(strWildcard, _T("*.*"));

       SearchDrive(strWildcard, strPath, true, false);

       return 0;
}

 

Article in Programmez for January 2019

My next article will be in french Magazine Programmez in January 2019. It will talks about rootkits and key logger.

225

Visual Studio 2019 Live Share

With LiveShare feature, you can do remote debugging and remote coding from and to VS &nd VS Code:

Microsoft Connect(); 2018 KeyNote !

https://blogs.msdn.microsoft.com/dotnet/2018/12/04/announcing-net-core-3-preview-1-and-open-sourcing-windows-desktop-frameworks/

En vrac:

  • Visual Studio Code Collaboration Mode
  • Visual Studio 2019 Preview
  • Intellicode : Intellisence avec machine learning
  • Nouveautés Github
  • NET Core 3
  • Open Source WinForms, WPF et XAML UI
  • Nouveautés Azure Cognitive Services

Microsoft Connect() ; 2018

You as developers more productive.

The world runs on software.

It’s a great time to be a developer

Next generation of softwares

Powerful open in a productive way

Connected Apps

Intellicode vs code extension

Azure App Service

Python function

VS Code and VS collaboration

Microsoft loves open source

Nat GitHub CEO

CD with Github

Build your pipeline

VS2019

.NET Core 3

New capabilities

Fix my code

Power of Intellicode

Azure

AI

Articles C++ en Serie…

OOP : http://bit.ly/2rhQFYl

Basic – Partie 1 : http://bit.ly/2FT92Nn

Les classes – Partie 2 : http://bit.ly/2FROSDK

Les Templates – Partie 3 : http://bit.ly/2zwZ4fd

STL – Partie 4 : http://bit.ly/2Sp6kAJ

Boost – Partie 5: http://bit.ly/2U7r4OX

 

La Programmation Orientée Objet (OOP)

Introduction

Il y a plusieurs façons d’utiliser le C++. On peut être utilisateur de classes ou concepteur de classes. Dans les deux cas, définir, designer et implémenter des classes sont les activités primaires des développeurs C++. Par où on commence ? En général, on démarre avec une abstraction ou un concept. On considère un truc à coder et puis on essaie de voir comment on va gérer son ou ses fonctions, ce qui est interne et ce qui est public et comment on va l’utiliser… Au démarrage de cette réflexion, il faut se mettre la casquette de celui qui va utiliser la classe. Notre exemple de concept pour cet article sera un élément graphique à dessiner comme une ligne, un rectangle, une ellipse, etc. En anglais, c’est un shape.

// C’est une déclaration de classe

class CShape;

// C’est la future manière avec laquelle je veux dessiner des Shapes

bool Draw(const CShape& item);

Ensuite on envisage la classe dans son ensemble :

class CShape

{

public:

// interface publique

private:

// implémentation privée

};

Qu’est-ce que l’on met en private/public et comment va-t-elle être structurée. On commence petit bras et puis la classe prend du volume.

Constructeur et Destructeur

La première question qui vient à l’esprit est comment je vais créer mon objet et ai-je besoin de lui fournir des paramètres. Avec ma classe CShape, on peut se dire, je n’ai pas besoin mais aussi j’en ai besoin. Les deux possibilités existent ! Soit c’est une figure connue -> d’où une énumération pour les types connus et autre possibilité qui est de dessiner une image et là, il me faut un nom de fichier ; tout est ouvert !

enum ShapeType

{

line,

circle,

rectangle,

left_arrow,

right_arrow,

picture

};

L’avantage de l’énumération es que c’est évalué à la compilation. Il ne peut pas y avoir d’erreur sur le nommage car c’est un type explicite contrairement à une simple chaîne de caractères. Voyons sommairement comment pourrait être construite cette fameuse classe CShape…

class CShape

{

public:

CShape();

CShape(int x, int y, int xx, int yy, ShapeType type);

CShape(int x, int y, int xx, int yy, std::string fileName);

virtual ~CShape();

private:

std::string m_fileName;

ShapeType m_type;

};

J’y vois deux façons de créer un shape. Soit à partir de l’énumération soit à partir d’un fichier pour les images. Pour le moment, je ne sais pas comment organiser le dessin du shape. Dois-je le mettre à l’intérieur de la classe ou à l’extérieur ? Je ne sais pas… Il faut du temps pour considérer quelle sera la façon la plus naturelle de réaliser cette opération. Dans le monde Windows GDI+, pour dessiner un élément il faut un handle particulier qui possède toutes les primitives de dessins. En pensant comment allait être dessiner un élément, on peut envisager d’inclure une méthode Draw dans la classe CShape. La première pensée, c’est de se dire, je vais implémenter tous les cas possibles dans Draw avec une construction qui ressemble à ça :

void Draw(const CDrawingContext& ctxt) const

{

if (m_type == ShapeType::line)

{

ctxt.Line(m_rect.left, m_rect.bottom,

m_rect.top, m_rect.right);

}

if (m_type == ShapeType::rectangle)

{

ctxt.Rectangle(m_rect);

}

// TODO

}

Dans la classe, il est important de marquer les méthodes qui ne modifient pas les données private. Pour faire cela, on leur ajoute le mot-clé const à la fin comme c’est indiqué sur la méthode Draw.

De plus, lorsque l’on passe un argument qui n’as pas vocation à être modifié, on le passe en référence const. Comme on peut le voir ci-dessus, la définition d’une classe est un travail itératif dans lequel, les meilleures idées chassent celles d’avant ou les conforte. Si une classe est bien conçue, le lecture de ses membres public est limpide car on va à l’essentiel. Attention, les membres private expliquent aussi beaucoup de choses sur les détails d’implémentation. Maintenant, mettons-nous dans un contexte où il existe plusieurs classes et les principes vu ci-dessus ne suffisent pas à faire un modèle objet. Il nous faut des mécanismes plus sophistiqués pour relier les classes les unes avec les autres.

Les classes possèdent des associations et des comportements et c’est ici que les Jedi se positionnent pour y designer u nmodèle élégant, facile à utiliser et qui rend les services voulus.

Notre méthode Draw n’est pas OOP car si je rajoute un type dans l’énumération, je dois ajouter un if() dans la méthode Draw() et coder l’implémentation. On peut faire mieux, beaucoup mieux.

Les concepts de OOP

Les deux piliers de l’OOP sont l’héritage et le polymorphisme. L’héritage permet de grouper les classes en famille de types et permet de partager des opérations et des comportements et des données. Le polymorphisme permet de déclencher des opérations dans ces familles à un niveau unitaire. Ainsi ajouter ou supprimer une classe n’est pas trop un problème.

L’héritage définit une relation parent/enfant. Le parent définit l’interface public et l’implémentation private est commune à tous ses enfants. Chaque enfant choisit s’il veut hériter d’un comportement unique ou bien le surcharger. En C++, le parent est appelé « classe de base » et l’enfant « classe dérivée ». Le parent et les enfants définissent une hiérarchie de classes. Le parent est souvent une classe abstraite et les enfants implémentent les méthodes virtuelles pures pour que l’ensemble fonctionne.

Dans un programme OOP, on manipule les classes via un pointeur sur la classe de base plutôt que sur les objets dérivés.

Une classe abstraite

Revenons sur la classe CShape… La prochaine étape dans le design est de créer une classe abstraite pour identifier les opérations propres à chaque item – ce qui implique des opérations avec une implémentation basée sur une classe dérivée. Ces opérations sont des fonctions virtuelles pures de la classe de base. Une méthode virtuelle pure est une méthode abstraite. Il n’y a pas de corps. Cela implique que le classe devient abstraite aussi. Il n’est pas possible de créer un objet à partir d’une classe abstraite. La méthode Draw() doit être définie comme virtuelle pure car il y a plusieurs types de dessin à faire et que chaque dessin est propre à une classe donnée. Le corps de Draw() n’existe pas, c’est la raison pour laquelle on dit que la classe est abstraite ; il y a au moins une méthode virtuelle pure.

Voici donc comment on fait :

class CShapeEx

{

public:

CShapeEx() {}

CShapeEx(ShapeType type, RECT rect, const std::string &fileName)

: m_type(type), m_rect(rect), m_fileName(fileName) {}

virtual ~CShapeEx() {}

public:

virtual void Draw(const CDrawingContext& ctxt) const = 0;

virtual void DrawTracker(const CDrawingContext& ctxt) const = 0;

private:

std::string m_fileName;

ShapeType m_type;

RECT m_rect;

};

Et nous allons maintenant déclarer des classes dérivées de CShapeEx :

class CRectangle : public CShapeEx

{

public:

CRectangle() {}

virtual ~CRectangle() {}

public:

virtual void Draw(const CDrawingContext& ctxt) const

{

// TODO

std::cout << "Rectangle::Draw" << std::endl;

}

virtual void DrawTracker(const CDrawingContext& ctxt) const

{

// TODO

}

};

class CLine : public CShapeEx

{

public:

CLine() {}

virtual ~CLine() {}

public:

virtual void Draw(const CDrawingContext& ctxt) const

{

// TODO

std::cout << "Line ::Draw" << std::endl;

}

virtual void DrawTracker(const CDrawingContext& ctxt) const

{

// TODO

}

};

Si on veut rajouter un item à dessiner dans la hiérarchie, il suffit de rajouter une classe dérivée ! Maintenant nous allons voir comment fonctionne ce mécanisme de virtual…

CDrawingContext ctxt;

CRectangle * pRect = new CRectangle();

CLine * pLine = new CLine();

CShapeEx * ptr = nullptr;

ptr = pRect;

ptr->Draw(ctxt);

ptr = pLine;

ptr->Draw(ctxt);

On commence par déclarer 2 items à dessiner. Puis le manager, un pointeur sur la classe de base CShapeEx est déclaré. A chaque fois qu’il pointe sur un objet d’une classe dérivée, la fonction virtuelle Draw() appelée est celle de la classe dérivée car cette fonction est définie comme virtuelle.

Maintenant, il nous manque quelque chose de très utile et qui évitera les usines à gaz de construction d’objets dérivés. Il nous faut une factory ! Un mécanisme de création d’objet.

La factory

class CFactory

{

private:

CFactory();

public:

static CShapeEx * CreateObject(ShapeType type)

{

if (type == ShapeType::line)

{

CLine * pObj = new CLine();

pObj->m_type = type;

return pObj;

}

if (type == ShapeType::rectange)

{

CRectangle * pObj = new CRectangle();

pObj->m_type = type;

return pObj;

}

// TODO

return nullptr;

}

};

Cette classe permet via une méthode static de créer un objet en fonction de son type dans l’énumération. Le type est affecté en variable membre de l’objet créé. On notera que le constructeur est private, cela veut dire que l’on ne peut pas déclarer d’objet CFactory. On ne peut qu’utiliser la méthode static CreateObject. Sauf que, la compilation tombe en erreur :

Le membre m_type est inaccessible vue son niveau de protection… On va donc ajouter quelque chose à la classe CShapeEx :

class CShapeEx

{

public:

CShapeEx() {}

CShapeEx(ShapeType type, RECT rect, const std::string &fileName)

: m_type(type), m_rect(rect), m_fileName(fileName) {}

virtual ~CShapeEx() {}

public:

virtual void Draw(const CDrawingContext& ctxt) const = 0;

virtual void DrawTracker(const CDrawingContext& ctxt) const = 0;

private:

std::string m_fileName;

ShapeType m_type;

RECT m_rect;

// La classe CFactory peux avoir accès aux membres…

friend class CFactory;

};

On ajoute en fin de classe que la classe CFactory est amie de la classe CShapeEx. Et là, il n’y a plus d’erreur de compilation ! Les puristes comme AlainZ, à Redmond, vous diront que cela viole la mécanique objet et que c’est une construction exotique et qu’il vaut mieux passer par le constructeur pour passer le type… Je ne suis pas de cet avis. Je pense que c’est élégant de faire une entorse pour la factory. Voici maintenant le code qui appelle la factory :

CDrawingContext ctxt;

CShapeEx * pRect = CFactory::CreateObject(ShapeType::rectange);

CShapeEx * pLine = CFactory::CreateObject(ShapeType::line);

CShapeEx * ptr = nullptr;

ptr = pRect;

ptr->Draw(ctxt);

ptr = pLine;

ptr->Draw(ctxt);

Et le résultat est équivalent.

Compilation sous Linux

Pour ceux qui ne le savent pas, le langage C++ est normalisé ISO. Sur la plateforme Linux, là où tout est fait en C/C++, le langage possède plusieurs compilateurs dont le célèbre GCC. Vous pouvez utiliser Visual Studio Code que fournit Microsoft comme IDE pour compiler sous Linux via le terminal.

Il faut installer l’extension C++. Moi j’ai installé Cygwin et MSYS. Ainsi je dispose de commandes Linux.

Une touche de C++ 11

On va essayer de continuer notre application de dessin en matérialisant la zone de dessiner. Cela ressemble à un ensemble de shapes sur un support de dessin. Première étape quand on pense au concept de la planche à dessin c’est quoi ? Le stockage des éléments graphiques dans un container STL. Oui Monsieur, quand on veut stocker quelque chose, on passe par les containers STL. On va donc utiliser le std::vector<T> de la STL. On va stocker des shapes un peu particulier que sont les shapes partagés. Partagés ? Par qui ? Dans un vrai logiciel de dessin, on affiche les propriétés dans une grille et le dessin est sur le panneau central donc on partage nos objets et ce n’est pas que de l’affichage. Je vous montre le résultat.

Sur cet écran, on voit que le rectangle bleu sélectionné a ses attributs directement dans le grille de propriétés mais aussi sur le Ribbon. Comment faire pour partager les données entre le panneau de dessin, le Ribbon et la grille de propriétés ? Est-ce que l’on fait des copies des objets ? Oui, mais on fait des copies de pointeurs et non des copies d’objets. Pour faire cela, une utilise le mécanisme des pointeurs partagés de la STL. Voyons son utilisation. Premièrement, on va designer la zone de dessin.

La zone de dessin

Cette zone contient les objets shape à dessiner. On va donc créer un vector<shared_ptr<CShapeEx>>. Le container vector est défini dans <vector>. On va avoir une collection d’éléments partagés CShapeEx. Ainsi, on pourra stocker ce pointeur particulier où on veut sans se soucier de la libération de la mémoire. La zone de dessin est une vue fille MDI en jargon Windows. Elle possède deux scrollbars, un verticale et une horizontale. Si on veut faire une gestion de shared_ptr<T>, il faut faire évoluer la factory :

static std::shared_ptr<CShapeEx> CreateObject(ShapeType type)

{

std::shared_ptr<CShapeEx> pObj = nullptr;

if (type == ShapeType::line)

{

pObj = std::make_shared<CLine>();

pObj->m_type = type;

return pObj;

}

if (type == ShapeType::rectangle)

{

pObj = std::make_shared<CRectangle>();

pObj->m_type = type;

return pObj;

}

return nullptr;

}

On ne fait plus un new() mais un appel à std::make_shared<T>().

De plus, dans l’utilisation des objets, cela donne ça :

CDrawingContext ctxt;

std::shared_ptr<CShapeEx> pRect = CFactory::CreateObject(ShapeType::rectangle);

std::shared_ptr<CShapeEx> pLine = CFactory::CreateObject(ShapeType::line);

std::shared_ptr<CShapeEx> ptr = nullptr;

ptr = pRect;

ptr->Draw(ctxt);

ptr = pLine;

ptr->Draw(ctxt);

Il existe même une façon de laisser le compilateur déterminer le type de variable utilisée en utilisant auto :

CDrawingContext ctxt;

auto pRect = CFactory::CreateObject(ShapeType::rectangle);

auto pLine = CFactory::CreateObject(ShapeType::line);

auto ptr = pRect;

ptr->Draw(ctxt);

ptr = pLine;

ptr->Draw(ctxt);

Revenons à notre design de classes. Le container de shapes est juste une collection std::vector<T> de std::shared_ptr<CShapeEx> :

class CElementContainer : public CObject

{

private:

public:

DECLARE_SERIAL(CElementContainer);

CElementContainer();

virtual ~CElementContainer(void);

// ../..

// overridden for document i/o

virtual void Serialize(CElementManager * pElementManager, CArchive& ar);

// Debuging Operations

public:

void DebugDumpObjects(CModeler1View * pView);

// Operations MFC like

public:

std::shared_ptr<CElement> GetHead();

int GetCount();

void RemoveAll();

void Remove(std::shared_ptr<CElement> pElement);

void AddTail(std::shared_ptr<CElement> pElement);

// Attributes

private:

vector<std::shared_ptr<CElement>> m_objects;

};

Là, je vous montre un vrai code et les éléments sont matérialisés par la classe CElement mais c’est pareil que CShapeEx. On notera que les fonctions de gestion associées au vector sont des méthodes helpers dans cette classe. Il faut maintenant intégrer cette classe dans la mécanique de fenêtrage Windows.

Je pratique le C++ – Partie 5/5 – Boost

Je pratique le C++ – Partie 5/5.

Nous vous proposons une série d’articles sur la pratique de C++ pour que vous puissiez tous vous y mettre. Ce mois-ci on aborde la librairie Boost du site www.boost.org. Boost est une communauté de programmeurs C++ dont le but est de pousser les librairies qu’ils font vers la standardisation C++. Boost c’est une librairie C++ portable. Vous pouvez l’utiliser avec Visual C++ sur Windows et GCC ou CLang sur Linux.

Comment obtenir Boost ?

Très simplement en allant dans la rubrique download du site et vous êtes et vous télécharger un zip de 120 MB ou un targz de 80 MB. Vous décompressez votre archive et vous êtes presque prêt. La décompression vous donne une arborescence de 430 MB. Il y a énormément de documentation HTML c’est la raison pour laquelle la taille du dossier est conséquente.

 

Compilation de Boost

Malgré que Boost soit en majorité un ensemble de templates et de fonctions inline donc juste les fichiers d’entêtes suffisent, il y a des librairies qu’il faut compiler.

La première chose à faire c’est de lancer la compilation de la librairie. Cela se fait en deux étapes. Premièrement, il faut compiler le custom Make de Boost qui se nomme bjam. Il faut lancer bootstrap.bat. Maintenant que bjam est compilé, on va builder la librairie avec notre chaine de compilation particulière à une plateforme. Je travaille avec Visual Studio 2013 donc voici ma ligne de commande :

 

bjam toolset=msvc-12.0 variant=debug,release threading=multi link=shared

 

Je fais une compilation en debug et en relase et indiquant que je veux une librairie multi-thread et en mode dynamique (DLL). La compilation prends un peu de temps mais disons qu’en 15 minutes c’est terminé.

 

 

Introduction à Boost.Serialization

Maintenant que nous avons listé l’ensemble des librairies disponibles dans Boost, nous allons passer au code ! Examinons une librairie très utile qui sait sérialiser et dé-sérialiser des classes en format binaire ou XML sans forcer. Cette librairie permet de sérialiser des containers ce qui évitera de parcourir nos différentes collections pour sérialiser des éléments simples. Par contre, nous devons faire attention à une collision de nom dans l’espace de noms disponible. Je m’explique… Boost sérialise les boost ::string, les boost ::vector & co… Ce qui veut dire qu’il va falloir transvaser nos objets écrits avec la STL standard dans des objets Boost compatibles à moins que notre application soit écrite complètement avec Boost. C’est un choix d’Architecture !

Nous allons partir sur une application que j’ai écrite pour ma fille en 2012. Il s’agit d’une application de dessin ou l’on dispose des éléments prédéfinis sur une page comme des images, des rectangles, des ronds, des triangles, etc. Cette application a permit à ma fille de 8 ans à l’époque de savoir manier la souris et de jouer avec le Ribbon pour changer les caractéristiques des objets (couleur, épaisseur de traits, etc).

 

Le résultat de la sérialisation XML

Commençons par la fin… Le dessin est le suivant :

Le but du jeu est de sauvegarder ce dessin sous forme de document XML.

Ouvrons la documentation de Boost.Serialization pour prendre la chose du bon côté et pour arriver au résultat ci-dessus. Si vous avez l’œil, vous verrez que l’application que je vous propose est réalisée avec les Microsoft Foundation Classes (MFC) car elle me permet de faire un joli Ribbon et de mettre en place le support du Document/Vue qui me permet de faire une application qui charge/enregistre mes données. Bref, MFC rocks. Je crois d’ailleurs qu’on va refaire une série Je Pratique le C++ sous Windows en 5 épisodes pour que vous puissiez développer vous aussi des applications qui rocks ! On verra… revenons à nos moutons. Le XML. Boost.Serialization fonctionne avec ce que l’on appelle une Archive dans laquelle on a accès à des données. Il faut nourrir l’archive avec des données. J’ai donc une classe d’objets à dessiner et des objets. Le modèle de données est constitué de 2 classes : CSimpleShape et CShapeCollection.

 

La classe CSimpleShape

Cette classe contient les propriétés suivantes :

public:

wstring m_name;

wstring m_id;

string m_rect;

long m_type;

long m_shapeType;

wstring m_caption;

wstring m_text;

long m_x1;

long m_y1;

long m_x2;

long m_y2;

int m_colorFillR;

int m_colorFillG;

int m_colorFillB;

int m_colorLineR;

int m_colorLineG;

int m_colorLineB;

bool m_bSolidColorFill;

bool m_bColorLine;

bool m_bColorFill;

};

 

BOOST_CLASS_VERSION(CSimpleShape, 1)

 

La classe CShapeCollection

Cette classe contient la liste des elements:

public:

vector<boost::shared_ptr<CSimpleShape> > m_shapes;

};

 

BOOST_CLASS_VERSION(CShapeCollection, 1)

 

C’est un vecteur de CSimpleShape tout simplement… Pour que ces éléments soient stockés sous forme de fichier texte, binaire ou XML il faut préciser quelques petites choses dans les classes. Il faut spécifier cela en private :

private:

friend class boost::serialization::access;

template<class Archive>

void save(Archive & ar, const unsigned int version) const

{

ar & BOOST_SERIALIZATION_NVP(m_shapes);

}

 

template<class Archive>

void load(Archive & ar, const unsigned int version)

{

ar & BOOST_SERIALIZATION_NVP(m_shapes);

}

 

BOOST_SERIALIZATION_SPLIT_MEMBER()

 

Cette partie de code nous montre le template à définir. Il s’agit d’une fonction template load et save qui travaille sur une Archive. La syntaxe pour enroler une donnée dans l’archive se fait au travers de l’opérateur &. Cette partie de code enregistre le vecteur d’éléments. Mais pour que cela fonctionne, il faut aussi que le classe CSimpleShape enregistre ses éléments dans l’archive.

template<class Archive>

void save(Archive & ar, const unsigned int version) const

{

// ar & name;

// ar & id;

ar & BOOST_SERIALIZATION_NVP(m_name);

ar & BOOST_SERIALIZATION_NVP(m_id);

ar & BOOST_SERIALIZATION_NVP(m_rect);

ar & BOOST_SERIALIZATION_NVP(m_type);

ar & BOOST_SERIALIZATION_NVP(m_shapeType);

ar & BOOST_SERIALIZATION_NVP(m_caption);

ar & BOOST_SERIALIZATION_NVP(m_text);

ar & BOOST_SERIALIZATION_NVP(m_x1);

ar & BOOST_SERIALIZATION_NVP(m_y1);

ar & BOOST_SERIALIZATION_NVP(m_x2);

ar & BOOST_SERIALIZATION_NVP(m_y2);

ar & BOOST_SERIALIZATION_NVP(m_colorFillR);

ar & BOOST_SERIALIZATION_NVP(m_colorFillG);

ar & BOOST_SERIALIZATION_NVP(m_colorFillB);

ar & BOOST_SERIALIZATION_NVP(m_colorLineR);

ar & BOOST_SERIALIZATION_NVP(m_colorLineG);

ar & BOOST_SERIALIZATION_NVP(m_colorLineB);

ar & BOOST_SERIALIZATION_NVP(m_bSolidColorFill);

ar & BOOST_SERIALIZATION_NVP(m_bColorLine);

ar & BOOST_SERIALIZATION_NVP(m_bColorFill);

}

 

Le code du load et du save est le même. L’archive sait dans quel sens on travaille soit en lecture soit en écriture. Il faut noter que les MFC fournissent une infrastructure similaire avec les opérateurs << et >> ; je trouve que c’est plus lisible à mon goût mais bon. Pour pouvoir compiler ce code, il faut disposer des entêtes suivante :

#define BOOST_SERIALIZATION_DYN_LINK TRUE

#define BOOST_ALL_DYN_LINK TRUE

 

#include <boost/smart_ptr/shared_ptr.hpp>

#include <boost/archive/tmpdir.hpp>

#include <boost/serialization/nvp.hpp>

 

#include <boost/archive/xml_iarchive.hpp>

#include <boost/archive/xml_oarchive.hpp>

#include <boost/archive/text_iarchive.hpp>

#include <boost/archive/text_oarchive.hpp>

#include <boost/serialization/shared_ptr.hpp>

#include <boost/serialization/base_object.hpp>

#include <boost/serialization/string.hpp>

#include <boost/serialization/list.hpp>

#include <boost/serialization/vector.hpp>

#include <boost/serialization/map.hpp>

#include <boost/serialization/utility.hpp>

#include <boost/serialization/assume_abstract.hpp>

using namespace boost;

 

L’écriture des données

Maintenant que nous avons nos classes qui savent utiliser une Archive, voyons le code qui donne l’ordre de faire Load ou Save. Vous allez voir, c’est très simple :

boost::shared_ptr<CShapeCollection> data(new CShapeCollection());

for( vector<std::shared_ptr<CElement>>::iterator i = m_objects.m_objects.begin() ; i!=m_objects.m_objects.end() ; i++ )

{

std::shared_ptr<CElement> pElement = *i;

boost::shared_ptr<CSimpleShape> pNewElement(new CSimpleShape());

pNewElement->m_name = pElement->m_name;

pNewElement->m_id = pElement->m_objectId;

pNewElement->m_type = pElement->m_type;

pNewElement->m_shapeType = pElement->m_shapeType;

pNewElement->m_caption = pElement->m_caption;

pNewElement->m_text = pElement->m_text;

 

CPoint p1 = pElement->m_rect.TopLeft();

CPoint p2 = pElement->m_rect.BottomRight();

pNewElement->m_x1 = p1.x;

pNewElement->m_y1 = p1.y;

pNewElement->m_x2 = p2.x;

pNewElement->m_y2 = p2.y;

 

pNewElement->m_colorFillR = GetRValue(pElement->m_colorFill);

pNewElement->m_colorFillG = GetGValue(pElement->m_colorFill);

pNewElement->m_colorFillB = GetBValue(pElement->m_colorFill);

pNewElement->m_colorLineR = GetRValue(pElement->m_colorLine);

pNewElement->m_colorLineG = GetGValue(pElement->m_colorLine);

pNewElement->m_colorLineB = GetBValue(pElement->m_colorLine);

 

pNewElement->m_bSolidColorFill = pElement->m_bSolidColorFill;

pNewElement->m_bColorLine = pElement->m_bColorLine;

pNewElement->m_bColorFill = pElement->m_bColorFill;

 

data->m_shapes.push_back(pNewElement);

}

 

std::ofstream xofs(filename.c_str());

boost::archive::xml_oarchive xoa(xofs);

xoa << BOOST_SERIALIZATION_NVP(data);

 

La variable filename est remplie par l’ouverture d’une Common Dialog Windows SaveAs et toute la magie consiste à déclarer une xml_archive et à utiliser l’opérateur << pour sauver toutes nos données.

 

La lecture des données

La lecture des données est calquée sur l’écriture, on récupère les données et on les transvase dans la collection qui va s’afficher à l’écran :

 

boost::shared_ptr<CShapeCollection> data(new CShapeCollection());

// load an archive

std::ifstream xifs(filename.c_str());

assert(xifs.good());

boost::archive::xml_iarchive xia(xifs);

xia >> BOOST_SERIALIZATION_NVP(data);

// Clear existing shapes

m_objects.RemoveAll();

 

for( vector<boost::shared_ptr<CSimpleShape> >::iterator i = data->m_shapes.begin() ; i!=data->m_shapes.end() ; i++ )

{

boost::shared_ptr<CSimpleShape> pElement = *i;

//AfxMessageBox(pElement->m_name + ” ” + pElement->m_id);

 

std::shared_ptr<CElement> pNewElement = CFactory::CreateElementOfType((ElementType)pElement->m_type,

(ShapeType)pElement->m_shapeType);

pNewElement->m_name = pElement->m_name.c_str();

pNewElement->m_objectId = pElement->m_id.c_str();

pNewElement->m_caption = pElement->m_caption.c_str();

pNewElement->m_text = pElement->m_text.c_str();

pNewElement->m_pManager = this;

pNewElement->m_pView = pView;

 

CPoint p1;

CPoint p2;

p1.x = pElement->m_x1;

p1.y = pElement->m_y1;

p2.x = pElement->m_x2;

p2.y = pElement->m_y2;

pNewElement->m_rect = CRect(p1, p2);

 

int colorFillR = pElement->m_colorFillR;

int colorFillG = pElement->m_colorFillG;

int colorFillB = pElement->m_colorFillB;

pNewElement->m_colorFill = RGB(colorFillR, colorFillG, colorFillB);

int colorLineR = pElement->m_colorLineR;

int colorLineG = pElement->m_colorLineG;

int colorLineB = pElement->m_colorLineB;

pNewElement->m_colorLine = RGB(colorLineR, colorLineG, colorLineB);

 

pNewElement->m_bSolidColorFill = pElement->m_bSolidColorFill;

pNewElement->m_bColorLine = pElement->m_bColorLine;

pNewElement->m_bColorFill = pElement->m_bColorFill;

 

m_objects.AddTail(pNewElement);

pView->LogDebug(_T(“object created ->”) + pNewElement->ToString());

}

 

// Redraw the view

Invalidate(pView);

 

Conclusion

L’utilisation de Boost et de Boost.Serialization est plutôt simple à appréhender. La documentation fournie est de bonne facture et les exemples sont légions dans la documentation. L’avantage de Boost.Serialization est que les archives sont portables… Donc pour faire du multi-plateforme, c’est easy ! Le code complet de l’application de dessin est disponible ici : http://ultrafluid.codeplex.com .

 

Conclusion sur la série « Je Pratique C++ »

Avec cet article, la série « Je pratique le C++ » s’arrête mais je vais trouver un deal avec Programmez pour que nous ayons toujours du C++ dans Programmez. Tous les logiciels Microsoft sont faits en C/C++ à 90% et il ne serait pas normal de ne pas en parler. Le C++ n’est pas mort, loin de la. Avec C++11, le C++ à rajeuni et les standards C++14 et C++17 apportent leur lot de nouveautés. Stay tuned comme dirait l’autre et à Bientôt pour de nouveaux articles sur le C++.

Je pratique le C++ – Partie 4/5 – la STL

Je pratique le C++ – Partie 4/5.

Nous vous proposons une série d’articles sur la pratique de C++ pour que vous puissiez tous vous y mettre. Ce mois-ci on aborde la librairie standard du C++ nommé STL : Standard Template Library.

Le langage C possède sa runtime et le C++ possède aussi sa runtime, appelée STL. Dans le monde Microsoft, la runtime du C se nomme MSVCRTxxx.dll et celle du C++ se nomme MSVCPxxx.dll. Cependant, la STL ne se comporte pas comme une liste de fonctions que l’on peut appeler – exemple fopen en C. La STL est constituée de templates. SI vous avez lu l’article précédent 3/5, nous y avons étudié les templates en douceur. Cela veut dire plusieurs choses. La STL est organisée via des fichiers d’entêtes.

Les fonctionnalités offertes par la bibliothèque standard peuvent être classées comme suit :

• Support du run-time du langage (par exemple, pour l’allocation et les informations de type run-time).

• La bibliothèque C standard (avec des modifications vraiment mineures pour minimiser la violation du système de type).

• Les chaînes (avec le support des jeux de caractères internationaux et localisation).

• Support pour la correspondance des expressions régulières.

• Les flux d’entrée/sortie sont un framework extensible pour l’entrée et la sortie vers lesquels les utilisateurs peuvent ajouter leurs propres types, flux, stratégies de buffering, locales et jeux de caractères.

• Un framework de conteneurs (comme vector et map) et d’algorithmes (comme find(), sort(), et merge()). Ce framework, appelé conventionnellement la STL, est extensible pour que les utilisateurs puissent y ajouter leurs propres conteneurs et algorithmes.

• Support pour le calcul numérique (comme les fonctions mathématique standard, les nombres complexes, les vecteurs avec des opérations arithmétiques, et les générateurs de nombres aléatoires).

• Support pour la programmation parallèle, incluant les threads et les verrous. Le support parallèle est fondamental pour les utilisateurs puissent ajouter le support aux nouveaux modèles parallèles dans des bibliothèques.

• Des classes utilitaires pour le support de la méta programmation à base de templates (par exemple, type traits, la programmation générique avec le style STL (par exemple, pair), et la programmation générale (par exemple, clock).

• Les ‘‘Pointeurs Intelligent’’ pour la gestion des ressources (par exemple, unique_ptr et shared_ptr) et une interface pour la libération des ressources.

• Des conteneurs spéciaux, comme array, bitset, et tuple.

Le critère principal pour inclure une classe dans la bibliothèque étaient que:

• cela pourrait être utile à presque tous les programmeurs C++ (les novices ou les experts)

• cela pourrait être fournie sous une forme générale qui n’ajoute pas de surcharge importante par rapport à une version simplifiée de la même fonctionnalité, et

• que l’utilisation simple doit être simple à apprendre (par rapport à la complexité de la tâche).

La bibliothèque standard de C++ fournit essentiellement les structures de données fondamentales les plus courantes ainsi que les algorithmes fondamentaux à utiliser avec. Nous allons balayer tout ou partie de cette liste exhaustive.

Sélection des fichiers d’entêtes de la bibliothèque standard:
<algorithm> copy(), find(), sort()
<array> array
<chrono> duration, time_point
<cmath> sqrt(), pow()
<complex> complex, sqrt(), pow()
<forward_list> forward_list
<fstream> fstream, ifstream, ofstream
<future> future, promise
<ios> hex,dec,scientific,fixed,defaultfloat
<iostream> istream, ostream, cin, cout
<map> map, multimap
<memory> unique_ptr, shared_ptr, allocator
<random> default_random_engine, normal_distribution
<regex> regex, smatch
<string> string, basic_string
<set> set, multiset
<sstream> istrstream, ostrstream
<stdexcept> length_error, out_of_rang e, runtime_error
<thread> thread
<unordered_map> unordered_map, unordered_multimap
<utility> move(), swap(), pair
<vector> vector

Concepts généraux

La STL utilise l’espace de nom (namespace) std. Il est possible de s’en passer en faisant un using :

using namespace std;

Les fichiers d’entêtes de la STL sont nommés sans l’extension .h, exemple :

#include <iostream>

#include <sstream>

#include <fstream>

#include <string>

#include <vector>

#include <list>

#include <map>

#include <memory>

#include <array>

#include <utility>

#include <thread>

#include <algorithm>

using namespace std;

Le type string

Ce type est défini dans le fichier d’entête <string> et c’est le type qui permet de faire la liaison entre les type char du C et le type string d’un niveau plus haut qu’est le type string. Le type string permet de concaténer des string entre elles. Ce n’est pas un pointeur de char. C’est un template.

typedef basic_string<char, char_traits<char>, allocator<char> >

string;

Il est possible de fournir son propre allocateur mémoire. Mais qui va utilisez cela ? Et pourtant c’est prévu… Rappelez-vous dans l’article sur les classes lorsque je parlais de la casquette auteur de classe et de la casquette utilisateur de classe. On y est exactement sauf que c’est une classe template.

string msg = “Le nutty est coquine !”;

string msg2 = “Maggie est la reine des coquines !”;

string s = msg + ” ” + msg2;

cout << s << endl;

string nutty = s.substr(0, 9);

cout << nutty << endl;

msg[3] = toupper(msg[3]);

cout << msg << endl;

string temp(“VS 2015 is here !”);

size_t t = temp.find_first_of(“here”);

cout << temp << ” ” << t << endl;

La classe string contient de nombreuses opérations qui évitent le parcours des chaines C à l’ancienne. Pour passer d’une string à un type C char *, il faut utiliser la fonction c_str() :

const char * psz = msg.c_str();

printf(“String const char * = %sn”, psz);

Les expressions régulières (regex)

La fonctionnalité des regex est disponible dans le fichier d’entête <regex>. Le support est le même que dans d’autres langages ; on y trouve les fonctions regex_match, regex_search, regex_replace, regex_iterator, regex_token_ietrator.

Les IO/Streams

La STL utilise les flux pour gérer des I/O avec des buffers. La fonction la plus connue est cout et son opérateur ‘<<’.

cout << “On n’est pas que des Mickey !” << endl;

int i = 10;

float f = 2.5;

cout << i << “, ” << f << endl;

Le meilleur ami de cout est cin. Cela permet de capter les entrées du clavier :

int j = 0;

cout << “Give ma a Int !” << endl;

cin >> j;

cout << “Merci pour ” << j << endl;

En plus de gérer les types standards et les string, il est possible de tirer parti de la librairie pour afficher d’autres types. Exemple, considérons le type suivant :

struct CPersonne

{

int age;

string name;

};

ostream& operator<<(ostream& os, const CPersonne& p)

{

return os << p.name << ” ” << p.age << ” ans”;

}

void TestIO()

{

CPersonne p;

p.age = 5;

p.name = “Audrey Maggie”;

cout << “La soeur de Lisa c’est ” << p << endl;

}

La définition de l’opérateur << avec le type ostream permet de passer un type CPersonne à cout et cela sans forcer ! Autres aspects de la librairie I/O, c’est le formatage. Pour afficher des types comme le fait une fonction printf en C avec les %d, %f, %x et %s ont leur équivalent dans la librairie.

void TestFormating()

{

float f = 2.50;

cout << f << “;”

<< scientific << f << “;”

<< hexfloat << f << “;”

<< fixed << f << “;”

<< defaultfloat << f << endl;

}

La gestion des fichiers est assurée au travers du fichier d’entêtes <fstream> :

  • ifstream permet de lire un fichier
  • ofstream permet d’écrire un fichier
  • fstream permet de lire et d’écrire dans un fichier

Exemple d’écriture de fichier :

ofstream ofs(“c:\temp\MyGirls.txt”);

ofs << “Edith” << endl;

ofs << “Lisa” << endl;

ofs << “Maggie” << endl;

ofs.close();

C’est vraiment très simple. La librairie I/O permet de gérer les buffers. Le fichier d’entête <sstream> permet de manipuler des chaînes :

  • istringstream pour lire des chaînes
  • ostringstream pour écrire des chaînes
  • stringstream pour lire et écrire des chaînes

int i = 20;

float f = 5.75;

string s = “Maggie est trop coquine !”;

ostringstream oss;

oss << i << “, ” << f << “, ” << s;

string str = oss.str();

cout << str << endl;

Les containers

Le container le plus utilisé est vector<T>. Il est disponible dans le fichier d’entête <vector>. C’est une séquence d’éléments d’un type donné. Les éléments sont stockés de manière contiguë. Pour ajouter des éléments à un vector, il faut utiliser la méthode push_back(). Le parcours d’un vector peut se faire avec un range-for ou bien en utilisant un itérateur. Les containers de la STL sont tous accessibles au travers un itérateur. Les fonctions begin(), end(), operator++, operator—m operator* permettent de manipuler un itérateur. Exemple :

vector<CPersonne> v = { {12, “Edith”}, {9, “Lisa”}, {5, “Maggie”} };

CPersonne p;

p.age = 41;

p.name = “Papa”;

v.push_back(p);

for (auto item : v)

{

cout << item.age << ” ” << item.name << endl;

}

for (vector<CPersonne>::const_iterator it = begin(v); it != end(v); ++it)

{

CPersonne p = *it;

cout << p.age << ” ” << p.name << endl;

}

Voici la liste des opérations principales pour vector<T> :

Opération Explication
v.empty() Retourne true si v est vide. Sinon retourne false.
v.size() Retourne le numbre d’élements dans v.
v.push_back(t) Ajoute un élément de valeur t à la fin de v.
v[n] Retourne une référence vers l’élement en position n dans v.
v1=v2 Remplace les éléments dans v1 avec une copie des éléments dans v2.
v1={a, b, c…} Remplace les éléments dans v1 avec une copie des éléments de la liste.
v1==v2 v1 et v2 sont égaux si il ya le même nombre d’élements et de valeur.
v1!=v2 opposé de v1==v2
<, <=, >, >= Suivant l’ordre des valeurs retourne un bool

Il existe un container qui représente une double liste chaînées au travers de list. Il est disponible au travers le fichier d’entête <list>.

list<CPersonne> l = { { 12, “Edith” },{ 9, “Lisa” },{ 5, “Maggie” } };

CPersonne p = { 41, “Papa” };

l.push_front(p);

CPersonne p2 = { 40, “Maman” };

l.push_back(p2);

for (auto item : l)

{

cout << item.age << ” ” << item.name << endl;

}

Le container map<K,V> est très utile. Il est disponible dans le fichier d’entête <map>. C’est un container associatif.

map<string, int> family = { { “Edith”, 12 },{ “Lisa”, 9 },{ “Maggie”, 5 } };

family[“Papa”] = 41;

for (map<string, int>::const_iterator it = begin(family); it != end(family); ++it)

{

string name = it->first;

int age = it->second;

cout << name << ” ” << age << endl;

}

Il existe d’autres containers dans la STL comme la hashtable nommée unordered_map. Le container hashtable et ses dérivées (voir tableau ci-dessous) ne contiennent pas le terme hashtable pour des soucis de nommage. Il y a surement du code existant qui ont fait une classe ou un temlplate hashtable et c’est pour éviter la collision de nom.

Les containers disponibles dans la STL
vector<T> un vecteur de taille variable
list<T> une liste doublement chaînée
forward_list<T> une liste chaînée
deque<T> une queue
set<T> un set (une map avec une clé sans valeur)
multiset<T> un set qui peut être en doublon
map<K,V> un tableau associative
multimap<K,V> une map avec une clé qui peut être en doublon
unordered_map<K,V> une map qui utilise un lookup hashtable
unordered_multimap<K,V> une multimap qui utilise un lookup hashtable
unordered_set<K,V> un set qui utilise un lookup hashtable
unordered_multiset<K,V> un multiset qui utilise un lookup hashtable

Les algorithmes

La STL fournit des fonctions simple pour parcourir des ensembles, faire des copies, des insertions, des suppressions, des recherches simple ou complexes. Le fichier d’entête est <algorithm>. La force des algorithmes réside dans le fait qu’elles prennent pour la plupart un itérateur de début et un itérateur de fin afin de réaliser le parcours sur un ensemble fini. C’est un peu déroutant au début et puis finalement, on s’aperçoit que la plupart des parcours proposé dans ce fichier d’entêtes sont bien fait. Il faut maitriser les itérateurs : c’est la seule contrainte. Dans le tableau ci-dessous, b vaut begin() et e vaut end().

Sélection d’algorithmes dans la STL
p=find(b,e ,x) p est le premier p dans [b:e) de telle manière que
p==x

p=find_if(b,e ,f)

p est le premier p dans [b:e) de telle manière que f(p)==true

n=count(b,e ,x)

n est le nombre d’éléments
q dans [b:e) de telle manière
que
q==x

n=count_if(b,e ,f)

n est le nombre d’éléments
q dans [b:e) de telle manière
que f(
q,x)

replace(b,e ,v,v2)

Remplace les éléments
q dans [b:e) de telle manière
que
q==v par v2

replace_if(b,e ,f,v2)

Remplace les éléments
q dans [b:e) de telle manière
que f(
q) par v2

p=copy(b,e ,out)

Copie [b:e) dans [out:p)

p=copy_if(b,e ,out,f)

Copie les éléments
q de [b:e) de telle manière
que f(
q) jusqu’a [out:p)

p=move(b,e ,out)

Déplace [b:e) vers [out:p)

p=unique_copy(b,e ,out)

Copie [b:e) vers [out:p); ne copie pas les duplicates adjacent

sort(b,e)

Tri les éléments de [b:e) en utilisant < comme critère de tri

sort(b,e,f)

Tri les éléments de [b:e) en utilisant la fonction de tri f

(p1,p2)=equal_range(b,e ,v)

[p1:p2) est la subséquence de tri [b:e) avec la valeur v; un binary search de v

p=merge(b,e ,b2,e2,out)

Merge deux séquences [b:e) et [b2:e2) dans [out:p)

Exemple avec find :


vector<string>
v = {
“Edith”,
“Lisa”,
“Maggie” };


//auto res = find(begin(v), end(v), “Maggie”);


vector<string>::iterator
res = find(begin(v), end(v),
“Maggie”);


if (res
== end(v))

{

cout
<<
“Not found !”
<< endl;

}


else

{

cout
<<
“Found ! “
<<
*res
<< endl;


}

Les templates utilities

Tout dans la STL n’est pas aussi simple que les containers ou la librairie I/O stream. Il existe des classes templates qui permettent de tirer parti de fonctionnalités avancées.

Considérons la gestion des ressources, il existe deux templates que sont unique_ptr<T> et share_ptr<T>. unique_ptr<T> représente la possession unique. shared_ptr<T> représente la possession partagée.
Disponible dans le fichier d’entête <memory>, ces « smart pointers » ou pointeurs intelligents permettent de ne plus coder le delete, c’est le template qui s’en occupe. Le principal avantage d’utiliser les smart pointers est d’éviter les fuites mémoire.


unique_ptr<CPersonne>
ptr(
new
CPersonne());


ptr->age
= 41;

ptr->name
=
“Itchy”;


// use ptr


// delete fait automatiquement

Voici la liste des opérations communes entre unique_ptr<T> et shared_ptr<T> :

Opération

Explication

shared_ptr<T> sp

Smart pointer null qui pointe sur un objet T

unique_ptr<T> up

Smart pointer null qui pointe sur un objet T

p

Utilise p comme une condition; true si p pointe sur un objet

*p

Déreference p pour obtenir l’objet sur lequel p pointe

p->mem

Synonyme pour (*p).mem

p.get()

Retourne le pointeur dans p.

swap(p,q)

Swap les pointeurs dans p et q

p.swap(q)

Swap les pointeurs dans p et q

Le template share_ptr<T> possède quelques subtilités :

Opération

Explication

make_shared<T>(args)

Retourne un shared_ptr sur la mémoire allouée et initialise l’objet via args

shared_ptr<T> p(q)

p est une copie de q. Incrémente le compteur de référence interne

p=q

Incrémente le compteur de référence de q

p.use_count()

Retourne le nombre d’objets partagés avec p

p.unique()

Retourne true si p.use_count vaut 1 sinon false

Le template array<T, C> permet de gérer les tableaux aussi rapidement que les built-in arrays.


array<string,
3> ar;

ar[0] = “Edith”;

ar[1] = “Lisa”;

ar[2] = “Audrey”;

for (auto element : ar)

{

cout << element << endl;

}

Le template pair<T, U> représente deux éléments et est disponible dans le fichier d’entête <utility>. Utilisez make_pair pour remplir l’objet pair.

pair<string, float> p;

p.first = “The C++ Object Model”;

p.second = 50.0;

cout << p.first << ” ” << p.second << endl;

pair<string, string> p2 = make_pair(“Maggy”, “t’es une coquine !”);

cout << p2.first << ” ” << p2.second << endl;

Le template tuple<T…> représente une séquence de types variés et de types différents. Utilisez make_tuple pour remplir l’object tuple.

tuple<string, float, string> t;

t = make_tuple(“C++ Primer”, 50.0, “The best of all books !”);

cout << get<0>(t) << “;” << get<1>(t) << “;” << get<2>(t) << endl;

Concurrency : le multithreading !

Il est possible de lancer une tâche en parallèle et d’en attendre la fin. On va utiliser la classe thread disponible dans le fichier d’entête <thread>. Il suffit de passer une routine en argument du constructeur de la classe thread. La méthode join() sur l’objet thread permet d’en attendre la fin.

void ThreadFunc()

{

// ../..

}

void TestThread()

{

thread t(ThreadFunc);

std:thread::id id = t.get_id();

t.join();

cout << “TestThread: TID:” << id << “, ” << “Main:” << GetCurrentThreadId() << endl;

}

Dans l’exemple ci-dessus, la fonction du thread ne prend pas de paramètres. Cependant, dans certains cas, on veut pouvoir passer des paramètres au thread. Il suffit de passer les paramètres au constructeur de l’objet thread :

class Param

{

public:

string s;

int i;

float f;

};

void ThreadFunc2(Param param)

{

cout << param.s << “, ” << param.i << “, ” << param.f << endl;

// ../..

}

void TestThreadWithParam()

{

Param param = { “C++”, 14, 50.0 };

thread t(ThreadFunc2, param);

t.join();

}

Il existe des classes de type verrou comme mutex ou unique_lock<T> pour protéger l’accès aux données partagées. Vous pouvez remarquer que la gestion des threads est plutôt simple à utiliser.

Conclusion

Nous avons balayé les différentes composantes de la librairie standard et le constat est le suivant : le code source de la STL est complexe car c’est un framework extensible ; on l’a vu sur iostream et le passage d’une structure à cout ; qu’il faut apprendre à maîtriser. A partir des exemples simples présents dans cet article, vous n’avez plus qu’à vous lancer. Il y a des domaines que je n’ai pas couvert et c’est volontaire mais sachez que cet article couvre 90% de la STL. L’aspect le plus important est la maîtrise des containers. Ne construisez plus vos propres structures de list, hashtable ou autres, utilisez la STL ! La seule utilisation de vector<T> vous fait rentrer de plein pied dans la STL. Vous aurez des performances inégalables. N’utilisez plus les delete et passez aux smart pointers ! unique_ptr<T> et shared_ptr<T> justifient ; comme vector<T> ; une utilisation systématique. La fin des memory leak (fuite mémoire) en est une autre justification. Un conseil : laissez le code un peu ancien (legacy) avec un style à la papa, et sur le nouveau code, déchainez les enfers avec de la STL partout… J Tout ça est à prendre avec beaucoup de modération et de sagesse mais je suis certain que vous me comprenez. Le nouveau code doit être codé avec tous les artifices du C++ 11. Le code est plus lisible, les performances sont au rendez-vous. Vous n’avez plus aucunes excuses pour ne pas essayer ; à vous de jouer. C’est « power & performance » comme disait Herb Sutter, chairman du standard C++ ISO.

Je pratique le C++ – Partie 3/5 – les templates

Je pratique le C++ – Partie 3/5.

Nous vous proposons une série d’articles sur la pratique de C++ pour que vous puissiez tous vous y mettre. Ce mois-ci on aborde les templates C++. Les templates représentent la partie programmation générique du langage C++.

 

La programmation orientée objet (OOP) et la programmation générique travaillent avec des types qui ne sont pas connus au moment ou le programme est écrit. La distinction entre les deux est que OOP travaille avec des types connu qu’au runtime tandis que la programmation générique utilise des types qui sont connus à la compilation.

Les containers, les itérateurs et les algorithmes sont des exemples de programmation générique. Quand on écrit un programme générique, le code est indépendant des types qu’il manipule. Quand on utilise un programme générique, nous fournissons les types ou les valeurs sur l’instance du programme qui va tourner.

Par exemple, la librairie standard fournit une simple définition générique de chaque container, comme vector. Nous pouvons utiliser la définition générique pour définir plusieurs types de vector, chacun étant différent des autres suivant le type qu’il contient. Les templates sont les fondations de la programmation générique en C++. Nous pouvons les utiliser sans comprendre comment ils sont définis. Un template est une « formule » pour créer des classes ou des fonctions. Lorsque nous utilisons un type générique comme vector, ou une fonction générique comme find, on fournit les informations de type nécessaire à l’exécution de cette classe ou fonction à la déclaration. Exemple : vector<string> v.

 

Concepts et programmation générique

Pourquoi les templates sont-ils fait ? Quelles techniques de programmation sont mises en œuvre quand on utilise les templates ? Les templates offrent :

  • La possibilité de passer des types (des valeurs et des templates) en tant qu’arguments sans perte d’information.
  • La vérification de type faite à l’instanciation.
  • La possibilité de passer des valeurs constantes comme arguments. Cela implique de faire du calcul à la compilation.

Les templates fournissent un mécanisme puissant de compile-time computation et de manipulation de type qui permettent d’avoir un code efficace et très compact. Rappelons-nous que les types (classes) peuvent contenir du code et des valeurs. Le premier et le plus commun scenario d’utilisation des templates est le support de la programmation générique, qui est, un modèle de programmation qui met l’accent sur le design, l’implémentation et l’utilisation d’algorithmes général. Ici, « général » veut dire qu’un algorithme peut être conçu pour accepter un large panel de types tant qu’ils sont passés en arguments. Les templates sont (compile-time) un mécanisme de polymorphisme paramétrique.

Considérez la fonction sum :

 

template<typename Container, typename Value>

Value sum(const Container& c, Value v)

{ for ( auto x : c ) v+=x;

return v;

}

Elle peut être invoquée pour n’importe quelle structure de données qui supporte begin() et end() de tel manière que le range-for puisse s’exécuter. De telles structures de la librairie standard (Standard Template Library) comme vector, list et map font l’affaire. Pour aller plus loin, le type d’élément de cette structure de données est seulement limité par son utilisation : elle doit être un type auquel on peut ajouter un argument Value. Les exemples sont ints, doubles et Matrices. On peut dire que l’algorithme sum() est générique dans 2 dimensions : le type de data structure pour stocker les éléments (le container) et le type de ses éléments. Donc, sum() requiert que le premier argument soit une sorte de container et le second argument de template soit une sorte de nombre. De tels prérequis se nomment des concepts. De bon et précieux concepts sont fondamentaux pour la programmation générique. Les exemples sont les entiers et les nombres à virgules flottantes (comme définis même en C classique) et plus généralement des concepts mathématiques comme les vector et les containers. Ils représentent les concepts fondamentaux pour un champ d’applications. L’identification et la formalisation à un degré nécessaire pour une programmation générique efficace peut être un challenge. Pour une utilisation basique, considérez le concept Régulier. Un type est régulier lorsqu’il se comporte comme un int ou un vector.

Un objet de type régulier :

  • Peut être construit par défaut
  • Peut être copié en utilisant un constructeur ou une affectation
  • Peut être comparé en utilisant == et !=
  • Ne souffre pas de problème technique à l’utilisation

 

Une string est un autre exemple de type régulier. Comme int, string est aussi Ordonné. Cela veut dire que 2 strings peuvent être comparées avec <, <=, >, >= avec les sémantiques appropriées. Les concepts ce n’est pas juste une notion syntaxique, c’est fondamentalement de la sémantique.

 

Les fonctions templates

Imaginons une fonction compare qui fonctionne avec tous les types, comment allons-nous écrire cela :

template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}

Le return 0 est juste là pour faire jolie car il faut finir la fonction et retourner quelque chose…

C’est une simple fonction et qui prend deux types T en paramètre. Remarquez l’élégance du style. Dans une définition de template, la liste des paramètres de template ne peut être vide. Une définition de template commence avec le mot-clé template suivi d’une liste de paramètre de template qui sont séparés par des virgules et qui sont entre les token < et >.

 

Instanciation d’une fonction template

Quand on appelle une fonction template, le compilateur utilise les arguments de l’appel pour déduire les arguments de template pour nous. Lors de l’’appel  à compare, le compilateur utilise le type des arguments pour déterminer quel type associer au paramètre de template T.

 

cout << compare(1, 0) << endl;       // T est int

vector<int> vec1{1, 2, 3}, vec2{4, 5, 6};
cout << compare(vec1, vec2) << endl; // T est vector<int>

 

Pour le premier appel, le compilateur va écrire et compiler une version de compare avec T remplacé par int :

int compare(const int &v1, const int &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}

Pour le second appel la fonction sera générée avec des vector<int>.

 

Les paramètres de type template

Il est possible d’utiliser le paramètre de type template comme les arguments de la fonction. Exemple :

template <typename T> T foo(T* p)
{
T tmp = *p;
// …
return tmp;
}

Chaque paramètre de type doit être précédé du mot clé class ou typename.

 

template <typename T, U> T calc(const T&, const U&);

template <typename T, class U> calc (const T&, const U&);

 

 

La compilation des templates

Quand le compilateur voit la définition d’un template, il ne génère pas de code. Il génère du code seulement quand on instancie une instance spécifique d’un template. Le fait que le code soit généré seulement quand on utilise le template (et non quand on le définit) affecte la manière avec laquelle nous organisons le source code et la manière avec laquelle les erreurs sont détectés. Il en va aussi de la taille de l’exe ou de la librairie lib ou dll.

 

Quand on appelle une fonction, le compilateur a besoin de voir seulement la déclaration de la fonction. Quand on utilise un objet de type de class, la définition de classe doit être disponible mais la définition des fonctions membres n’a pas besoin d’être présent. Donc, on met les définitions de classes et les déclarations de fonctions dans des fichiers d’entêtes (.h) et les définitions des fonctions membres de classes dans le source code (.cpp).

 

Les templates sont différents. Pour générer une instanciation, le compilateur a besoin d’avoir le code qui définit une fonction template ou une fonction membre de classe template. Donc, les entêtes (.h) pour les templates contiennent leurs définitions et leurs déclarations et leurs fonctions membres.

 

Les erreurs de compilation des templates

La détection des erreurs sur les template peut être parfois un véritable parcours du combattant car le compilateur check plusieurs choses lors de l’instanciation du template. Une des erreurs les plus fréquentes est celle rencontrée sur les types. Exemple : un template requiert une opération cout <<. La compilation ne détecte pas d’erreur tant que l’instanciation sur un type n’est pas effectuée. Si le type ne supporte pas l’opérateur << pour iostream cout sur l’objet passé dans le paramètre, l’erreur est immédiatement détectée.

Exemple :

template

 

Erreur de compilation : binary ‘<<‘: no operator found which takes a right-hand operand of type ‘const std::vector<std::string,std::allocator<_Ty>>’ (or there is no acceptable conversion)

 

Dans notre exemple, il n’existe aucun opérateur << défini dans vector<T> qui puisse travailler avec la fonction cout définie dans iostream de la STL. La fonction Echo ne fait que un cout << mais ce n’est pas possible sur vector<T>. C’était possible sur string et sur int mais pas sur vector<T>.

Une bonne pratique consiste à essayer de minimiser le nombre de prérequis demandés sur le type d’argument.

C’est au designer du template de vérifier que les arguments passés au template supportent toutes les opérations que le template utilise et que ces opérations se comportent correctement dans le contexte que le template utilise.

 

Les templates de classes

Un template de classe sert à générer des classes. Les templates de classes diffèrent des templates de fonctions dans le fait que le compilateur ne peut pas déduire les types de paramètres du template pour un template de classe. Pour utiliser un template de classes, nous devons fournir une information additionnelle dans les token ( < et >) juste après le nom du template. Les informations en extra sont la liste des arguments du template à utiliser pour ce template.

 

Définir un template de classe 

Comme les templates de fonctions, les templates de classes commencent par le mot-clé template suivi d’une liste de paramètres du template. Voici un template pour la gestion des pointeurs. Plus besoin de faire de delete, le template s’en charge.

 

template<typename T>

class MyPtr

{

public:

MyPtr()

{

m_count = 0;

m_ptr = nullptr;

cout << “No Pointer catched” << endl;

}

 

MyPtr(T* ptr) : m_ptr(ptr)

{

m_count = 0;

m_count++;

cout << “Pointer catched” << endl;

}

 

virtual ~MyPtr()

{

m_count–;

if (m_count == 0)

{

delete m_ptr;

cout << “Pointer deleted !” << endl;

}

}

 

T& operator*(void)

{

return *m_ptr;

}

 

T* operator->(void)

{

return m_ptr;

}

 

MyPtr& operator=(MyPtr<T> &ptr)

{

if ( m_ptr != nullptr)

delete m_ptr;

m_ptr = ptr.m_ptr;

m_count++;

return *this;

}

 

MyPtr& operator=(T* ptr)

{

if (m_ptr != nullptr)

delete m_ptr;

m_ptr = ptr;

m_count++;

return *this;

}

 

private:

T * m_ptr;

int m_count;

};

 

Le template MyPtr possède un type de paramètre template nommé T. Nous pouvons utiliser ce paramètre n’importe où pour représenter le type que MyPtr détient. Par exemple, nous définissons le type de retour d’une opération qui fournit accès à l’élément de MyPtr comme T&. Quand un développeur va instancier ce template, les utilisations de T seront remplacé par un argument de type template spécifique.

 

Instanciation du template de classe

Pour instancier le template de classe, il faut fournir explicitement un type de paramètre au template comme, par exemple, une classe CElement qui est une classe fictive pour désigner des éléments à dessiner:

 

MyPtr<CElement> pElement(new CElement(10));

pElement->Draw();

 

Il est possible d’avoir en données membre un stockage des éléments dans vecteur par exemple en utilisant le paramètre de type du template :

 

std::shared_ptr<std::vector<T>> data;

Le type T est type comme les autres donc on peut l’utiliser comme on veut, en T, en T& en T*. Toutes les combinaisons sont possibles.

 

Les fonctions membres des templates de classe

Les fonctions membres peuvent être définies dans le header ou dans le corps. Si elle le sont hors du header, il faut spécifier template<T> et le nom de la classe suivi de :: et du nom de la méthode.

Exemple :

Dans le header :

MyPtr& operator=(MyPtr<T> &ptr);

Dans le cpp :

 

template<typename T>

MyPtr<T>& MyPtr<T>::operator=(MyPtr<T> &ptr)

{

if (m_ptr != nullptr)

delete m_ptr;

m_ptr = ptr.m_ptr;

m_count++;

return *this;

}

 

Dans le template MyPtr<T>, on distingue plusieurs subtilités. Les opérateurs * et -> ont été redéfini pour une utilisation transparente du mécanisme des smart pointeurs (pointeurs intelligents).

 

Membres static et les templates

Il est possible de mettre des membres static dans les templates de classes.

template <typename T> class Foo {
public:
static std::size_t count() { return ctr; }
private:
static std::size_t ctr;
};

Tous les objets de type Foo<T> partagent les mêmes données static.

 

// instantiation des membres static Foo<string>::ctr and Foo<string>::count
Foo<string> fs;
// tous les 3 objets partage les membres Foo<int>::ctr and Foo<int>::count
Foo<int> fi, fi2, fi3;

Templates et arguments par défaut

Il est possible de passer des types par défaut pour les paramètres de type d’un template.

template <typename T, typename F = less<T>>

Ici le deuxième paramètre du template est par défaut et c’est less<T>. La fonction less de la STL prend deux paramètres (x et y) et retourne x<y.

 

Spécialisation de template

Dans certains cas précis, on est obligé de demander un fonctionnement particulier du template pour un type de paramètre précis. Prenons un exemple :

 

template <typename T>

struct Wrap

{

typename

static const T& MakeWrap(const T& t)

{

return t;

}

static const T& Unwrap(const T& t)

{

return t;

}

};

Ce template Wrap<T> représente un wrapper universel. Par défaut MakeWrap et Unwrap retourne T et le typedef type aussi retourne un T. Pour l’ensemble des types simples, ce wrapper fonctionne mais pour les type HSTRING et les pointers, il faut wrapper cela autrement. Ce développement est spécifique Windows 8.

 

 

template

Le type HSTRING ne peut pas être manipulé tel-que car c’est comme un HANDLE. C’est la raison pour la laquelle HSTRING est traité avec une classe HStringHelper et type devient HStringHelper Il en va de même pour les pointeurs qui doivent être encapsulés dans des ComPtr<T> car ce sont des composants COM. Le type est ComPtr<T> car les pointeurs sont des interfaces COM et il faut les encapsuler pour les utiliser. Oui c’est du développement Windows mais l’essentiel c’est de remarquer la spécialisation du template Wrap<T> et ses variantes Wrap<HSTRING> et Wrap<T*>.

 

Le concept clé c’est que les règles standard s’appliquent aux spécialisations. Pour spécialiser un template, une déclaration du template original doit être accessible. Il en va de même pour le template spécialisé avant son utilisation.

 

Avec des classes ou des fonctions ordinaires, les déclarations manquantes sont faciles à trouver. Pour les templates, c’est souvent plus compliquer. Surtout si la spécialisation du template n’est pas contenue dans le même fichier d’entête que le template principal. Les templates et les templates spécialisés doivent être déclaré dans le même fichier d’entête. En premier lieu, on définit le template original et ensuite les templates spécialisés.

 

Facilités de déclaration dans les templates

 

Dans certains cas, le typedef sur un type va permettre une meilleure lisibilité du code. Exemple :

template <class T>

class Iterator : public RuntimeClass<IIterator<T>>

{

InspectableClass(L”Library1.Iterator”, BaseTrust)

 

private:

 

typedef typename std::shared_ptr<WrappedVector> V;

typedef typename WrappedVector::iterator IT;

 

../..

private:

V _v;

IT _it;

T _element;

boolean _bElement = FALSE;

};

 

Ce template Iterator<T> contient un WrappedVector qui est un typedef sur vector<typename Wrap<T> ::type>. Les fait d’utiliser Wrap<T>::type fait la magie de ce template. Pour des types simples, T vaut T et pour HSTRING et T*, ça vaudra HStringHelper et ComPtr<T>. Ce mécanisme est très puissant.

Dans cet exemple, le template Iterator<T> représente un itérateur sous Windows 8 en mode Windows Store. Oui je sais c’est du Windows mais c’est le style qui compte. Regardez les typedef, ils utilisent Wrap<T> dans un vector<T> et l’itérateur du vector est défini en IT, plus simple à écrire.

 

La déclaration de V est un shared_ptr de vector<T> avec T qui vaut Wrap<T>. C’est assez complexe mais cela veut dire que c’est un container générique d’objets wrappés qui peut contenir à la fois des types simples mais aussi des HSTRING et aussi des pointeurs d’interfaces T* qui sera géré comme des pointeurs d’interfaces COM donc encapsulé avec la spécialisation de Wrap<T*> via ComPtr<T>.

 

Regardons comment est définie la méthode GetCurrent pour cette classe itérateur.

 

virtual HRESULT STDMETHODCALLTYPE get_Current(T *current)

{

try {

_LogInfo(L”Iterator::get_Current()…”);

if (_it != _v->end())

{

_bElement = TRUE;

_element = Wrap<T>::Unwrap(*_it);

*current = _element;

}

else

{

_bElement = FALSE;

}

return S_OK;

_EXCEPTION_HANDLER(L”Iterator::get_Current()…”);

}

 

GetCurrent doit retourner l’élément courant ; on check l’itérateur pour savoir si on est à la fin ou pas. On Unwrap l’élément et on le retourne dans le paramètre de la fonction. Le booléen _bElement est positionné pour avoir une information en double sur le statut de position de l’itérateur. Il est utile pour une autre méthode du template pour avoir s’il existe un élément courant.

 

Vous allez me dire, OK on déclare un itérateur mais où est la classe du container qui utilise cet itérateur. Voici la classe Vector<T> spécifique pour Windows 8 qui permet de gérer un container générique en prenant soin des types simples et des types complexes (HSTRING, pointeurs d’interface COM) avec Wrap<T>.

 

template <typename T>

class Vector : public RuntimeClass<IVector<T>,

IIterable<T>,

IObservableVector<T>>

{

InspectableClass(L”Library1.Vector”, BaseTrust)

 

private:

typedef typename std::vector<typename Wrap<T>::type> WrappedVector;

typedef typename WrappedVector::const_iterator CIT;

typedef typename VectorChangedEventHandler<T> WFC_Handler;

 

public:

Vector()

{

_LogInfo(L”Vector::Vector()…”);

_v = std::make_shared<WrappedVector>();

m_observed = false;

}

public:

virtual HRESULT STDMETHODCALLTYPE GetAt(unsigned index, T *item)

{

_LogInfo(L”Vector::GetAt()…”);

*item = Wrap<T>::Unwrap((*_v)[index]);

return S_OK;

}

 

../..

virtual HRESULT STDMETHODCALLTYPE First(IIterator<T> **first)

{

_LogInfo(L”Vector::First()…”);

ComPtr<Iterator<T>> p = Make<Iterator<T>>();

p->Init(_v);

*first = p.Detach();

return S_OK;

}

../..

private:

std::shared_ptr<WrappedVector> _v;

bool m_observed;

EventSource<VectorChangedEventHandler<T>> m_events;

};

 

Pour simplifier la compréhension de ce template Vector<T>, je n’ai fait figurer que la méthode GetAt qui permet de récupérer un élément et la méthode First qui retourne un itérateur sur le Vector<T> via Iterator<T>, template que nous avons découvert précédemment. Le template Vector<T> hérite de plusieurs classes qui sont aussi des templates.

 

Conclusion

Les templates sont des classes ou des fonctions qui permettent de générer des classes. Leur utilisation nécessite un peu de pratique mais c’est quelque chose d’appréhensible avec un peu d’effort. La puissance des templates résident dans la possibilité de définir une classe générique et de prévoir les adaptations nécessaires pour qu’elle puisse être utilisée avec le minimum de contrainte. La spécialisation partielle des templates est un formidable mécanique pour corriger les type un peu limité ou trop pénible à gérer. Dans ces opérations, le typedef est ton ami. On déclare un T et puis avec son utilisation on se rend compte que T est trop simple et qu’il faut wrapper le T dans certains cas. C’est cool. Cet article vous permet de découvrir ce que sont les templates. Cela peut paraître compliquer mais avec un peu de pratique, on est complètement aspiré et tout devient limpide. Il faut toujours se mettre en situation avec la casquette du designer du template et changer de temps en temps en tant qu’utilisateur du template. La découverte des erreurs de compilations les plus complexes se fera via le code d’utilisation des templates. Enjoy ! Le code source Windows 8 autour de Vector<T>, Iterator<T>, IIterable<T> et Wrap<T> est accessible ici : http://code.msdn.microsoft.com/windowsapps/Windows-Runtime-Component-4dc6fa20

Je pratique le C++ – Partie 2/5 – les classes

Je pratique le C++ – Partie 2/5.

Nous vous proposons une série d’articles sur la pratique de C++ pour que vous puissiez tous vous y mettre. En C++, nous utilisons les classes pour définir nos propres types de données.

Cet article va vous donner une idée du support de l’abstraction et de la gestion des ressources en C++. Comment définir des nouveaux types définis par l’utilisateur et aussi les propriétés basic, les techniques d’implémentation et les possibilités du langage pour les classes concrètes, les classes abstraites et les hiérarchies de classes. Le langage supporte le style de programmation orientée objet et de programmation générique (avec les templates).

La fonctionnalité principale du langage C++ est la classe. Une classe est un type défini par l’utilisateur qui permet de représenter un concept dans le code d’un programme.

N’importe quel design d’un programme possède des concepts, des idées, des entités, etc, que nous essayons de traduire en classe de telle manière que la lisibilité, la maintenance et l’évolution du programme en soit améliorée. Un programme est un ensemble de classes définies par l’utilisateur pour faire un taf bien précis. Les librairies sont des ensembles de classes mis à disposition.

On distingue deux catégories de développeur : celui qui fait les classes et celui qui les utilise. L’approche est complètement différente. La plupart des techniques de programmation évoquent comment designer et implémenter des types de classes. Il existe 3 sortes de classes : les classes concrètes, les classes abstraites et les classes dans les hiérarchies de classes.

Nous allons nous attacher à l’importance de l’abstraction des données, ce qui permet de séparer l’implémentation d’un objet des opérations que cet objet peut effectuer. Les objets peuvent être copiés, déplacés ou détruits. Il est même possible de définir ses propres opérateurs. Les idées fondamentales derrières les classes sont l’abstraction des données et l’encapsulation. L’abstraction des données est une technique de programmation qui se base sur la séparation de l’interface et de l’implémentation. L’interface d’une classe présente les opérations qu’un utilisateur de la classe peut exécuter. L’implémentation contient les données membres de la classe, le corps des fonctions présentes dans l’interface et toutes les fonctions internes à la classe pour faire son job. L’encapsulation permet la séparation de l’interface de classe de son implémentation. Une classe cache son implémentation et les utilisateurs de cette classe n’ont pas accès (des fois) à son implémentation. C’est le mécanisme des librairies dans lequel on ne possède que le point .h (header) de la classe et l’implémentation est fourni sous forme de binaire (.dll).

Une classe possède un header ; c’est un fichier d’entête .h et un corps, c’est un fichier cpp. Par convention, il est possible que les headers soit déposés dans un répertoire INCLUDE et les fichiers CPP dans un dossier SRC. Vous allez me dire « ouaip mais en java ou en C#, on met tout dans la classe et puis c’est tout ! ». Oui c’est vrai mais en C++ on ne fait pas comme ça.

Examinons une classe de log toute simple au travers de son header. La classe est préfixée du nom de l’entreprise pour laquelle elle a été faite (Cerius) en 1995. Cette classe utilise un type CString. C’est un type qui provient d’une librarie très connue faite par Microsoft qui se nomme MFC (Microsoft Foundation Classes).

class CerLog

{

public:

CerLog(const CString& path);

~CerLog();

BOOL Log(ORB_REQUETE * pReq);

private:

CString m_path; // Répertoire du log

static long m_stCompt;

};

Cette classe est dans un fichier CerLog.h qui est dans le répertoire INCLUDE. Que remarque t-on ? Elle possède un constructeur qui porte le nom de la classe. Ce constructeur sera appelé dès la création d’un objet. Le ctor prend une chaîne de caractères en paramètre. C’est obligatoire ; il n’y a pas de ctor vide. La première partie de la classe est dans un bloc public mais sur la fin on voit du private ce qui implique que ce sont des données membres que l’on ne pourra pas utiliser. C’est réservé à la classe. Donc, dans cette classe, on a un ctor, un dtor (destructeur) et une méthode Log, c’est tout. Ouvrons le code de cette classe CerLog :

#include “cerlog.h”

long CerLog::m_stCompt = 0;

CerLog::CerLog(const CString& path) : m_path(path)

{

SECURITY_ATTRIBUTES sa;

::CreateDirectory(m_path,&sa);

}

CerLog::~CerLog()

{}

BOOL CerLog::Log(ORB_REQUETE * pReq)

{

HANDLE hFic;

bDone = ::WriteFile(hFic,(LPSTR) pReq,

(DWORD)pReq->usLgRequete,

&dwBytesWritten,

NULL);

::CloseHandle(hFic);

return TRUE;

}

On remarque que le membre static est initialisé et on trouve le code du ctor, du dtor et la méthode Log.

Rentrons dans les détails du fonctionnement d’une classe.

Définir une fonction membre

Il est possible de définir la signature dans le header et de mettre l’implémentation dans le fichier cpp. Mais pour certains membres, on peut les définir dans le header. Ainsi les propriétés simples ont leur place dans le header.

Voici comment on aurait designer la classes CerLog2 pour une utilisation simple :

void Discover_Class()

{

CerLog2 log(“c:\temp”);

log.Log(“hello the logger”);

}

class CerLog2

{

public:

CerLog2(const string &path) : m_path(path) {}

~CerLog2() {}

string GetPath() const

{

return m_path;

}

void Log(string message)

{

//…

}

private:

string m_path;

};

Et là vous me dites : « mais ça ressemble à du Java ou du C# ! ». En effet, si on met tout le code dans le header… Mais généralement on propose une solution avec deux personnages différents. Il y a celui qui construit et design la classe et il y a celui (ou celle) qui l’utilise.

Introduction au this et const

This représente un pointeur sur l’objet à l’intérieur d’une classe.

string GetInternalPath() const { return this->m_path; }

Le fait de préciser que la fonction membre est const indique que l’on ne peut pas modifier les valeurs des données membres. Le this devient un this const. Les objets qui sont const et les références ou pointeurs vers des objets const ne peuvent appeler que des fonctions membres const.

Il faut noter que lorsqu’on écrit une fonction membre à l’extérieur du header, il faut préciser le nom de la classe avec :: et respecter les paramètres de la fonction.

Définir une fonction qui retourne l’objet « this »

Ajoutons une méthode Merge dans la classe MyLogger pour merger un logger.

class MyLogger

{

public:

MyLogger& Merge(const MyLogger &logger);

};

Voici l’implémentation :

MyLogger& MyLogger::Merge(const MyLogger &logger)

{

m_path = logger.m_path;

return *this;

}

Le logger positionne le path et retourne un objet this dans sa totalité avec le * sur this. C’est une référence qui est retournée.

Définir des fonctions non membres d’une classe

Des fois, il est nécessaire de créer des fonctions auxiliaires (read,write, print) qui travaillent avec notre classe. Dans ce cas, il faut définir la fonction dans le même header que la classe.

Dans le header :

void LogAMessage(string message);

Dans le cpp :

void LogAMessage(string message)

{

MyLogger logger(“c:\temp”);

logger.Log(message);

}

Le constructeur

Par défaut, le compilateur défini un ctor qui ne fait rien. Chaque classe définit comment les objets sont initialisés. Les classes contrôlent l’initialisation des objets en définissant un ou plusieurs fonctions membres qui portent le nom de la classe et ce sont des constructeurs (ctor). Le ctor initialise les données membres d’un objet de classe. Un constructeur est exécuté lorsque l’objet d’une classe est créé. Les constructeurs n’ont pas de type de retour et peuvent être un bloc vide. Une classe peut avoir plusieurs constructeurs qui diffèrent par leurs paramètres. Un constructeur ne peut pas être marqué const. La classe MyLogger peut avoir les ctor suivants :

MyLogger()

{

m_path = “c:\temp”;

}

MyLogger(string path)

{

m_path = path;

}

Les ctor et l’initialisation par liste

Il est possible de fournir une liste au ctor. C’est le C++ 11 qui permet cela.

Dans le header :

MyLogger(string path, initializer_list<string> log_headers);

Dans le cpp:

MyLogger::MyLogger(string path, initializer_list<string> log_headers)

{

m_path = path;

for (auto it = log_headers.begin(); it != log_headers.end(); ++it)

{

Log(*it);

}

}

Et voici comment utiliser ce ctor :

MyLogger log4(“c:\temp”, { “begin log”, “1 juin 2015”, “Application Totor” });

log4.Log(“the logger log4”);

Contrôle d’accès et encapsulation

Lorsque nous définissons une interface pour notre classe, rien ne force l’utilisateur à respecter les appels dans le bon ordre ou le choix des méthodes. C’est la raison pour laquelle nous cachons l’implémentation dans des blocs private. Le contrôle d’accès garantie l’encapsulation. Les membres définis après public sont accessible dans toutes les parties du programme. Les membres public définissent l’interface de la classe. Les membres définis après private sont accessibles aux fonctions membres de la classe mais ne sont pas accessible au code qui utilise la classe. Les sections private encapsulent (cache) l’implémentation.

Class ou struct, il faut choisir

Class et struct ont le même sens si ce n’est que dans struct par défaut tout est public et que dans class, par défaut tout est privé. Mais c’est la même chose.

Les fonctions ou classes friend (amies)

Reprenons la classe MyLogger et ajoutons lui un membre private pour la sécurité (un exemple) :

class MyLogger

{

private:

string m_path;

SECURITY_ATTRIBUTES sa;

};

Et définissons la fonction LogAsAdministrator :

void LogAsAdministrator(string message)

{

MyLogger logger(“c:\temp”);

// fake function :)

logger.m_sa = ::CreateRestrictedToken(Windows::Administrator);

logger.Log(message);

}

Cette fonction doit accéder au membre private m_sa qui est le jeton de sécurité. Problème, cette fonction n’est pas dans la classe. Donc, il faut la déclarer en friend (amie) et ainsi elle aura le droit d’accéder à tous les membres de la classe. Magique !

class MyLogger

{

friend void LogAsAdministrator(string message);

La mécanique est la même pour les classes friend.

Je vais ajouter une classe qui fournit le privilège fictif administrator.

Dans le header de MyLogger :

void LogAsAdministrator(const string & message);

private:

CSecurityHelper m_sec;

};

Dans le cpp de MyLogger:

void MyLogger::LogAsAdministrator(const string & message)

{

m_sec.m_sa = m_sa;

m_sec.EnableAdministratorMode();

Log(message);

}

La classe MyLogger au travers sa méthode LogAsAdministrator va renseigner un membre private de CSecurityHelper qui est m_sa en lui fournissant le sien pour demander une élévation de privilège.

Pour pouvoir accèder au membre privé, il faut que la classe MyLogger soit friend de CSecurityHelper dont voici le header :

#pragma once

class CSecurityHelper

{

friend class MyLogger;

public:

CSecurityHelper() {}

~CSecurityHelper() {}

void EnableAdministratorMode();

private:

SECURITY_ATTRIBUTES m_sa;

};

Voici le cpp :

#include “stdafx.h”

#include “MyLogger.h”

#include “SecurityHelper.h”

void CSecurityHelper::EnableAdministratorMode()

{

// use m_sa

// fake function :)

m_sa.lpSecurityDescriptor = NULL; // ::CreateRestrictedToken(Windows::Administrator);

}

Pour que cela compile, il faut faire un #include de MyLogger.h pour que le compilateur connaisse la définition de la classe friend.

Les typedef dans les classes

Pour définir des types utilisateurs, on utilise parfois le typedef à l’intérieur d’une classe. Cela rend la classe plus lisible. Reprenons la classe MyLogger qui possède des entêtes avant de logger et que nous allons stocker dans un vector<string>. Nous pouvons stocker le vector en private et déclarer les itérateurs comme typedef avec un nom plus simple… Voici à quoi cela ressemble :

class MyLogger

{

public:

typedef vector<string> HEADERS;

typedef vector<string>::const_iterator CIT;

private:

HEADERS m_headers;

Le typedef est là pour vous aider à rendre les types plus lisibles.

La résolution des noms

Le compilateur passe son temps à chercher les noms de fonctions qui matche. De temps en temps dans le code on trouvera une ligne de code comme ::WriteFile() ; cela veut dire que le scope recherché est global et que cela ne se trouve pas dans la classes dans laquelle on utilise cette fonction.

Les membres static

Une classe peut avoir des membres static mais ils doivent être initialisé explicitement dans le fichier cpp.

Example : Dans le fichier header:

class NewLogger

{

public:

static void Log(const string& message);

static string m_path;

};

Dans le cpp :

string NewLogger::m_path = “c:\temp”;

void NewLogger::Log(const string& message)

{

cout << message << endl;

}

Dans le programme qui l’utilise :

NewLogger::Log(“here is a static logger”);

Il n’y a rien de compliqué.

La surcharge des opérateurs

Il est possible de surcharger tous les opérateurs de ++ en passant par -> en passant par [] ou ==.

bool operator==(const MyLogger &left, const MyLogger &right)

{

return left.GetPath() == right.GetPath();

}

Introduction à la programmation orientée objet

Les idées clé dans la programmation orientée objet sont l’abstraction de données, l’héritage et le binding dynamic. Avec l’abstraction de données, on peut définir des classes qui ont une séparation entre l’interface et leur implémentation. Au travers de l’héritage, on peut définir des classes qui forment un modèle de relation avec des types similaires. Avec le binding dynamic, on peut utiliser des objets avec des types dont on ignore les différences par rapport à la classe de base. Prenons un exemple simple qui explique l’héritage, les classes abstraites et les fonctions virtuelles. Il y a la classe abstraite Animal et deux classes dérivées que sont Cat et Dog :

class Animal

{

public:

Animal() {}

virtual ~Animal() {}

public:

virtual void Eat() = 0;

virtual string Type() { return “Animal”; }

};

class Cat : public Animal

{

virtual void Eat()

{

cout << “whyskas croquettes for Cat” << endl;

}

virtual string Type() { return “Cat”; }

};

class Dog : public Animal

{

public:

virtual void Eat()

{

cout << “whyskas croquettes for Dog” << endl;

}

virtual string Type() { return “Dog”; }

};

J’ai volontairement tout mis dans le header pour faire plus de concis. Voici ce qu’il faut retenir : la classe Animal est une classe abstraite car elle contient la méthode Eat() qui est virtuelle pure (=0) ; ça veut dire que toute classe qui hérite de Animal à l’obligation de redéfinir la méthode Eat. Le destructeur est annoté virtual : c’est obligatoire pour les dtor dans les classes mères et dérivées.

Animal * ptrAnimal;

Cat c1;

Dog d1;

// Pointe sur le Cat

ptrAnimal = &c1;

cout << ptrAnimal->Type() << endl;

ptrAnimal->Eat();

// Pointe sur le Dog

ptrAnimal = &d1;

cout << ptrAnimal->Type() << endl;

ptrAnimal->Eat();

Ce petit bout de code montre comme faire un binding avec un pointeur sur la classe abstraite. On n’a pas le droit de déclarer un type Animal car c’est une classe abstraite ; par contre, on a le droit de s’en servir comme pointeur et de pointer sur des types enfants. On pointe sur un Cat puis sur un Dog et les méthodes virtuelles sont appelées comme par magie.

Le concept clé : le polymorphisme en C++

L’idée clé derrière l’OOP est le polymorphisme. Ce mot est dérivé d’un mot grec qui veut dire plusieurs formes. On parle des types liés à l’héritage comme des types polymorphiques par ce que nous pouvons utiliser plusieurs formes de ces types tout en ignorant les différences entre eux. Quand on appelle une fonction dans une classe de base au travers une référence ou un pointeur de la classe de base, on ne connait pas le type de l’objet sur lequel ce membre est appelé. L’objet peut être un objet de la classe de base ou un objet de la classe dérivée. Si la fonction est virtuelle, alors la décision de savoir quelle fonction va s’exécuter est repoussé à l’exécution. La version de la fonction virtuelle qui tourne est celle définie par le type d’objet avec lequel la référence est liée ou pour un pointeur sur le type d’objet pointé. D’un autre côté, les appels à des fonctions non virtuelles sont déterminés à la compilation.

Contrôle d’accès et héritage

Comme dans une classe pour cacher ses propres membres, chaque classe contrôle si ses membres sont accessibles à une classe dérivée.

Une classes utilise protected pour les membres qu’elle veut accessible à ses classes dérivées mais veut protéger depuis un accès général. L’accès protected se positionne entre le private et le public. Comme private, les membres protected sont inaccessibles aux utilisateurs de cette classe. Comme public, les membres protected sont accessibles aux membres et friend des classes dérivées de cette classe. Un membre d’une classe dérivée ou friend peut accèder aux membres protected de la classe de base seulement au travers d’un objet dérivé. La classe dérivée n’a aucun accès spécial sur les membres protected des objets de la classe de base.

Conclusion

En C++, la classe est l’inséparable alliée de programmation orientée objet. Cet article se concentre sur la manière d’expérimenter simplement les fonctionnalités offertes par une classe. Il y a encore beaucoup de choses à appréhender comme la sémantique de déplacement, la redéfinition des opérateurs, les fonctions virtuelles. Avec cet article, vous avez les bases pour faire votre propre construction. Créez des classes, assemblez les et n’oubliez pas : il y a deux casquettes, celui qui conçoit la classes et celui qu’il l’utilise. Il est bien plus simple d’être utilisateur que concepteur de classes. La maîtrise des principes de la programmation objet passent par de l’entrainement et de l’expérience. En C++, il n’y a jamais (ou presque) de classes deprecated. Donc une fois créée, la classe est là et pour longtemps. Prenez le framework de classes des MFC ; élaboré vers 1990, ce framework ne cesse de grossir et d’évoluer. Quand vous faites du C++, vous avez en tête que le code marche pour longtemps. C’est un exercice que l’on ne rencontre pas avec les langages dits modernes comme java ou C#. En C++, on garde tout. La pyramide grossit mais on ne jette rien. Regardez vers le monde Linux : GTK, Kde, Xfce, toutes ces librairies évoluent depuis des années et c’est toujours utilisé. Regarder QT, il ne s’est jamais aussi bien porté. Les ISV adorent QT car il permet de faire des GUI multi-plateformes de folie. Allez, lancez-vous !

Je pratique le C++ – Partie 1/5 – basic

Je pratique le C++ – Partie 1/5.

Nous vous proposons une série d’articles sur la pratique de C++ pour que vous puissiez tous vous y mettre. Pas besoin de débourser un rond pour pratiquer car tout est gratuit. Que ce soit l’ancien Visual Studio Express, Visual Studio Community Edition 2013 ou GCC, ou CLang via Linux ou MinGW, ça ne coûte rien. L’essentiel est d’avoir un compilateur récent pour bénéficier du C++ 11 alias le C++ Moderne. C++ 11 apporte énormément de changement à C++ 03 et C++05TR1 et c’est la raison pour laquelle il y a un engouement énorme autour de C++ 11. C’est la renaissance de C++. C++ est le langage incontournable pour faire des applications sur téléphone Android, iOS, Windows Phone mais aussi pour faire des applications sur Windows, Linux et sur Mac. Et pourtant on n’en fait pas beaucoup de publicité. C’est un secret de polichinelle ; le Marketing essaie de vous orienter sur des langages comme C#, VB.NET ou Java mais savez-vous que les applications que vous utilisez tous les jours sont faites en C/C++. Le système d’exploitation, le navigateur Internet, votre lecteur de vidéos, votre lecteur de musique, la suite Bureautique, votre serveur de base de données et bien sûr, vos applications préférées comme facebook ou twitter sur mobile…

Le langage C++

C++ est un langage normalisé par un comité ISO qui améliore constamment le langage et la librairie standard STL (Standard Template Library). Le comité ISO est en préparation de la version C++17 (qui fait suite aux versions C++11, C++14).

Les types prédéfinis

Il existe de nombreux types prédéfinis :

Type Sens Taille minimum
bool Booléen. true ou false. NA
char Charactère 8 bits
wchar_t Charactère large 16 bits
char16_t Charactère Unicode 16 bits
char32_t Charactère Unicode 32 bits
short Entier court 16 bits
int Entier 16 bits
long Entier long 32 bits
long long Entier long 64 bits
float Flottant à précision simple 6 digits
double Flottant à double précision 10 digits
long double Flottant à double précision 10 digits

Les types peuvent être signés ou non via le specifier signed ou unsigned. Les types sont tous signés par défaut.

La taille d’une variable peut être obtenue via sizeof(t).

Les variables

Une variable se déclare comme suit :

<type> nom_de_variable = valeur_d’initialisation ;

L’initialisation n’est pas obligatoire mais si elle est manquante, le contenu de la variable est indéfini. En mode DEBUG, ça vaudra 0 ou null mais en RELEASE c’est indéfini. Conseil : il faut initialiser ses variables.

Les références

Une référence pointe sur un objet. C’est une référence vers quelque chose. C’est un alias.

void Sample_References()

{

int a = 10;

int &b = a;

a++;

cout << “b=” << b << endl;

}

L’affichage sera : b=11.

Les références sont comme des pointeurs mais avec un pouvoir limité. (ça gratte ?).

Les pointeurs

Un pointeur sert à désigner un espace mémoire qui pointe vers quelque chose, un objet ou une variable. C’est comme une référence sauf qu’il n’est pas obligé d’être initialisé à la déclaration.

void Sample_Pointer()

{

int a = 10;

int * ptr = nullptr;

ptr = &a;

a++;

cout << “ptr adress=” << ptr << ” value=” << *ptr << endl;

}

L’affichage sera :

ptr adress=0x0062FE20 value=11

Dans cet exemple, on initialise le pointeur à nullptr. Ensuite on indique que ptr vaut l’adresse (via &) de a. La valeur de a est son emplacement mémoire et le contenu du pointeur est accessible via *.

const

Utiliser const sur une variable empêche cette variable d’être modifiée. Cependant, elle doit être initailisée à la déclaration.

void Sample_Const()

{

const int a = 100;

a++; // error

a = 200; // error

}

Par défaut, les objets const sont local à un fichier .cpp pour la compilation. Pour définir et partager un objet const à plusieurs, il faut déclarer cette variable comme extern sinon le compilateur indiquera ne pas trouver la variable avec un message du style « var is undefined. ».

Références const

Il existe aussi des références const. Ce sont des réfrences que l’on ne peut pas changer.

void Sample_ConstRef()

{

int a = 10;

const int & aref = a;

int b = 20;

aref = b; // error aref est const

}

Alias de types

Il est possible de définir un alias sur un type. Pourquoi ? C’est pour rendre le programme plus lisible.

typedef std::string String;

typedef vector<string> MesFilles;

typedef vector<string>::iterator IT;

void Sample_Typedef()

{

String maggie = “je m’appelle Maggie et j’ai bientôt 5 ans et j’aime Papa”;

cout << maggie << endl;

MesFilles girls;

girls.push_back(“Edith”);

girls.push_back(“Lisa”);

girls.push_back(“Audrey Maggie”);

for (IT it = begin(girls); it != end(girls); ++it)

{

cout << *it << endl;

}

}

Dans cet exemple, j’ai créé 3 alias : sur std ::string, sur le vector<string> et sur son iterator.

Le mot-clé auto

Disponible depuis C++ 11 « modern C++ », le mot clé auto permet de laisser le compilateur déduire le type d’un objet. C’est le var du C# pour ceux qui connaissent.

void Sample_auto()

{

MesFilles girls = { “Edith”, “Lisa”, “Audrey” };

for (auto it = begin(girls); it != end(girls); ++it)

{

cout << *it << endl;

}

}

Vous remarquez que j’ai initialisé le vector de string (typedef MesFilles) avec des {}. C’est le C++ 11 qui permet cela.

Les structures

Pour créer un type utilisateur qui contient des variables, le plus simple c’est de créer une structure.

struct Book

{

string name;

int price;

string comments;

};

void Sample_Book()

{

Book CPPPrimer5ThEdition = { “C++ Primer”, 50, “the bible” };

cout << “book :” << CPPPrimer5ThEdition.name << endl;

cout << “price : ” << CPPPrimer5ThEdition.price << endl;

cout << “comments : ” << CPPPrimer5ThEdition.comments << endl;

}

Dans cet exemple, la structure est déclarée en dehors de la fonction et la définition d’une variable de ce type se fait avec les {}. On aurait pu aussi initialiser les membres un par un.

Book ref;

ref.name = “The C++ language”;

ref.price = 50;

ref.comments = “by Bjarne Stroustrup, the creator of C++”;

Une structure est comme une classe dont tous les membres sont public alors qu’ils sont private pour une classe.

Les entêtes .h (ou .hpp)

Un programme peut contenir plusieurs fichiers .cpp. Et il est nécessaire de partager des informations sachant que lorsque le compilateur prend un fichier, il faut que toutes les déclarations de types soient connues. Donc, on écrit des fichiers header (.h) qui contiennent toutes déclarations que nous allons utiliser. Si je veux que la structure Book soit accessible par plusieurs fichiers .cpp, il faut que je la définisse dans un header .h et que je fasse un #include au début du fichier .cpp qui veut l’utiliser pour que le compilateur puisse faire son job.

Voici donc la version « propre » du sample qui utilise la structure book :

#include “Book.h”

void Sample_Book()

{

Book CPPPrimer5ThEdition = { “C++ Primer”, 50, “the bible” };

Les entêtes précompilées

Par convention, on met les #include au début du fichier .cpp. Pour optimiser le temps de compilation, il existe quelque chose qui se nomme les entêtes précompilées. Il suffit de mettre tous ses fichiers d’entêtes dans un seul header (stdafx.h pour le Visual C++) et de paramétrer le projet pour dire que cette entêtes doit être précompilées ainsi, le fichier stdafx.cpp contient le #include stdafx.h et c’est tout. Donc, le fichier qui contient toutes les entêtes en parsé une fois et c’est tout.

Le préprocesseur

Hérité du langage C, le préprocesseur permet de changer le contenu du programme. #include ne fait qu’injecter le source dans le programme et pour inclure plusieurs fois le même fichier dans un source, nous utilisons des barrières. Je m’explique… En réalité on ne met pas tous les headers dans les entêtes précompilées, on met juste les entêtes pour les fichiers .h définies par le système d’exploitation et les librairies tierces. Les structures que nous définissons doivent comporter une structure qui ressemble à cela :

// Book.h – this header contains the definition of an entity Book

#ifndef BOOK_HEADER

#define BOOK_HEADER

#include <string>

struct Book

{

string name;

int price;

string comments;

};

#endif

Pour un compilateur qui ne supporte pas les entêtes précompilées, le fichier peut être inclus plusieurs fois, il ne sera parsé qu’une seule fois juste avec l’astuce d’un ifndef.

Le namespace std

Il est possible de définir des types dans des espaces de nom. Pour ce il suffit de déclarer un bloc namespace <nom> {…} et tous les types seront dedans. C’est utile pour séparer les choses. La librairie standard du C++ (STL alias Standard Template Library) définie tous ses types dans le namespace std (standard). Pour inclure la résolution des noms dans un programme, il suffit d’écrire using namespace <nom> ;

Le type string

La STLdéfinie le type string. C’est une classe template qui permet de gérer les chaînes de caractères. Avant de pouvoir utiliser string, il faut ajouter l’instruction #include <string>.

string est définie dans le namespace std. Le type string est une classe donc il y a des méthodes pour manipuler les chaînes. On peut même utiliser l’opérateur + pour concaténer des chaînes. On peut utiliser la méthode c_str() pour obtenir le pointeur const sur la chaîne de caractères comme en C.

void Sample_String()

{

string audrey = “je suis une coquine”;

string maggie = audrey + string(” a sa maman”);

if( !maggie.empty() )

{

cout << “taille: ” << maggie.size() << endl;

}

for (auto c : maggie) // pour chaque charactère

{

cout << c;

}

cout << endl;

const char * pChar = maggie.c_str();

printf(“%sn”, pChar);

}

La classe string est simple à utiliser.

Le type vector

C’est le type le plus connu de la STL avec string. vector<T> permet de stocker des éléments contigus. Si vous cherchez à stocker des éléments en mémoire, il faut utiliser le vector<T>

void Sample_Vector()

{

vector<int> v = { 1, 2, 3, 4, 5, 6 };

vector<string> girls;

girls.push_back(“Edith”);

girls.push_back(“Lisa”);

girls.push_back(“Audrey Maggie”);

string lacoquine = girls[2];

cout << “la coquine c’est ” << lacoquine << endl;

for (auto & f : girls)

{

cout << f << endl;

}

}

Les Itérateurs

La STL introduit la notion d’itérateurs qui permet, comme un pointeur, d’avoir un accès indirect à un objet. Dans le cas d’un itérateur, l’objet est un élément dans un container ou dans une chaîne string. Un itérateur permet de récupérer un élément et de passer la position suivante. Comme un pointeur, un itérateur peut être valide ou non.

La fonction begin() permet d’obtenir la première position de l’itérateur et end() indique la dernière position invalide. Si le container est vide, begin() et end() auront la même valeur.

void Sample_Iterator()

{

vector<string> girls = { “didou”, “mini-mini”, “méga teuteute” };

vector<string>::const_iterator cit = begin(girls);

cout << “my 12y baby is ” << *cit << endl;

++cit;

++cit;

cout << “super maggie is ” << *cit << endl;

++cit;

if (cit == end(girls))

{

cout << “iterator is end…” << endl;

}

// it est un vector<string>::iterator

for (auto it = girls.begin(); it != girls.end(); ++it)

{

cout << *it << endl;

}

}

Dans cet example, j’utilise un iterateur const et un itérateur normal. Voici les opérations possibles sur un itérateur de container :

Opération Explication
*iter Retourne une référence vers l’objet pointé par iter
Iter->mem Accède à l’objet memoire
++iter Avance d’une position pour aller sur le prochain objet
–iter Recule d’une position pour avoir l’objet precedent
Iter1==iter2 Compare deux itérateurs
Iter1 !=iter2 Compare deux itérateurs

Il existe deux types d’itérateurs: const_iterator et iterator. Les operations begin() et end() peuvent s’écrire de deux façons :

auto it1 = begin(girls);

auto it2 = girls.begin();

if (it1 == it2)

{

cout << “it1 == it2” << endl;

}

Les opérations (on passe vite)

Un bloc en C++ commence par { et se termine par }. Il existe les opérations suivantes: if, switch, while, do while et for. Le C++ 11 introduit le for simplifié alias range-for :

void Sample_For()

{

vector<string> girls = { “didou”, “mini-mini”, “maggie” };

for (auto & girl : girls)

{

cout << girl << endl;

}

}

D’autres opérations existent comme break, continue ou goto.

La gestion des exceptions

Les exceptions sont un bloc try..catch avec le type d’exception à catcher. Il est possible de lancer une exception via un throw. La STL défini la class exception pour les problèmes principaux. Les exceptions sont définies dans std::except.

Les fonctions et le linker (édition de lien)

Pour définir une fonction, il faut un nom, un type de retour et éventuellement des arguments. Comme les programmes peuvent devenir complexes, on créé des fonctions dans des fichiers .cpp et on les utilise dans d’autres fichiers. C’est le principe de la compilation séparée. Pour pouvoir utiliser ce mécanisme, il faut que la fonction soit définie dans un point .h ou .cpp avec juste sa déclaration avec un ; (qu’on appelle le prototype). Cela permet au compilateur de savoir qu’un appel est fait à une fonction et c’est l’éditeur de lien (le link) qui va se charger de faire la résolution des trucs pour construire un exécutable.

Passage d’argument par références

Les habitués du C passent des pointeurs en paramètres alors que les habitués du C++ passent leur paramètre par références.

void Reset_Int_Ptr(int * i)

{

*i = 0;

}

void Reset_Int_Ref(int & i)

{

i = 0;

}

void Sample_Function()

{

int a = 10;

Reset_Int_Ptr(&a); // on passe l’adresse de a

cout << a << endl;

int b = 200;

Reset_Int_Ref(b); // on passe b par reference

cout << b << endl;

}

Gérer la ligne de commande

La ligne de commande du main est simple à gérer. Le premier argument donne le nombre d’éléments et le deuxième est un tableau de chaînes contenant les paramètres.

int main(int argc, char **argv) { … }

Des fonctions avec des paramètres variables

Introduit avec le C++ 11, les fonctions à paramètres variables tirent parti des initializer_lists. Un type initializer_list est un type de a STL qui représente un tableau. Il est présent dans le header initializer_list.

void error_msg(initializer_list<string> il)

{

for (auto beg = il.begin(); beg != il.end(); ++beg)

cout << *beg << ” “;

cout << endl;

}

void Sample_Initializer_List()

{

initializer_list<string> params = { “I am happy”, “with a beer”, “at 6PM” };

error_msg(params);

}

Arguments de fonctions par défaut

Il est possible de donner une valeur par défaut aux paramètres d’une fonction. Ceci doit être spécifié dans la déclaration de fonction dans un fichier d’entête spécifique.

Fonctions inlines

Hérité du C, une fonction peut être annotée inline. Cela veut dire qu’à chaque appel dans le code source, la fonction sera éclatée en vrac avec son code. C’est pour gagner des pouillèmes en performance mais c’est très souvent utilisé. Ne méprisez pas.

Les macros du préprocesseur

Il existe 4 macros qui sont super utiles en debugging:

__FILE__ : nom du fichier source

__LINE__ : numéro de ligne

__DATE__ : date de compilation

__TIME__ : heure de compilation

Les pointeurs de fonctions

Un pointeur de fonction est un pointeur standard sauf qu’au lieu de pointer sur un objet, il pointe sur une fonction. Plus précisément, il pointe sur une fonction qui possède un type retour et des arguments d’un certain type. Le nom de la fonction n’entre pas en ligne de compte pour le pointeur.

bool CompareInteger(const int & a, const int & b)

{

if (a > b)

return true;

else

return false;

}

void Sample_FunctionPointer()

{

// déclaration du pointeur de fonction

bool(*pFn)(const int&, const int&);

// association ptr

pFn = &CompareInteger;

if( (*pFn)(100, 99) == true )

{

cout << “superior !” << endl;

}

}

La suite, c’est du lourd…

Dans l’article suivant, nous aborderons les classes qui sont l’essence de C++. Nous verrons les types simples, les constructeurs, les destructeurs, les méthodes virtuelles, l’héritage simple et multiple, les classes amies et peut-être les templates – ce sera peut-être un article à part – donc un beau programme en perspective !

Amiga stuff…

Amiga Workbench:

Démos:

Games:

Development Tools: