Standardwert für std :: map

85

Gibt es eine Möglichkeit , den Standardwert angeben std::map‚s operator[]zurückgibt , wenn ein Schlüssel nicht existiert?

anon
quelle

Antworten:

52

Nein, gibt es nicht. Die einfachste Lösung besteht darin, eine eigene kostenlose Vorlagenfunktion zu schreiben, um dies zu tun. Etwas wie:

#include <string>
#include <map>
using namespace std;

template <typename K, typename V>
V GetWithDef(const  std::map <K,V> & m, const K & key, const V & defval ) {
   typename std::map<K,V>::const_iterator it = m.find( key );
   if ( it == m.end() ) {
      return defval;
   }
   else {
      return it->second;
   }
}

int main() {
   map <string,int> x;
   ...
   int i = GetWithDef( x, string("foo"), 42 );
}

C ++ 11 Update

Zweck: Berücksichtigen Sie generische assoziative Container sowie optionale Komparator- und Allokatorparameter.

template <template<class,class,class...> class C, typename K, typename V, typename... Args>
V GetWithDef(const C<K,V,Args...>& m, K const& key, const V & defval)
{
    typename C<K,V,Args...>::const_iterator it = m.find( key );
    if (it == m.end())
        return defval;
    return it->second;
}
WhozCraig
quelle
1
Schöne Lösung. Möglicherweise möchten Sie einige Vorlagenargumente hinzufügen, damit die Funktionsvorlage mit Karten funktioniert, die nicht die Standardvorlagenparameter für Komparator und Allokator verwenden.
sbi
3
+1, aber um genau das gleiche Verhalten wie operator[]mit dem Standardwert bereitzustellen , sollte der Standardwert in die Karte innerhalb des if ( it == m.end() )Blocks eingefügt werden
David Rodríguez - dribeas
12
@ David Ich gehe davon aus, dass das OP dieses Verhalten nicht wirklich will. Ich verwende ein ähnliches Schema zum Lesen von Konfigurationen, möchte jedoch nicht, dass die Konfiguration aktualisiert wird, wenn ein Schlüssel fehlt.
2
@ GMMan-Bool-Parameter werden von einigen als schlechter Stil angesehen, da Sie anhand des Aufrufs (im Gegensatz zur Deklaration) nicht erkennen können, was sie tun. In diesem Fall bedeutet "true" "use default" oder "don '". t default verwenden "(oder etwas ganz anderes)? Eine Aufzählung ist immer klarer, aber natürlich mehr Code. Ich bin mir in diesem Punkt einig.
2
Diese Antwort funktioniert nicht, wenn der Standardwert nullptr ist, aber stackoverflow.com/a/26958878/297451 .
Jon
42

Dies beantwortet zwar nicht genau die Frage, aber ich habe das Problem mit Code wie diesem umgangen:

struct IntDefaultedToMinusOne
{
    int i = -1;
};

std::map<std::string, IntDefaultedToMinusOne > mymap;
SurvivalMachine
quelle
1
Dies ist die beste Lösung für mich. Einfach zu implementieren, sehr flexibel und generisch.
Acegs
11

Der C ++ - Standard (23.3.1.2) gibt an, dass der neu eingefügte Wert standardmäßig erstellt wird, sodass er mapselbst keine Möglichkeit bietet, dies zu tun. Sie haben folgende Möglichkeiten:

  • Geben Sie dem Werttyp einen Standardkonstruktor, der ihn mit dem gewünschten Wert initialisiert, oder
  • Wickeln Sie die Karte in Ihre eigene Klasse ein, die einen Standardwert bereitstellt und implementiert operator[], um diesen Standard einzufügen.
Mike Seymour
quelle
8
Um genau zu sein, wird der neu eingefügte Wert wie folgt initialisiert (8.5.5): - Wenn T ein Klassentyp mit einem vom Benutzer deklarierten Konstruktor (12.1) ist, wird der Standardkonstruktor für T aufgerufen (und die Initialisierung ist fehlerhaft) -formed, wenn T keinen zugänglichen Standardkonstruktor hat); - Wenn T ein Nicht-Union-Klassentyp ohne einen vom Benutzer deklarierten Konstruktor ist, wird jedes nicht statische Datenelement und jede Basisklassenkomponente von T wertinitialisiert. - Wenn T ein Array-Typ ist, wird jedes Element mit einem Wert initialisiert. - Andernfalls wird das Objekt mit Null initialisiert.
Tadeusz Kopec,
6

Allgemeinere Version, Unterstützung von C ++ 98/03 und mehr Containern

Funktioniert mit generischen assoziativen Containern. Der einzige Vorlagenparameter ist der Containertyp selbst.

Unterstützte Behälter: std::map, std::multimap, std::unordered_map, std::unordered_multimap, wxHashMap, QMap, QMultiMap, QHash, QMultiHash, usw.

template<typename MAP>
const typename MAP::mapped_type& get_with_default(const MAP& m, 
                                             const typename MAP::key_type& key, 
                                             const typename MAP::mapped_type& defval)
{
    typename MAP::const_iterator it = m.find(key);
    if (it == m.end())
        return defval;

    return it->second;
}

Verwendung:

std::map<int, std::string> t;
t[1] = "one";
string s = get_with_default(t, 2, "unknown");

Hier ist eine ähnliche Implementierung unter Verwendung einer Wrapper-Klasse, die get()der dictTypmethode in Python ähnlicher ist : https://github.com/hltj/wxMEdit/blob/master/src/xm/xm_utils.hpp

template<typename MAP>
struct map_wrapper
{
    typedef typename MAP::key_type K;
    typedef typename MAP::mapped_type V;
    typedef typename MAP::const_iterator CIT;

    map_wrapper(const MAP& m) :m_map(m) {}

    const V& get(const K& key, const V& default_val) const
    {
        CIT it = m_map.find(key);
        if (it == m_map.end())
            return default_val;

        return it->second;
    }
private:
    const MAP& m_map;
};

template<typename MAP>
map_wrapper<MAP> wrap_map(const MAP& m)
{
    return map_wrapper<MAP>(m);
}

Verwendung:

std::map<int, std::string> t;
t[1] = "one";
string s = wrap_map(t).get(2, "unknown");
jyw
quelle
MAP :: mapped_type & kann nicht zurückgegeben werden, da der Typname MAP :: mapped_type & defval außerhalb des Gültigkeitsbereichs liegen kann.
Joe C
5

Es gibt keine Möglichkeit, den Standardwert anzugeben - es handelt sich immer um einen vom Standard (Nullparameter-Konstruktor) erstellten Wert.

Tatsächlich macht es operator[]wahrscheinlich mehr als Sie erwarten, als ob ein Wert für den angegebenen Schlüssel in der Zuordnung nicht existiert. Es wird ein neuer mit dem Wert aus dem Standardkonstruktor eingefügt.

Michael Anderson
quelle
2
Richtig, um zu vermeiden, dass Sie neue Einträge hinzufügen, finddie den Enditerator zurückgeben, wenn für einen bestimmten Schlüssel kein Element vorhanden ist.
Thomas Schaub
@ThomasSchaub Wie hoch ist findin diesem Fall die zeitliche Komplexität ?
KPMG
5
template<typename T, T X>
struct Default {
    Default () : val(T(X)) {}
    Default (T const & val) : val(val) {}
    operator T & () { return val; }
    operator T const & () const { return val; }
    T val;
};

<...>

std::map<KeyType, Default<ValueType, DefaultValue> > mapping;
Thomas Eding
quelle
3
Ändern Sie es dann so, dass es funktioniert. Ich werde mich nicht darum kümmern, einen Fall zu beheben, für den dieser Code nicht entwickelt wurde.
Thomas Eding
2

Der Wert wird mit dem Standardkonstruktor initialisiert, wie in den anderen Antworten angegeben. Es ist jedoch nützlich hinzuzufügen, dass bei einfachen Typen (integrale Typen wie int, float, pointer oder POD (Plan Old Data) -Typen) die Werte auf Null initialisiert (oder durch Wertinitialisierung auf Null gesetzt) ​​werden (was effektiv ist das gleiche), abhängig davon, welche Version von C ++ verwendet wird).

Unter dem Strich werden Karten mit einfachen Typen die neuen Elemente automatisch auf Null initialisieren. In einigen Fällen müssen Sie sich also nicht darum kümmern, den Standardanfangswert explizit anzugeben.

std::map<int, char*> map;
typedef char *P;
char *p = map[123],
    *p1 = P(); // map uses the same construct inside, causes zero-initialization
assert(!p && !p1); // both will be 0

Siehe Machen die Klammern nach dem Typnamen einen Unterschied zu new? für weitere Details zu diesem Thema.

das Schwein
quelle
2

Eine Problemumgehung ist die Verwendung von map::at()anstelle von []. Wenn kein Schlüssel vorhanden ist, wird ateine Ausnahme ausgelöst. Noch schöner ist, dass dies auch für Vektoren funktioniert und sich daher für die generische Programmierung eignet, bei der Sie die Karte gegen einen Vektor austauschen können.

Die Verwendung eines benutzerdefinierten Werts für nicht registrierte Schlüssel kann gefährlich sein, da dieser benutzerdefinierte Wert (wie -1) möglicherweise weiter unten im Code verarbeitet wird. Mit Ausnahmen ist es einfacher, Fehler zu erkennen.

Dean
quelle
1

Möglicherweise können Sie einen benutzerdefinierten Zuweiser angeben, der einen gewünschten Standardwert zuweist.

template < class Key, class T, class Compare = less<Key>,
       class Allocator = allocator<pair<const Key,T> > > class map;
VDVLeon
quelle
4
operator[]Gibt ein durch Aufrufen erstelltes Objekt zurück T(), unabhängig davon, was der Allokator tut.
sbi
1
@sbi: Ruft die Karte nicht die Allokatormethode auf construct? Das könnte man ändern, denke ich. Ich vermute jedoch eine constructFunktion, die nichts anderes tut als new(p) T(t);nicht gut geformt ist. EDIT: Im Nachhinein war das dumm, sonst wären alle Werte gleich: P Wo ist mein Kaffee ...
GManNickG
1
@GMan: Meine Kopie von C ++ 03 sagt (in 23.3.1.2), dass operator[]zurückgegeben wird (*((insert(make_pair(x, T()))).first)).second. Wenn mir also nichts fehlt, ist diese Antwort falsch.
sbi
Du hast recht. Aber das scheint mir falsch zu sein. Warum verwenden sie die Allokatorfunktion nicht zum Einfügen?
VDVLeon
2
@sbi: Nein, ich stimme zu, dass diese Antwort falsch ist, aber aus einem anderen Grund. Der Compiler arbeitet zwar insertmit a T(), aber innerhalb von insert wird der Allokator verwendet, um Speicher für einen neuen Speicher Taufzurufen constructund diesen Speicher mit dem angegebenen Parameter aufzurufen T(). Es ist also tatsächlich möglich, das Verhalten zu ändern operator[], damit es etwas anderes zurückgibt, aber der Allokator kann nicht unterscheiden, warum es aufgerufen wird. Selbst wenn wir constructden Parameter ignorieren und unseren speziellen Wert verwenden würden, würde dies bedeuten, dass jedes konstruierte Element diesen Wert hat, was schlecht ist.
GManNickG
0

Verwenden Sie std::map::insert().

Wenn Sie operator[]feststellen, dass ich zu spät zu dieser Party komme, aber wenn Sie sich für das Verhalten mit benutzerdefinierten Standardeinstellungen interessieren (dh das Element mit dem angegebenen Schlüssel suchen, wenn es nicht vorhanden ist, fügen Sie ein Element mit einem in die Karte ein Standardwert gewählt und einen Verweis entweder auf den neu eingefügten Wert oder auf den vorhandenen Wert zurückgegeben). Vor C ++ 17 steht Ihnen bereits eine Funktion zur Verfügung : std::map::insert(). insertwird nicht eingefügt, wenn der Schlüssel bereits vorhanden ist, sondern einen Iterator auf den vorhandenen Wert zurücksetzen.

Angenommen, Sie wollten eine Zuordnung von String zu Int und fügen einen Standardwert von 42 ein, wenn der Schlüssel noch nicht vorhanden war:

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

int count_answers( const std::string &question)
{
    auto  &value = answers.insert( {question, 42}).first->second;
    return ++value;
}

int main() {

    std::cout << count_answers( "Life, the universe and everything") << '\n';
    std::cout << count_answers( "Life, the universe and everything") << '\n';
    std::cout << count_answers( "Life, the universe and everything") << '\n';
    return 0;
}

welches 42, 43 und 44 ausgeben sollte.

Wenn die Kosten für die Erstellung des Kartenwerts hoch sind (wenn entweder das Kopieren / Verschieben des Schlüssels oder der Werttyp teuer ist), führt dies zu einem erheblichen Leistungsverlust, der meiner Meinung nach mit C ++ 17 umgangen werden würde try_emplace.

Dhavenith
quelle
0

Diese Vorlagenfunktion erweitert die Antwort https://stackoverflow.com/a/2333816/272642 und verwendet std::map's key_typeund mapped_typetypedefs, um den Typ von keyund abzuleiten def. Dies funktioniert nicht mit Containern ohne diese Typedefs.

template <typename C>
typename C::mapped_type getWithDefault(const C& m, const typename C::key_type& key, const typename C::mapped_type& def) {
    typename C::const_iterator it = m.find(key);
    if (it == m.end())
        return def;
    return it->second;
}

Dies ermöglicht Ihnen die Verwendung

std::map<std::string, int*> m;
int* v = getWithDefault(m, "a", NULL);

ohne die Argumente wie zu werfen std::string("a"), (int*) NULL.

Vortico
quelle