Notions abordées

A travers les exemples, plusieurs notions seront abordées de manières récurrentes. Ces notions sont importantes dans le langage C++.

Comprendre le déplacement et les références rvalue

La notion la plus complexe à appréhender est sans doute les références rvalue. Les références rvalue sont utilisées pour permettre le déplacement d'objet. En C++98, il existe une fonction std::swap qui permet l'échange de valeur. Par exemple, int a[5] = { 1, 2, 3, 4, 5}, b[5] = {}; std::swap(a,b); permet de déplacer le tableau a vers le tableau b. Cette fonction peut être surchargée pour tous types, elle évite une copie et elle permet de transférer un objet qui n'a pas volonté à être dupliqué (via une relation d'amitié friend). A partir du C++11, l'idée de déplacer un objet a été généralisée et modernisée grâce à la notation &&. Une référence rvalue s'accompagne toujours de la fonction std::move. La fonction std::move s'apparente à un cast en référence rvalue. Le déplacement est généralement implémenté pour des objets proposant des services de recopie de gros objets ou pour des objets détenant une ressource unique (mutex, thread, ...) à l'aide :

  • du constructeur de déplacement: class(class&&)
  • de l'opérateur de déplacement: class& operator=(class&&)

In [1]:
.rawInput


Using raw input
Out[1]:


In [2]:
#include <iostream>
#include <array>
#include <memory>

class Buffer
{
private:
    using array_int5_t = std::array<int,5>;
    array_int5_t tableau;
public:   
    // Constructeur prenant une référence lvalue
    Buffer(const array_int5_t& a)
    : tableau(a)
    {
        std::cout << "copie de tableau" << std::endl;
    }
    
    // Constructeur prenant une référence rvalue
    Buffer(array_int5_t&& a)
    : tableau(std::move(a))
    {
        std::cout << "déplacement de tableau" << std::endl;
    }
    
    // Constructeur par recopie
    Buffer(const Buffer& b)
    :tableau(b.tableau)
    {
        std::cout << "recopie de buffer" << std::endl;
    }
    
    // Constructeur par déplacement
    Buffer(Buffer&& b)
    :tableau(std::move(b.tableau))
    {
        std::cout << "déplacement de buffer" << std::endl;
    }
};
             
Buffer createBuffer()
{
    return {{1,2,3,4,5}};    
}


Out[2]:


In [3]:
.rawInput


Not using raw input
Out[3]:


In [4]:
std::array<int,5> a{1, 2, 3, 4, 5};

std::cout << "b1 ";
Buffer b1(a);

std::cout << "b2 ";
Buffer b2({1, 2, 3, 4 , 5});

std::cout << "b3 ";
Buffer b3(createBuffer()); // RVO : return value optimization

std::cout << "b4 ";
Buffer b4(b1);

std::cout << "b5 ";
std::make_unique<Buffer>(createBuffer()); // Déplacement du tableau dans un buffer, puis du buffer dans un pointeur.


b1 copie de tableau
b2 déplacement de tableau
b3 déplacement de tableau
b4 recopie de buffer
b5 déplacement de tableau
déplacement de buffer
Out[4]:
(std::_MakeUniq<Buffer>::__single_object) @0x7f1551042fb0

Remarque: Un objet est déplacé seulement si celui-ci est éligible au déplacement. Par exemple, un objet const ou les anciennes classes implémentées en C++98 ne sont pas éligibles au déplacement. Une opération de copie est réalisée à la place.

Remarque 2: Un retour de fonction est toujours optimisé par le compilateur. Il ne faut jamais définir de rvalue sur les retours de fonction.


In [5]:
const Buffer createConstBuffer()
{
    return {{1,2,3,4,5}};    
}
std::cout << "b6 ";
std::make_unique<Buffer>(createConstBuffer());


b6 déplacement de tableau
recopie de buffer
Out[5]:
(std::_MakeUniq<Buffer>::__single_object) @0x7f15510ad430

Comprendre la transmission parfaite et les références universelles

L'utilisation des template dans des concepts de métaprogrammation a fait émerger une nouvelle problématique : la transmission parfaite des arguments. Pour palier à cet problématique, la notation && pour un argument template ou pour une variable auto signifie que la variable est une référence universelle. Celle-ci indique que la variable peut être soit une référence lvalue ou soit une référence rvalue. Le type de référence est défini par l'appelant. Une référence universelle s'accompagne toujours de la fonction std::forward.


In [6]:
.rawInput


Using raw input
Out[6]:


In [7]:
int *m = nullptr;

// Plusieurs fonctions de mémorisation de données en fonction de son type
void memPtr(int& a)
{
    m = &a;
}

void memVal(int a)
{}

// Une fonction template réalisant le lock de la données avant d'appeler la fonction de mémorisation
template <typename FctT, typename T>
void lockAndMem(FctT fct, T a)
{
    // appel à la fonction de lock, puis appel à la fonction de mémorisation
    fct(a);
}

// Même template mais avec une référence lvalue
template <typename FctT, typename T>
void lockAndMemRef(FctT fct, T& a)
{
    // appel à la fonction de lock, puis appel à la fonction de mémorisation
    fct(a);
}

// Même template mais avec une référence univeselle
template <typename FctT, typename T>
void lockAndMemUni(FctT fct, T&& a)
{
    // appel à la fonction de lock, puis appel à la fonction de mémorisation
    fct(std::forward<T>(a));
}


Out[7]:


In [8]:
.rawInput


Not using raw input
Out[8]:


In [9]:
int i = 10;
lockAndMem(&memPtr, i);
if (m != &i) std::cout << "mauvaise sauvegarde : pointeur sur une variable locale" << std::endl;

// Cela ne fonctionne pas, ajoutons une référence au template
lockAndMemRef(&memPtr, i);
if (m == &i) std::cout << "bonne sauvegarde" << std::endl;


mauvaise sauvegarde : pointeur sur une variable locale
bonne sauvegarde
Out[9]:


In [10]:
//lockAndMemRef(&memVal, 10); // Echoue car 10 est une rvalue.


Out[10]:


In [11]:
// Fonctionne dans tous les cas
lockAndMemUni(&memPtr, i);
if (m == &i) std::cout << "bonne sauvegarde" << std::endl; 
lockAndMemUni(&memVal, 10);


bonne sauvegarde
Out[11]:
(void) @0x7f156b7fcb18

Comprendre auto

Le mot clé auto indique que l'on laisse le compilateur déduire le type. Le livre Programmer efficacement en C++, ScottMeyers donne le conseil de préférer l'utilisation de auto aux déclarations de type explicite. Afin de ne pas nuire à la lisibilité du code, il semble raisonnable de dire "préférer l'utilisation de auto aux déclarations de type explicite pour les types complexes". Une variable auto nécessite d'être initialisée (normal puisque le compilateur doit déduire son type). Outre l'avantage de forcer le developpeur à initialiser ses variables avec une valeur, les avantages sont :

  • de faciliter la syntaxe de types complexes comme les types à l'intérieur des conteneurs de la STL.
  • de creer des variables à partir d'expressions lambdas
  • d'éviter les erreurs de types introduisant des conversions implicites (et donc une baisse des performances)

In [12]:
.rawInput


Using raw input
Out[12]:


In [13]:
#include <iostream>
#include <typeinfo>
#include <memory>
#include <unordered_map>
#include <functional>

#include <cxxabi.h>
std::string demangle( const char* mangled_name ) 
{
    std::size_t len = 0 ;
    int status = 0 ;
    std::unique_ptr< char, decltype(&std::free) > ptr(
                __cxxabiv1::__cxa_demangle( mangled_name, nullptr, &len, &status ), &std::free ) ;
    return ptr.get() ;
}

struct example
{
    int val;
    
    example(int a)
    : val(a)
    {}
    example(const example& o)
    : val(o.val)
    {
        std::cout << "copie" << std::endl;
    }
    ~example()
    {
        std::cout << "destruction" << std::endl;
    }
    
    bool operator==(const example& o) const
    {
        return val == o.val;
    }
};

namespace std
{
    template <> 
    struct hash<example>
    {
        size_t operator()(const example& x) const
        {
            return hash<int>()(x.val);
        }
    };
}


Out[13]:


In [14]:
.rawInput


Not using raw input
Out[14]:


In [15]:
auto base = 0;
auto lambda = [&base](int a, int b) { return (a + b) % base;};
std::cout << "Type lambda : " << demangle(typeid(lambda).name()) << std::endl << std::endl;

std::unordered_map<example,std::string> umap;
umap.insert(std::make_pair(21, "Hello"));
umap.insert(std::make_pair(28, "World"));
// Transmission parfaite du type contenu dans umap
for (auto&& elem : umap)
{
    std::cout << "Type du contenu dans umap : " << demangle(typeid(elem).name()) << std::endl;
}

std::cout << std::endl << "Conversion implicite dûe à une erreur de type : " << std::endl;

// le type de elem est std::pair<const example, std::string>
// de manière naïve, on aurait pu écrire : 

for (const std::pair<example, std::string>& elem : umap)
{
    // Récupération d'un pointeur sur une variable locale alors que l'on 
    // croit récupérer un pointeur sur l'élément contenu
    
    const std::pair<example, std::string>* ptr = &elem;
}


Type lambda : __cling_Un1Qu37(void*)::$_0

Type du contenu dans umap : std::pair<example const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >
Type du contenu dans umap : std::pair<example const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >

Conversion implicite dûe à une erreur de type : 
copie
destruction
copie
destruction
Out[15]:

Comprendre nullptr

Le mot clé nullptr permet d'indiquer un pointeur nul. A la différence de NULL qui est de type int, nullptr est de type pointeur. Il est donc indispensable d'utiliser nullptr pour bénéficier de la déduction des types.


In [16]:
.rawInput


Using raw input
Out[16]:


In [17]:
template <typename FctT, typename T>
void logAndCall(FctT fct, T&& ptr)
{
    // appel à la fonction de log, puis appel à la fonction en paramètre
    fct(std::forward<T>(ptr));
}

void deletePtr(int* ptr)
{}


Out[17]:


In [18]:
.rawInput


Not using raw input
Out[18]:


In [19]:
logAndCall(deletePtr, nullptr);
//logAndCall(deletePtr, NULL); //échoue car NULL n'est pas un pointeur


Out[19]:
(void) @0x7f156b7fcb18

Comprendre la génération des fonctions membres spéciales

Les fonctions membres spéciales sont les fonctions générées automatiquement par le compilateur. On en connaît déjà 4 :

  • Le constructeur par défaut
  • Le destructeur
  • Le constructeur de recopie
  • L'opérateur de copie.

Le C++11 ajoute aux 4 premières :

  • Le constructeur de déplacement
  • L'opérateur de déplacement

Le comportement par défaut pour ces deux opérations est un déplacement membre à membre de la classe. Cependant, les règles de génération se sont un peu plus durcies par rapport à C++98 pour ces deux nouveaux. Pour les 4 premiers, la génération est indépendante, c'est à dire que si on déclare un destructeur, le constructeur de recopie et l'opérateur de copie sont quand même générés. Souvent, ce comportement n'est pas souhaité car si l'on déclare un destructeur, c'est qu'une ressource est détenue par la classe et nécessite une implémentation spéciale pour sa copie. Ce qui à engendré la règle de codage que tous le monde connaît qui est d'implémenter le destructeur, le constructeur de recopie et l'opérateur de copie lorsque la classe alloue une ressource. Avec les deux nouveaux, le problème ne se pose pas car elle ne sont pas générés si l'un des points suivants est vérifié :

  • Le destructeur est déclaré
  • Le constructeur de recopie ou l'opérateur de copie est déclaré
  • Une opération de déplacement est déclaré

Pour le dernier point, cela s'explique par le fait que s'il existe une implémentation pour le constructeur de déplacement, alors la génération du comportement par défaut pour l'opérateur de déplacement ne convient pas. Le raisonnement inverse est identique.

De plus, si une opération de déplacement est déclarée, les opérations de recopie ne sont pas générées.

Ces règles sont importantes. Prenons l'exemple que l'on crée un destructeur pour pouvoir tracer les appels, cette pratique va avoir un effet indésirable car à la place d'utiliser les opérations de déplacement parfaitement valides, le compilateur utilise les opérations de recopie pouvant être beaucoup plus coûteuses. Pour éviter cela, il est possible d'utiliser le mot clé default pour signifier que le comportement de déplacement par défaut est valide.

Une autre utilisation du mot clé default est l'utilisation pour le destructeur virtuel des classes de base. En effet, une règle du C++ impose de déclarer un destructeur virtuel de la classe de base pour permettre la désallocation complète de l'objet lors de l'utilisation de polymorphisme dynamique. Ce destructeur ne fait rien et le comportement par défaut est adapté au besoin.


In [20]:
class BufferCopie
{
public:
    BufferCopie()
    {}

    BufferCopie(const BufferCopie& b)
    {
        std::cout << "recopie de buffer" << std::endl;
    }
};

class BufferDeplacement
{
public:
    BufferDeplacement()
    {}

    BufferDeplacement(const BufferDeplacement& b)
    {
        std::cout << "recopie de buffer" << std::endl;
    }
    
    BufferDeplacement(BufferDeplacement&& b) = default;
};
// Pas de génération du constructeur de déplacement car le constructeur de recopie est défini.
// Appel au constructeur de recopie
std::cout << "b7 ";
std::make_unique<BufferCopie>(BufferCopie());

// Le constructeur de recopie est défini et le constructeur de déplacement est défini avec le comportement par défaut.
// Appel au constructeur de déplacement
std::cout << "b8 ";
std::make_unique<BufferDeplacement>(BufferDeplacement());

// Définition d'une classe interface
class INotifier
{
public:
    // Déclaration du destructeur virtuel pour l'appel au destructeur de la classe héritée lors de l'utilisation du 
    // polymorphisme dynamique
    virtual ~INotifier() = default;
    virtual void Notify() = 0;
};


b7 recopie de buffer
Out[20]:

Comprendre les fonctions supprimés

La génération des fonctions membres spéciales est parfois non souhaitée. Par exemple, la copie de classes de gestion d'entrées/sorties n'est pas vraiment un concept définissable : le port d'entrée doit-il être réinitialisé ? ou partagé ? Il est plus simple de d'interdire la copie de telles classes. Le mot-clé delete permet d'indiquer qu'une fonction est interdite.


In [21]:
class CopieInterdite
{
public:
    CopieInterdite()
    {}
    CopieInterdite(const CopieInterdite&) = delete;
    CopieInterdite& operator=(const CopieInterdite&) = delete;
};

CopieInterdite A, B;
//CopieInterdite C(A);
//B = A;


Out[21]:

Comprendre les fonctions de substitutions

Le langage C++ introduit deux nouveaux mots-clés override et final pour indiquer explicitement une redéfinition d'une fonction membre virtuelle d'une classe de base. Une erreur de redéfinition provoquera une erreur de compilation. Déclarer les fonctions membres redéfinies à l'aide de ces deux mots-clés est donc une bonne pratique pour repérer les erreurs le plus tôt possible.


In [22]:
class Base
{
public:
    virtual void operation()
    {
        std::cout << "Base" << std::endl;
    }
};

class DeriveFaux : public Base
{
public:
    virtual void operation() const // override // l'ajout du mot-clé provoque une erreur de compilation
    {
        std::cout << "DeriveFaux" << std::endl;
    }
};

class Derive : public Base
{
public:
    virtual void operation() override
    {
        std::cout << "Derive" << std::endl;
    }
};

class DeriveFinal : public Base
{
public:
    virtual void operation() final // ne peut plus être redéfinie dans une classe fille
    {
        std::cout << "DeriveFinal" << std::endl;
    }
};

std::unique_ptr<Base> ptrBase = std::make_unique<DeriveFaux>();
ptrBase->operation(); // Sans le mot-clé override, il faut tester toutes les classes dérivées 
                      // si la signature de la classe de base change.

ptrBase = std::make_unique<Derive>();
ptrBase->operation();

ptrBase = std::make_unique<DeriveFinal>();
ptrBase->operation();


Base
Derive
DeriveFinal
Out[22]:
(void) @0x7f156b7fcb18

Comprendre constexpr

Le mot-clé constexpr peut s'appliquer aux variables et aux fonctions. Une variable constexpr signifie que la variable est constante et connue à la compilation. Elle sera allouée dans la mémoire en lecture seule. Une fonction constexpr signifie que la valeur de retour sera connue à la compilation si toutes les valeurs en paramètre de la fonction sont connues lors de la compilation.


In [23]:
.rawInput


Using raw input
Out[23]:


In [24]:
class Tool
{
public:
    static std::string DecodeString(const unsigned char* trame, const unsigned int start, const unsigned end)
    {
        return std::string(trame + start, trame + end);
    }
};

class Trame
{
protected:
    static constexpr unsigned int STX_SIZE = 1U;
    static constexpr unsigned int ETX_SIZE = 1U;
    static constexpr unsigned int TYPE_SIZE = 1U;
    
public:
    static constexpr char GetType(const unsigned char* trame) noexcept
    {
        return trame[STX_SIZE];
    }
};

class TextMsg : public Trame
{
public:
    static constexpr unsigned int GetContentStart() noexcept
    {
        return STX_SIZE + TYPE_SIZE;
    }
    
    static constexpr unsigned int GetContentEnd(const unsigned int trameSize) noexcept
    {
        return trameSize - ETX_SIZE;
    }
};

constexpr unsigned char HELLO_TRAME[] = {0x02, 0x54, 0x48, 0x45, 0x4C, 0X4C, 0x4F ,0x03};

// Constantes définies à la compilation
constexpr char HELLO_TYPE = Trame::GetType(HELLO_TRAME);                       // = 'T'
constexpr unsigned int HELLO_START = TextMsg::GetContentStart();               // = 2 
constexpr unsigned int HELLO_END = TextMsg::GetContentEnd(sizeof(HELLO_TRAME));// = 7

// Si la trame change, par exemple avec l'ajout d'un champ au début de la trame, seules les fonctions sont à modifiées. 
// Les constantes seront automatiquement mis à jour à la compilation.


Out[24]:


In [25]:
.rawInput


Not using raw input
Out[25]:


In [26]:
// Trace
std::cout << "Emission de la trame : " << HELLO_TYPE << " " << Tool::DecodeString(HELLO_TRAME, HELLO_START, HELLO_END) << std::endl;

// Simulation de la réception d'une trame (trame et taille non déclarée constexpr ou const)
unsigned char test[] = {0x02, 0x54, 0x54, 0x45, 0x53, 0X54, 0x03};
unsigned int lg = sizeof(test);

// Appel des fonctions constexpr à l'éxecution
std::cout << "Réception de la trame : " << Trame::GetType(test) << " " << Tool::DecodeString(test, TextMsg::GetContentStart(), TextMsg::GetContentEnd(lg)) << std::endl;


Emission de la trame : T HELLO
Réception de la trame : T TEST
Out[26]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f157d3a3e40

Remarque: En C++11, une fonction constexpr est limitée à une seule instruction return (pas de condition, pas de boucle). Cette limitation a été levée en C++14.

Comprendre noexcept

Le fait de déclarer une fonction noexcept indique au developpeur appelant que la fonction n'émet pas d'exception. Les avantages de déclarer une fonction noexcept sont :

  • de connaître sa sûreté face aux exceptions
  • d'optimiser le code de la fonction (Le compilateur ne générera pas le code de restauration de la pile suite à une interruption).

La connaissance de la sureté d'une fonction face aux exceptions est intéressante notamment pour les opérations de déplacement. Contrairement aux opérations de recopie, si une exception intervient lors d'un déplacement d'un objet, il n'est pas possible de revenir en arrière : une partie des attributs ont été déplacée dans une autre zone mémoire. Il est donc indispensable d'appeler des fonctions noexcept dans les opérations de déplacement.


In [27]:
class DeplacementNoExcept
{
public:
    DeplacementNoExcept()
    {}
    
    DeplacementNoExcept(const DeplacementNoExcept&)
    {
        std::cout << "recopie DeplacementNoExcept" << std::endl;
    }
    
    DeplacementNoExcept(DeplacementNoExcept&&) noexcept
    {
        std::cout << "deplacement DeplacementNoExcept" << std::endl;
    }
};

class Deplacement
{
public:
    Deplacement()
    {}
    
    Deplacement(const Deplacement&)
    {
        std::cout << "recopie Deplacement" << std::endl;
    }
    
    Deplacement(Deplacement&&) 
    {
        std::cout << "deplacement Deplacement" << std::endl;
    }
};

std::vector<DeplacementNoExcept> v1;
DeplacementNoExcept dne;
v1.push_back(dne);
v1.shrink_to_fit();
std::cout << std::endl;
std::cout << "Réallocation du vecteur : déplacement des éléments précédents si l'opération de déplacement ne renvoie pas d'exception : " << std::endl;
v1.push_back(dne);

std::cout << std::endl << std::endl;

std::vector<Deplacement> v2;
Deplacement d;
v2.push_back(d);
v2.shrink_to_fit();
std::cout << std::endl;
std::cout << "Réallocation du vecteur : déplacement des éléments précédents si l'opération de déplacement ne renvoie pas d'exception : " << std::endl;
v2.push_back(d);


recopie DeplacementNoExcept

Réallocation du vecteur : déplacement des éléments précédents si l'opération de déplacement ne renvoie pas d'exception : 
recopie DeplacementNoExcept
deplacement DeplacementNoExcept


recopie Deplacement

Réallocation du vecteur : déplacement des éléments précédents si l'opération de déplacement ne renvoie pas d'exception : 
recopie Deplacement
recopie Deplacement
Out[27]:
(void) @0x7f156b7fcb18

Remarque 1: Les conteneurs de la STL tirent partie de la déclaration noexcept sur les opérations de déplacement pour optimiser leur gestion.

Remarque 2: Il est possible de déclarer une fonction noexcept qui appelle des fonctions qui ne sont pas déclarées noexcept pour conserver la compatibilité avec les fonctions de la librairie C ou les anciennes fonctions C++98. Le compilateur ne générera pas d'erreur.

Remarque 3: Déclarer une fonction noexcept qui émet/laisse passer une exception provoquera la terminaison du programme. L'exception ne peut pas être catchée par le code appelant.

Remarque 4: Les destructeurs sont déclarés noexcept par nature (règle du langage). Il est inutile de les déclarer noexcept.

Comprendre using

Le mot-clé using permet d'écrire un typedef de manière simplifiée pour les template et les pointeurs de fonctions.


In [28]:
.rawInput


Using raw input
Out[28]:


In [29]:
#include <queue>
#include <stack>

// typedef
typedef int (*fonction_ptr_t1)(int, int);

template <typename T>
class container_types
{
public:
    typedef std::queue<T> fifo_t;
    typedef std::stack<T> lifo_t;
};

// using
using fonction_ptr_t2 = int (*)(int, int);

template <typename T>
using fifo_t = std::queue<T>;

template <typename T>
using lifo_t = std::stack<T>;


Out[29]:


In [30]:
.rawInput


Not using raw input
Out[30]:


In [31]:
int fonction(int, int)
{
    return 0;
}

fonction_ptr_t1 fct1 = &fonction;
fonction_ptr_t2 fct2 = &fonction;

// typedef
typename container_types<int>::fifo_t file1;

// using
fifo_t<int> file2;


Out[31]:

Comprendre decltype

Le mot-clé decltype renvoie presque toujours le type de l'expression passée en paramètre. (presque car si par exemple, l'expression en paramètre est une expression complexe, le type retourné peut-être une lvalue c'est-à-dire type&). Le mot-clé decltype est principalement utilisé avec le mot-clé auto pour le retour des fonctions template ou les expressions lambdas.


In [32]:
int CONSTANTE = 0;

// Pas de surprise, decltype renvoie le type de l'expression
using constante_t = decltype(CONSTANTE);      // int
using fonction_ptr_t3 = decltype(&fonction);  // int (*)(int, int)


Out[32]:


In [33]:
.rawInput


Using raw input
Out[33]:


In [34]:
template <typename ContainerT, typename IndexT>
//typename std::decay<ContainerT>::type::value_type AccessWithIntegrityCheck1(ContainerT&& container, IndexT index) // C++11
auto AccessWithIntegrityCheck1(ContainerT&& container, IndexT index) // syntaxe simplifié du C++14
{
    //CheckIntegrity(std::forward<ContainerT>(container));
    return std::forward<ContainerT>(container)[index];
}

template <typename ContainerT, typename IndexT>
//typename std::decay<ContainerT>::type::reference AccessWithIntegrityCheck2(ContainerT&& container, IndexT index) // C++11
auto& AccessWithIntegrityCheck2(ContainerT&& container, IndexT index) // syntaxe simplifié du C++14
{
    //CheckIntegrity(std::forward<ContainerT>(container));
    return std::forward<ContainerT>(container)[index];
}

template <typename ContainerT, typename IndexT>
//auto AccessWithIntegrityCheck3(ContainerT&& container, IndexT index) -> decltype(std::forward<ContainerT>(container)[index]) //C++11
decltype(auto) AccessWithIntegrityCheck3(ContainerT&& container, IndexT index) // syntaxe simplifié du C++14
{
    //CheckIntegrity(std::forward<ContainerT>(container));
    return std::forward<ContainerT>(container)[index];
}


Out[34]:


In [35]:
.rawInput


Not using raw input
Out[35]:


In [36]:
std::vector<int> ids = {1, 2, 3, 4, 5};
//AccessWithIntegrityCheck1(ids, 2) = 6; // Ne compile pas car le retour n'est pas une lvalue
AccessWithIntegrityCheck2(ids, 2) = 6;
AccessWithIntegrityCheck3(ids, 2) = 6;

std::vector<bool> flags = {false, false, true, false, false};
bool f1 = AccessWithIntegrityCheck1(flags, 2); 
//bool f2 = AccessWithIntegrityCheck2(flags, 2); // Ne compile pas car le type vector<bool> est optimisé 
                                                 // au niveau mémoire pour occuper qu'un seul bit par valeur.
                                                 // L'operator[] renvoit une rvalue dans ce cas présent.
bool f3 = AccessWithIntegrityCheck3(flags, 2);


Out[36]:

Comprendre enum class

Un problème des enumérations en C++ est la pollution de l'espace de nom. On distingue maintenant les énumérations non délimités (enum en C++98) et les énumérations délimités déclarées avec les mots-clés enum class. Les énumérations délimités ne sont accessibles qu'à l'intérieur de leur espace de nom. En C++11, il est également possible de déclarer le type sous-jacent pour les enumérations délimitées et non délimitées permettant d'optimiser l'espace mémoire. Le cast des énumérations délimitées est limité à son type sous-jacent et doit être fait de manière explicite.


In [37]:
enum class EtatSysteme : std::uint8_t // place en mémoire  = 1 octet à la place de 4 octets
{
    Ok,
    EnPanne
};

enum class EtatCommunication : uint8_t 
{
    Ok,
    EnPanne
};

// Il n'y a pas de doublon de valeurs, les valeurs sont déclarées dans des espaces de nom différents
EtatSysteme sys = EtatSysteme::Ok;
EtatCommunication com = EtatCommunication::EnPanne;


Out[37]:


In [38]:
#include <bitset>

template <typename E>
constexpr auto enum_cast(E enumerator) noexcept
{
    return static_cast<std::underlying_type_t<E>>(enumerator);
}

enum class ErrorEnum : uint8_t 
{
    MotorFailure       = 0,
    CableDeconnection  = 1,
    CameraFailure      = 2,
    MicroProgCorrupted = 3
};


Out[38]:


In [39]:
constexpr std::size_t FlagSize = 8U * sizeof(ErrorEnum);
std::bitset<FlagSize> ErrorFlag;

ErrorFlag.set(enum_cast(ErrorEnum::CameraFailure)); // Cast explicite pour obtenir la valeur sous-jacente
std::cout << ErrorFlag << std::endl;


00000100
Out[39]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f157d3a3e40

Comprendre les initializers

Le langage C++11 introduit un nouveau type pour l'initialisation des objets : std::initializer_list<T>. Son instanciation s'écrit simplement avec les accolades {}.


In [40]:
// initialiser un vecteur
std::vector<std::string> strVector = {"un", "deux", "trois"};
// initialiser un tableau
std::array<int,3> intArray({1, 2, 3});

// Attention à la déduction de type
auto values = {1, 2, 3};
auto expr = [](auto&& container, std::size_t index) { return std::forward<decltype(container)>(container)[index]; };
std::cout << expr(intArray, 0) << std::endl;
std::cout << expr(static_cast<std::vector<int>>(values), 0) << std::endl;


1
1
Out[40]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f157d3a3e40

Remarque: Le type d'un objet {} est std::initializer_list<T>. La déduction de type auto ou template necessitera parfois d'expliciter le type cible pour obtenir le bon type.