Quelle est la manière préférée / idiomatique d'insérer dans une carte?


113

J'ai identifié quatre façons différentes d'insérer des éléments dans un std::map:

std::map<int, int> function;

function[0] = 42;
function.insert(std::map<int, int>::value_type(0, 42));
function.insert(std::pair<int, int>(0, 42));
function.insert(std::make_pair(0, 42));

Laquelle de ces méthodes est la méthode préférée / idiomatique? (Et y a-t-il un autre moyen auquel je n'ai pas pensé?)


26
Votre carte devrait s'appeler «réponses», pas «fonction»
Vincent Robert

2
@Vincent: Hm? Une fonction est essentiellement une carte entre deux ensembles.
fredoverflow

7
@FredOverflow: il semble que le commentaire de Vincent soit une blague sur certains livres ...
Victor Sorokin

1
Cela semble contredire l'original - 42 ne peut pas être simultanément la réponse à (a) la vie, l'univers et tout, et (b) rien. Mais alors comment exprimez-vous la vie, l'univers et tout comme un int?
Stuart Golodetz

19
@sgolodetz Vous pouvez tout exprimer avec un int assez grand.
Yakov Galka

Réponses:


90

Tout d' abord, operator[]et les insertfonctions membres ne sont pas fonctionnellement équivalentes:

  • La operator[]va chercher la clé, insérez une construite par défaut valeur si elle est introuvable, et retourner une référence à laquelle vous attribuez une valeur. Évidemment, cela peut être inefficace si le mapped_typepeut bénéficier d'être directement initialisé au lieu d'être construit et attribué par défaut. Cette méthode rend également impossible de déterminer si une insertion a bien eu lieu ou si vous n'avez écrasé que la valeur d'une clé insérée précédemment
  • La insertfonction membre n'aura aucun effet si la clé est déjà présente dans la carte et, bien qu'elle soit souvent oubliée, retourne un std::pair<iterator, bool>qui peut être intéressant (notamment pour déterminer si l'insertion a effectivement été effectuée).

Parmi toutes les possibilités d'appel répertoriées insert, les trois sont presque équivalentes. Pour rappel, regardons la insertsignature dans la norme:

typedef pair<const Key, T> value_type;

  /* ... */

pair<iterator, bool> insert(const value_type& x);

Alors, en quoi les trois appels sont-ils différents?

  • std::make_pairrepose sur la déduction des arguments de modèle et pourrait (et dans ce cas produira ) quelque chose d'un type différent de celui réel value_typede la carte, ce qui nécessitera un appel supplémentaire au std::pairconstructeur de modèle afin de convertir en value_type(c'est- à -dire: ajouter constàfirst_type )
  • std::pair<int, int>nécessitera également un appel supplémentaire au constructeur de modèle de std::pairafin de convertir le paramètre en value_type(c'est- à -dire: ajouter constàfirst_type )
  • std::map<int, int>::value_typene laisse absolument aucun doute car il s'agit directement du type de paramètre attendu par la insertfonction membre.

En fin de compte, j'éviterais d'utiliser operator[]lorsque l'objectif est d'insérer, à moins qu'il n'y ait aucun coût supplémentaire dans la construction par défaut et l'attribution du mapped_type, et que je ne me soucie pas de déterminer si une nouvelle clé a effectivement été insérée. Lors de l'utilisation insert, la construction d'un value_typeest probablement la voie à suivre.


la conversion de Key en Const Key dans make_pair () demande-t-elle vraiment un autre appel de fonction? Il semble qu'un cast implicite suffirait quel compilateur devrait être heureux de le faire.
galactica

99

À partir de C ++ 11, vous disposez de deux options supplémentaires majeures. Tout d'abord, vous pouvez utiliser insert()avec la syntaxe d'initialisation de liste:

function.insert({0, 42});

Ceci est fonctionnellement équivalent à

function.insert(std::map<int, int>::value_type(0, 42));

mais beaucoup plus concis et lisible. Comme d'autres réponses l'ont noté, cela présente plusieurs avantages par rapport aux autres formes:

  • le operator[] approche nécessite que le type mappé soit assignable, ce qui n'est pas toujours le cas.
  • le operator[] approche peut écraser des éléments existants et ne vous donne aucun moyen de savoir si cela s'est produit.
  • Les autres formes insertque vous répertoriez impliquent une conversion de type implicite, ce qui peut ralentir votre code.

L'inconvénient majeur est que ce formulaire exigeait que la clé et la valeur soient copiables, donc cela ne fonctionnerait pas avec par exemple une carte avec des unique_ptrvaleurs. Cela a été corrigé dans la norme, mais le correctif n'a peut-être pas encore atteint votre implémentation de bibliothèque standard.

Deuxièmement, vous pouvez utiliser la emplace()méthode:

function.emplace(0, 42);

Ceci est plus concis que toutes les formes de insert(), fonctionne bien avec des types de mouvement uniquement comme unique_ptr, et théoriquement peut être légèrement plus efficace (bien qu'un compilateur décent devrait optimiser la différence). Le seul inconvénient majeur est que cela peut surprendre un peu vos lecteurs, car les emplaceméthodes ne sont généralement pas utilisées de cette façon.


8
il y a aussi les nouveaux insert_or_assign et try_emplace
sp2danny

12

La première version:

function[0] = 42; // version 1

peut ou non insérer la valeur 42 dans la carte. Si la clé 0existe, elle attribuera 42 à cette clé, écrasant la valeur de cette clé. Sinon, il insère la paire clé / valeur.

Les fonctions d'insertion:

function.insert(std::map<int, int>::value_type(0, 42));  // version 2
function.insert(std::pair<int, int>(0, 42));             // version 3
function.insert(std::make_pair(0, 42));                  // version 4

par contre, ne faites rien si la clé 0existe déjà dans la carte. Si la clé n'existe pas, elle insère la paire clé / valeur.

Les trois fonctions d'insertion sont presque identiques. std::map<int, int>::value_typeest le typedefpour std::pair<const int, int>, et std::make_pair()produit évidemment unstd::pair<> magie de déduction via template. Le résultat final, cependant, devrait être le même pour les versions 2, 3 et 4.

Lequel utiliserais-je? Je préfère personnellement la version 1; c'est concis et "naturel". Bien sûr, si son comportement d'écrasement n'est pas souhaité, alors je préférerais la version 4, car elle nécessite moins de frappe que les versions 2 et 3. Je ne sais pas s'il existe un moyen unique de facto d'insérer des paires clé / valeur dans un std::map.

Une autre façon d'insérer des valeurs dans une carte via l'un de ses constructeurs:

std::map<int, int> quadratic_func;

quadratic_func[0] = 0;
quadratic_func[1] = 1;
quadratic_func[2] = 4;
quadratic_func[3] = 9;

std::map<int, int> my_func(quadratic_func.begin(), quadratic_func.end());

5

Si vous souhaitez écraser l'élément avec la clé 0

function[0] = 42;

Autrement:

function.insert(std::make_pair(0, 42));

5

Depuis C ++ 17 std::map propose deux nouvelles méthodes d'insertion: insert_or_assign()et try_emplace(), comme également mentionné dans le commentaire de sp2danny .

insert_or_assign()

Fondamentalement, insert_or_assign()est une version "améliorée" de operator[]. Contrairement à operator[], insert_or_assign()ne nécessite pas que le type de valeur de la carte soit constructible par défaut. Par exemple, le code suivant ne se compile pas, car il MyClassn'a pas de constructeur par défaut:

class MyClass {
public:
    MyClass(int i) : m_i(i) {};
    int m_i;
};

int main() {
    std::map<int, MyClass> myMap;

    // VS2017: "C2512: 'MyClass::MyClass' : no appropriate default constructor available"
    // Coliru: "error: no matching function for call to 'MyClass::MyClass()"
    myMap[0] = MyClass(1);

    return 0;
}

Cependant, si vous remplacez myMap[0] = MyClass(1);par la ligne suivante, le code se compile et l'insertion se déroule comme prévu:

myMap.insert_or_assign(0, MyClass(1));

De plus, similaire à insert(), insert_or_assign()renvoie a pair<iterator, bool>. La valeur booléenne est truesi une insertion s'est produite et falsesi une affectation a été effectuée. L'itérateur pointe vers l'élément qui a été inséré ou mis à jour.

try_emplace()

Semblable à ce qui précède, try_emplace()est une "amélioration" de emplace(). Contrairement à emplace(), try_emplace()ne modifie pas ses arguments si l'insertion échoue en raison d'une clé déjà existante dans la carte. Par exemple, le code suivant tente de mettre en place un élément avec une clé qui est déjà stockée dans la carte (voir *):

int main() {
    std::map<int, std::unique_ptr<MyClass>> myMap2;
    myMap2.emplace(0, std::make_unique<MyClass>(1));

    auto pMyObj = std::make_unique<MyClass>(2);    
    auto [it, b] = myMap2.emplace(0, std::move(pMyObj));  // *

    if (!b)
        std::cout << "pMyObj was not inserted" << std::endl;

    if (pMyObj == nullptr)
        std::cout << "pMyObj was modified anyway" << std::endl;
    else
        std::cout << "pMyObj.m_i = " << pMyObj->m_i <<  std::endl;

    return 0;
}

Sortie (au moins pour VS2017 et Coliru):

pMyObj n'a pas été inséré
pMyObj a quand même été modifié

Comme vous pouvez le voir, pMyObjne pointe plus vers l'objet d'origine. Cependant, si vous remplacez auto [it, b] = myMap2.emplace(0, std::move(pMyObj));par le code suivant, la sortie est différente, car elle pMyObjreste inchangée:

auto [it, b] = myMap2.try_emplace(0, std::move(pMyObj));

Production:

pMyObj n'a pas été inséré
pMyObj pMyObj.m_i = 2

Code sur Coliru

Remarque: j'ai essayé de garder mes explications aussi courtes et simples que possible pour les intégrer dans cette réponse. Pour une description plus précise et complète, je vous recommande de lire cet article sur Fluent C ++ .


3

J'ai effectué des comparaisons de temps entre les versions susmentionnées:

function[0] = 42;
function.insert(std::map<int, int>::value_type(0, 42));
function.insert(std::pair<int, int>(0, 42));
function.insert(std::make_pair(0, 42));

Il s'avère que les différences de temps entre les versions d'insert sont minimes.

#include <map>
#include <vector>
#include <boost/date_time/posix_time/posix_time.hpp>
using namespace boost::posix_time;
class Widget {
public:
    Widget() {
        m_vec.resize(100);
        for(unsigned long it = 0; it < 100;it++) {
            m_vec[it] = 1.0;
        }
    }
    Widget(double el)   {
        m_vec.resize(100);
        for(unsigned long it = 0; it < 100;it++) {
            m_vec[it] = el;
        }
    }
private:
    std::vector<double> m_vec;
};


int main(int argc, char* argv[]) {



    std::map<int,Widget> map_W;
    ptime t1 = boost::posix_time::microsec_clock::local_time();    
    for(int it = 0; it < 10000;it++) {
        map_W.insert(std::pair<int,Widget>(it,Widget(2.0)));
    }
    ptime t2 = boost::posix_time::microsec_clock::local_time();
    time_duration diff = t2 - t1;
    std::cout << diff.total_milliseconds() << std::endl;

    std::map<int,Widget> map_W_2;
    ptime t1_2 = boost::posix_time::microsec_clock::local_time();    
    for(int it = 0; it < 10000;it++) {
        map_W_2.insert(std::make_pair(it,Widget(2.0)));
    }
    ptime t2_2 = boost::posix_time::microsec_clock::local_time();
    time_duration diff_2 = t2_2 - t1_2;
    std::cout << diff_2.total_milliseconds() << std::endl;

    std::map<int,Widget> map_W_3;
    ptime t1_3 = boost::posix_time::microsec_clock::local_time();    
    for(int it = 0; it < 10000;it++) {
        map_W_3[it] = Widget(2.0);
    }
    ptime t2_3 = boost::posix_time::microsec_clock::local_time();
    time_duration diff_3 = t2_3 - t1_3;
    std::cout << diff_3.total_milliseconds() << std::endl;

    std::map<int,Widget> map_W_0;
    ptime t1_0 = boost::posix_time::microsec_clock::local_time();    
    for(int it = 0; it < 10000;it++) {
        map_W_0.insert(std::map<int,Widget>::value_type(it,Widget(2.0)));
    }
    ptime t2_0 = boost::posix_time::microsec_clock::local_time();
    time_duration diff_0 = t2_0 - t1_0;
    std::cout << diff_0.total_milliseconds() << std::endl;

    system("pause");
}

Cela donne respectivement pour les versions (j'ai exécuté le fichier 3 fois, d'où les 3 décalages horaires consécutifs pour chacune):

map_W.insert(std::pair<int,Widget>(it,Widget(2.0)));

2198 ms, 2078 ms, 2072 ms

map_W_2.insert(std::make_pair(it,Widget(2.0)));

2290 ms, 2037 ms, 2046 ms

 map_W_3[it] = Widget(2.0);

2592 ms, 2278 ms, 2296 ms

 map_W_0.insert(std::map<int,Widget>::value_type(it,Widget(2.0)));

2234 ms, 2031 ms, 2027 ms

Par conséquent, les résultats entre différentes versions d'insert peuvent être négligés (cependant, n'a pas effectué de test d'hypothèse)!

La map_W_3[it] = Widget(2.0);version prend environ 10 à 15% de temps de plus pour cet exemple en raison d'une initialisation avec le constructeur par défaut pour Widget.


2

En bref, l' []opérateur est plus efficace pour mettre à jour les valeurs car il implique d'appeler le constructeur par défaut du type valeur puis de lui attribuer une nouvelle valeur, tandis queinsert() est plus efficace pour ajouter des valeurs.

L'extrait cité de Effective STL: 50 Specific Ways to Improve Your Use of the Standard Template Library par Scott Meyers, Item 24 pourrait vous aider.

template<typename MapType, typename KeyArgType, typename ValueArgType>
typename MapType::iterator
insertKeyAndValue(MapType& m, const KeyArgType&k, const ValueArgType& v)
{
    typename MapType::iterator lb = m.lower_bound(k);

    if (lb != m.end() && !(m.key_comp()(k, lb->first))) {
        lb->second = v;
        return lb;
    } else {
        typedef typename MapType::value_type MVT;
        return m.insert(lb, MVT(k, v));
    }
}

Vous pouvez décider de choisir une version générique sans programmation de ceci, mais le fait est que je trouve ce paradigme (différencier «ajouter» et «mettre à jour») extrêmement utile.


1

Si vous voulez insérer un élément dans std :: map - utilisez la fonction insert (), et si vous voulez trouver un élément (par clé) et lui en affecter - utilisez l'opérateur [].

Pour simplifier l'insertion, utilisez la bibliothèque boost :: assign, comme ceci:

using namespace boost::assign;

// For inserting one element:

insert( function )( 0, 41 );

// For inserting several elements:

insert( function )( 0, 41 )( 0, 42 )( 0, 43 );

1

Je viens de changer un peu le problème (map of strings) pour montrer un autre intérêt d'insert:

std::map<int, std::string> rancking;

rancking[0] = 42;  // << some compilers [gcc] show no error

rancking.insert(std::pair<int, std::string>(0, 42));// always a compile error

le fait que le compilateur ne montre aucune erreur sur "rancking [1] = 42;" peut avoir un impact dévastateur!


Les compilateurs n'affichent pas d'erreur pour le premier car il std::string::operator=(char)existe, mais ils affichent une erreur pour le second car le constructeur std::string::string(char)n'existe pas. Cela ne devrait pas produire d'erreur car C ++ interprète toujours librement tout littéral de style entier comme char, donc ce n'est pas un bogue du compilateur, mais plutôt une erreur de programmeur. En gros, je dis simplement que si cela introduit ou non un bogue dans votre code, vous devez faire attention à vous-même. BTW, vous pouvez imprimer rancking[0]et un compilateur utilisant ASCII produira *, ce qui est (char)(42).
Keith M

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.