Comment implémenter un itérateur de style STL et éviter les pièges courants?


306

J'ai créé une collection pour laquelle je veux fournir un itérateur à accès aléatoire de style STL. Je cherchais un exemple d'implémentation d'un itérateur mais je n'en ai trouvé aucun. Je connais le besoin de surcharges const []et d' *opérateurs. Quelles sont les exigences pour qu'un itérateur soit de "style STL" et quels sont les autres pièges à éviter (le cas échéant)?

Contexte supplémentaire: c'est pour une bibliothèque et je ne veux pas en dépendre à moins d'en avoir vraiment besoin. J'écris ma propre collection pour pouvoir fournir une compatibilité binaire entre C ++ 03 et C ++ 11 avec le même compilateur (donc pas de STL qui casserait probablement).


13
+1! Bonne question. Je me suis demandé la même chose. Il est assez facile de feuilleter quelque chose ensemble sur la base de Boost.Iterator, mais il est étonnamment difficile de simplement trouver une liste des exigences si vous l'implémentez à partir de zéro.
2011 à 17h21

2
N'oubliez pas que vos itérateurs doivent être effrayants. boost.org/doc/libs/1_55_0/doc/html/intrusive/…
alfC

Réponses:


232

http://www.cplusplus.com/reference/std/iterator/ a un tableau pratique qui détaille les spécifications du § 24.2.2 de la norme C ++ 11. Fondamentalement, les itérateurs ont des balises qui décrivent les opérations valides et les balises ont une hiérarchie. Ci-dessous est purement symbolique, ces classes n'existent pas réellement en tant que telles.

iterator {
    iterator(const iterator&);
    ~iterator();
    iterator& operator=(const iterator&);
    iterator& operator++(); //prefix increment
    reference operator*() const;
    friend void swap(iterator& lhs, iterator& rhs); //C++11 I think
};

input_iterator : public virtual iterator {
    iterator operator++(int); //postfix increment
    value_type operator*() const;
    pointer operator->() const;
    friend bool operator==(const iterator&, const iterator&);
    friend bool operator!=(const iterator&, const iterator&); 
};
//once an input iterator has been dereferenced, it is 
//undefined to dereference one before that.

output_iterator : public virtual iterator {
    reference operator*() const;
    iterator operator++(int); //postfix increment
};
//dereferences may only be on the left side of an assignment
//once an output iterator has been dereferenced, it is 
//undefined to dereference one before that.

forward_iterator : input_iterator, output_iterator {
    forward_iterator();
};
//multiple passes allowed

bidirectional_iterator : forward_iterator {
    iterator& operator--(); //prefix decrement
    iterator operator--(int); //postfix decrement
};

random_access_iterator : bidirectional_iterator {
    friend bool operator<(const iterator&, const iterator&);
    friend bool operator>(const iterator&, const iterator&);
    friend bool operator<=(const iterator&, const iterator&);
    friend bool operator>=(const iterator&, const iterator&);

    iterator& operator+=(size_type);
    friend iterator operator+(const iterator&, size_type);
    friend iterator operator+(size_type, const iterator&);
    iterator& operator-=(size_type);  
    friend iterator operator-(const iterator&, size_type);
    friend difference_type operator-(iterator, iterator);

    reference operator[](size_type) const;
};

contiguous_iterator : random_access_iterator { //C++17
}; //elements are stored contiguously in memory.

Vous pouvez soit vous spécialiser std::iterator_traits<youriterator>, soit mettre les mêmes typedefs dans l'itérateur lui-même, soit hériter de std::iterator(qui a ces typedefs). Je préfère la deuxième option, pour éviter de changer les choses dans l' stdespace de noms et pour la lisibilité, mais la plupart des gens en héritent std::iterator.

struct std::iterator_traits<youriterator> {        
    typedef ???? difference_type; //almost always ptrdiff_t
    typedef ???? value_type; //almost always T
    typedef ???? reference; //almost always T& or const T&
    typedef ???? pointer; //almost always T* or const T*
    typedef ???? iterator_category;  //usually std::forward_iterator_tag or similar
};

Notez le iterator_category devrait être l' un std::input_iterator_tag, std::output_iterator_tag, std::forward_iterator_tag, std::bidirectional_iterator_tagou std::random_access_iterator_tag, selon laquelle les exigences itérateur satisfait. En fonction de votre iterator, vous pouvez choisir de se spécialiser std::next, std::prev, std::advanceet std::distanceaussi bien, mais cela est rarement nécessaire. Dans des cas extrêmement rares , vous souhaiterez peut-être vous spécialiser std::beginet std::end.

Votre conteneur devrait probablement également avoir un const_iterator, qui est un itérateur (éventuellement modifiable) pour des données constantes similaire à la vôtre, iteratorsauf qu'il devrait être implicitement constructible à partir de a iteratoret les utilisateurs ne devraient pas être en mesure de modifier les données. Il est courant que son pointeur interne soit un pointeur vers des données non constantes et en iteratorhérite const_iteratorafin de minimiser la duplication de code.

Mon message à Writing your own STL Container a un prototype de conteneur / itérateur plus complet.


2
En plus de vous spécialiser std::iterator_traitsou de définir les typedefs vous-même, vous pouvez également en dériver std::iterator, qui les définit pour vous, en fonction de ses paramètres de modèle.
Christian Rau

3
@LokiAstari: La documentation complète est assez étendue (40 pages dans le brouillon) et ne fait pas partie de Stack Overflow. Cependant, j'ai ajouté plus d'informations détaillant les balises d'itérateur et const_iterator. Que manquait-il d'autre à mon message? Vous semblez impliquer qu'il y a plus à ajouter à la classe, mais la question concerne spécifiquement l'implémentation des itérateurs.
Mooing Duck

5
std::iteratora été proposé d'être déconseillé en C ++ 17 ; ce n'était pas le cas, mais je ne m'attendrais pas à ce que ça dure plus longtemps.
einpoklum

2
Une mise à jour du commentaire de @ einpoklum: a std::iteratorété dépréciée après tout.
scrry

1
@JonathanLee: Wow, c'est operator boolincroyablement dangereux. Quelqu'un essaiera de l'utiliser pour détecter la fin d'une plage while(it++), mais tout ce qu'il vérifie vraiment, c'est si l'itérateur a été construit avec un paramètre.
Mooing Duck

16

La documentation iterator_facade de Boost.Iterator fournit ce qui ressemble à un joli didacticiel sur l'implémentation d'itérateurs pour une liste chaînée. Pourriez-vous l'utiliser comme point de départ pour créer un itérateur à accès aléatoire sur votre conteneur?

Si rien d'autre, vous pouvez jeter un œil aux fonctions membres et aux typedefs fournis par iterator_facadeet les utiliser comme point de départ pour créer les vôtres.



10

Voici un exemple d'itérateur de pointeur brut.

Vous ne devez pas utiliser la classe itérateur pour travailler avec des pointeurs bruts!

#include <iostream>
#include <vector>
#include <list>
#include <iterator>
#include <assert.h>

template<typename T>
class ptr_iterator
    : public std::iterator<std::forward_iterator_tag, T>
{
    typedef ptr_iterator<T>  iterator;
    pointer pos_;
public:
    ptr_iterator() : pos_(nullptr) {}
    ptr_iterator(T* v) : pos_(v) {}
    ~ptr_iterator() {}

    iterator  operator++(int) /* postfix */         { return pos_++; }
    iterator& operator++()    /* prefix */          { ++pos_; return *this; }
    reference operator* () const                    { return *pos_; }
    pointer   operator->() const                    { return pos_; }
    iterator  operator+ (difference_type v)   const { return pos_ + v; }
    bool      operator==(const iterator& rhs) const { return pos_ == rhs.pos_; }
    bool      operator!=(const iterator& rhs) const { return pos_ != rhs.pos_; }
};

template<typename T>
ptr_iterator<T> begin(T *val) { return ptr_iterator<T>(val); }


template<typename T, typename Tsize>
ptr_iterator<T> end(T *val, Tsize size) { return ptr_iterator<T>(val) + size; }

Solution de boucle basée sur une plage de pointeurs bruts. S'il vous plaît, corrigez-moi, s'il existe un meilleur moyen de créer une boucle basée sur une plage à partir d'un pointeur brut.

template<typename T>
class ptr_range
{
    T* begin_;
    T* end_;
public:
    ptr_range(T* ptr, size_t length) : begin_(ptr), end_(ptr + length) { assert(begin_ <= end_); }
    T* begin() const { return begin_; }
    T* end() const { return end_; }
};

template<typename T>
ptr_range<T> range(T* ptr, size_t length) { return ptr_range<T>(ptr, length); }

Et test simple

void DoIteratorTest()
{
    const static size_t size = 10;
    uint8_t *data = new uint8_t[size];
    {
        // Only for iterator test
        uint8_t n = '0';
        auto first = begin(data);
        auto last = end(data, size);
        for (auto it = first; it != last; ++it)
        {
            *it = n++;
        }

        // It's prefer to use the following way:
        for (const auto& n : range(data, size))
        {
            std::cout << " char: " << static_cast<char>(n) << std::endl;
        }
    }
    {
        // Only for iterator test
        ptr_iterator<uint8_t> first(data);
        ptr_iterator<uint8_t> last(first + size);
        std::vector<uint8_t> v1(first, last);

        // It's prefer to use the following way:
        std::vector<uint8_t> v2(data, data + size);
    }
    {
        std::list<std::vector<uint8_t>> queue_;
        queue_.emplace_back(begin(data), end(data, size));
        queue_.emplace_back(data, data + size);
    }
}

5

Tout d'abord, vous pouvez consulter ici une liste des différentes opérations que les types d'itérateurs individuels doivent prendre en charge.

Ensuite, lorsque vous avez créé votre classe d'itérateur, vous devez soit vous spécialiser std::iterator_traitset fournir certains typedefs nécessaires (comme iterator_categoryou value_type), soit en dériver std::iterator, ce qui définit les typedefs nécessaires pour vous et peut donc être utilisé avec la valeur par défaut.std::iterator_traits .

Avertissement: Je sais que certaines personnes n'aiment pas cplusplus.combeaucoup cela, mais elles fournissent des informations très utiles à ce sujet.


Je ne comprends vraiment pas le différend cplusplus vs cppreference, ils sont à la fois bons et manquent beaucoup de choses. Cependant, C ++ est le seul langage où l'implémentation d'itérateurs de bibliothèque standard est un enfer XD. La plupart du temps, il est plus simple d'écrire une classe wrapper sur un conteneur stl que de mettre en œuvre un itérateur XD
CoffeDeveloper

@GameDeveloper vérifiez cette bibliothèque de modèles que j'ai écrite pour implémenter les itérateurs: github.com/VinGarcia/Simple-Iterator-Template . C'est très simple et ne nécessite que 10 lignes de code pour écrire un itérateur.
VinGarcia

Belle classe, je l'apprécie, cela vaut probablement la peine d'être porté pour être compilé également avec des conteneurs non-STL (EA_STL, UE4) .. Considérez-le! :)
CoffeDeveloper

Quoi qu'il en soit, si la seule raison est que cplusplus.com fournit des informations vraiment utiles, cppreference.com fournit des informations plus utiles ...
LF

@LF N'hésitez pas à remonter le temps et à ajouter ces informations à la version 2011 du site. ;-)
Christian Rau

3

J'étais / suis dans le même bateau que vous pour différentes raisons (en partie éducatives, en partie contraintes). J'ai dû réécrire tous les conteneurs de la bibliothèque standard et les conteneurs devaient être conformes à la norme. Cela signifie que si j'échange mon conteneur avec la version stl , le code fonctionnera de la même manière. Ce qui signifiait également que je devais réécrire les itérateurs.

Quoi qu'il en soit, j'ai regardé EASTL . En plus d'apprendre une tonne sur les conteneurs que je n'ai jamais appris tout ce temps en utilisant les conteneurs stl ou à travers mes cours de premier cycle. La raison principale est que EASTL est plus lisible que l' homologue stl (j'ai trouvé que c'était simplement à cause du manque de toutes les macros et du style de codage simple). Il y a des choses épineuses là-dedans (comme #ifdefs pour les exceptions) mais rien pour vous submerger.

Comme d'autres l'ont mentionné, consultez la référence de cplusplus.com sur les itérateurs et les conteneurs.


3

J'essayais de résoudre le problème de pouvoir itérer sur plusieurs tableaux de texte différents, tous stockés dans une base de données résidente en mémoire qui est grande struct.

Les éléments suivants ont été élaborés à l'aide de Visual Studio 2017 Community Edition sur une application de test MFC. J'inclus ceci à titre d'exemple, car cette publication était l'une des nombreuses que j'ai rencontrées et qui fournissaient de l'aide mais qui étaient encore insuffisantes pour mes besoins.

Le structcontenant les données résidentes en mémoire ressemblait à quelque chose comme suit. J'ai supprimé la plupart des éléments par souci de concision et n'ai pas non plus inclus les définitions de préprocesseur utilisées (le SDK utilisé est pour C ainsi que C ++ et est ancien).

Ce qui m'intéressait, c'était d'avoir des itérateurs pour les différents WCHARtableaux bidimensionnels qui contenaient des chaînes de texte pour les mnémoniques.

typedef struct  tagUNINTRAM {
    // stuff deleted ...
    WCHAR   ParaTransMnemo[MAX_TRANSM_NO][PARA_TRANSMNEMO_LEN]; /* prog #20 */
    WCHAR   ParaLeadThru[MAX_LEAD_NO][PARA_LEADTHRU_LEN];   /* prog #21 */
    WCHAR   ParaReportName[MAX_REPO_NO][PARA_REPORTNAME_LEN];   /* prog #22 */
    WCHAR   ParaSpeMnemo[MAX_SPEM_NO][PARA_SPEMNEMO_LEN];   /* prog #23 */
    WCHAR   ParaPCIF[MAX_PCIF_SIZE];            /* prog #39 */
    WCHAR   ParaAdjMnemo[MAX_ADJM_NO][PARA_ADJMNEMO_LEN];   /* prog #46 */
    WCHAR   ParaPrtModi[MAX_PRTMODI_NO][PARA_PRTMODI_LEN];  /* prog #47 */
    WCHAR   ParaMajorDEPT[MAX_MDEPT_NO][PARA_MAJORDEPT_LEN];    /* prog #48 */
    //  ... stuff deleted
} UNINIRAM;

L'approche actuelle consiste à utiliser un modèle pour définir une classe proxy pour chacun des tableaux, puis à avoir une seule classe d'itérateur qui peut être utilisée pour itérer sur un tableau particulier en utilisant un objet proxy représentant le tableau.

Une copie des données résidentes en mémoire est stockée dans un objet qui gère la lecture et l'écriture des données résidentes en mémoire depuis / vers le disque. Cette classe, CFileParacontient la classe proxy sur matrice ( MnemonicIteratorDimSizeet la sous - classe à partir de laquelle elle est dérivée est, MnemonicIteratorDimSizeBase) et la classe d'itération, MnemonicIterator.

L'objet proxy créé est attaché à un objet itérateur qui accède aux informations nécessaires via une interface décrite par une classe de base à partir de laquelle toutes les classes proxy sont dérivées. Le résultat est d'avoir un seul type de classe d'itérateur qui peut être utilisé avec plusieurs classes proxy différentes car les différentes classes proxy exposent toutes la même interface, l'interface de la classe de base proxy.

La première chose a été de créer un ensemble d'identifiants qui seraient fournis à une fabrique de classes pour générer l'objet proxy spécifique pour ce type de mnémonique. Ces identifiants sont utilisés dans le cadre de l'interface utilisateur pour identifier les données d'approvisionnement particulières que l'utilisateur souhaite voir et éventuellement modifier.

const static DWORD_PTR dwId_TransactionMnemonic = 1;
const static DWORD_PTR dwId_ReportMnemonic = 2;
const static DWORD_PTR dwId_SpecialMnemonic = 3;
const static DWORD_PTR dwId_LeadThroughMnemonic = 4;

La classe proxy

La classe proxy basée sur un modèle et sa classe de base sont les suivantes. J'avais besoin de prendre en charge plusieurs types de wchar_ttableaux de chaînes de texte. Les tableaux bidimensionnels avaient différents nombres de mnémoniques, selon le type (but) de la mnémonique et les différents types de mnémoniques étaient de longueurs maximales différentes, variant entre cinq caractères de texte et vingt caractères de texte. Les modèles de la classe proxy dérivée correspondaient naturellement au modèle nécessitant le nombre maximal de caractères dans chaque mnémonique. Une fois l'objet proxy créé, nous utilisons ensuite la SetRange()méthode pour spécifier le tableau mnémonique réel et sa plage.

// proxy object which represents a particular subsection of the
// memory resident database each of which is an array of wchar_t
// text arrays though the number of array elements may vary.
class MnemonicIteratorDimSizeBase
{
    DWORD_PTR  m_Type;

public:
    MnemonicIteratorDimSizeBase(DWORD_PTR x) { }
    virtual ~MnemonicIteratorDimSizeBase() { }

    virtual wchar_t *begin() = 0;
    virtual wchar_t *end() = 0;
    virtual wchar_t *get(int i) = 0;
    virtual int ItemSize() = 0;
    virtual int ItemCount() = 0;

    virtual DWORD_PTR ItemType() { return m_Type; }
};

template <size_t sDimSize>
class MnemonicIteratorDimSize : public MnemonicIteratorDimSizeBase
{
    wchar_t    (*m_begin)[sDimSize];
    wchar_t    (*m_end)[sDimSize];

public:
    MnemonicIteratorDimSize(DWORD_PTR x) : MnemonicIteratorDimSizeBase(x), m_begin(0), m_end(0) { }
    virtual ~MnemonicIteratorDimSize() { }

    virtual wchar_t *begin() { return m_begin[0]; }
    virtual wchar_t *end() { return m_end[0]; }
    virtual wchar_t *get(int i) { return m_begin[i]; }

    virtual int ItemSize() { return sDimSize; }
    virtual int ItemCount() { return m_end - m_begin; }

    void SetRange(wchar_t (*begin)[sDimSize], wchar_t (*end)[sDimSize]) {
        m_begin = begin; m_end = end;
    }

};

La classe des itérateurs

La classe d'itérateur elle-même est la suivante. Cette classe fournit simplement une fonctionnalité d'itérateur direct de base, ce qui est tout ce qui est nécessaire pour le moment. Cependant, je m'attends à ce que cela change ou soit étendu lorsque j'en ai besoin de quelque chose de plus.

class MnemonicIterator
{
private:
    MnemonicIteratorDimSizeBase   *m_p;  // we do not own this pointer. we just use it to access current item.
    int      m_index;                    // zero based index of item.
    wchar_t  *m_item;                    // value to be returned.

public:
    MnemonicIterator(MnemonicIteratorDimSizeBase *p) : m_p(p) { }
    ~MnemonicIterator() { }

    // a ranged for needs begin() and end() to determine the range.
    // the range is up to but not including what end() returns.
    MnemonicIterator & begin() { m_item = m_p->get(m_index = 0); return *this; }                 // begining of range of values for ranged for. first item
    MnemonicIterator & end() { m_item = m_p->get(m_index = m_p->ItemCount()); return *this; }    // end of range of values for ranged for. item after last item.
    MnemonicIterator & operator ++ () { m_item = m_p->get(++m_index); return *this; }            // prefix increment, ++p
    MnemonicIterator & operator ++ (int i) { m_item = m_p->get(m_index++); return *this; }       // postfix increment, p++
    bool operator != (MnemonicIterator &p) { return **this != *p; }                              // minimum logical operator is not equal to
    wchar_t * operator *() const { return m_item; }                                              // dereference iterator to get what is pointed to
};

La fabrique d'objets proxy détermine quel objet à créer en fonction de l'identifiant mnémonique. L'objet proxy est créé et le pointeur renvoyé est le type de classe de base standard afin d'avoir une interface uniforme quelle que soit la section mnémonique à laquelle on accède. La SetRange()méthode est utilisée pour spécifier à l'objet proxy les éléments de tableau spécifiques que le proxy représente et la plage des éléments de tableau.

CFilePara::MnemonicIteratorDimSizeBase * CFilePara::MakeIterator(DWORD_PTR x)
{
    CFilePara::MnemonicIteratorDimSizeBase  *mi = nullptr;

    switch (x) {
    case dwId_TransactionMnemonic:
        {
            CFilePara::MnemonicIteratorDimSize<PARA_TRANSMNEMO_LEN> *mk = new CFilePara::MnemonicIteratorDimSize<PARA_TRANSMNEMO_LEN>(x);
            mk->SetRange(&m_Para.ParaTransMnemo[0], &m_Para.ParaTransMnemo[MAX_TRANSM_NO]);
            mi = mk;
        }
        break;
    case dwId_ReportMnemonic:
        {
            CFilePara::MnemonicIteratorDimSize<PARA_REPORTNAME_LEN> *mk = new CFilePara::MnemonicIteratorDimSize<PARA_REPORTNAME_LEN>(x);
            mk->SetRange(&m_Para.ParaReportName[0], &m_Para.ParaReportName[MAX_REPO_NO]);
            mi = mk;
        }
        break;
    case dwId_SpecialMnemonic:
        {
            CFilePara::MnemonicIteratorDimSize<PARA_SPEMNEMO_LEN> *mk = new CFilePara::MnemonicIteratorDimSize<PARA_SPEMNEMO_LEN>(x);
            mk->SetRange(&m_Para.ParaSpeMnemo[0], &m_Para.ParaSpeMnemo[MAX_SPEM_NO]);
            mi = mk;
        }
        break;
    case dwId_LeadThroughMnemonic:
        {
            CFilePara::MnemonicIteratorDimSize<PARA_LEADTHRU_LEN> *mk = new CFilePara::MnemonicIteratorDimSize<PARA_LEADTHRU_LEN>(x);
            mk->SetRange(&m_Para.ParaLeadThru[0], &m_Para.ParaLeadThru[MAX_LEAD_NO]);
            mi = mk;
        }
        break;
    }

    return mi;
}

Utilisation de la classe proxy et de l'itérateur

La classe proxy et son itérateur sont utilisés comme indiqué dans la boucle suivante pour remplir un CListCtrlobjet avec une liste de mnémoniques. J'utilise de std::unique_ptrsorte que lorsque la classe proxy n'est plus nécessaire et qu'elle std::unique_ptrsort du cadre, la mémoire sera nettoyée.

Ce que fait ce code source, c'est de créer un objet proxy pour le tableau dans structlequel correspond à l'identifiant mnémonique spécifié. Il crée ensuite un itérateur pour cet objet, utilise une distance forpour remplir le CListCtrlcontrôle, puis nettoie. Ce sont toutes des wchar_tchaînes de texte brutes qui peuvent être exactement le nombre d'éléments de tableau, nous copions donc la chaîne dans un tampon temporaire afin de nous assurer que le texte se termine par zéro.

    std::unique_ptr<CFilePara::MnemonicIteratorDimSizeBase> pObj(pFile->MakeIterator(m_IteratorType));
    CFilePara::MnemonicIterator pIter(pObj.get());  // provide the raw pointer to the iterator who doesn't own it.

    int i = 0;    // CListCtrl index for zero based position to insert mnemonic.
    for (auto x : pIter)
    {
        WCHAR szText[32] = { 0 };     // Temporary buffer.

        wcsncpy_s(szText, 32, x, pObj->ItemSize());
        m_mnemonicList.InsertItem(i, szText);  i++;
    }

1

Et maintenant un itérateur de clés pour la boucle basée sur la plage.

template<typename C>
class keys_it
{
    typename C::const_iterator it_;
public:
    using key_type        = typename C::key_type;
    using pointer         = typename C::key_type*;
    using difference_type = std::ptrdiff_t;

    keys_it(const typename C::const_iterator & it) : it_(it) {}

    keys_it         operator++(int               ) /* postfix */ { return it_++         ; }
    keys_it&        operator++(                  ) /*  prefix */ { ++it_; return *this  ; }
    const key_type& operator* (                  ) const         { return it_->first    ; }
    const key_type& operator->(                  ) const         { return it_->first    ; }
    keys_it         operator+ (difference_type v ) const         { return it_ + v       ; }
    bool            operator==(const keys_it& rhs) const         { return it_ == rhs.it_; }
    bool            operator!=(const keys_it& rhs) const         { return it_ != rhs.it_; }
};

template<typename C>
class keys_impl
{
    const C & c;
public:
    keys_impl(const C & container) : c(container) {}
    const keys_it<C> begin() const { return keys_it<C>(std::begin(c)); }
    const keys_it<C> end  () const { return keys_it<C>(std::end  (c)); }
};

template<typename C>
keys_impl<C> keys(const C & container) { return keys_impl<C>(container); }

Usage:

std::map<std::string,int> my_map;
// fill my_map
for (const std::string & k : keys(my_map))
{
    // do things
}

Voilà ce que je cherchais. Mais personne ne l'avait, semble-t-il.

Vous obtenez mon alignement de code OCD en bonus.

Comme exercice, écrivez le vôtre values(my_map)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.