valor padrão std :: map


89

Existe uma maneira de especificar o valor padrão std::map's operator[]retorna quando uma chave não existe?

Respostas:


58

Não, não existe. A solução mais simples é escrever sua própria função de modelo grátis para fazer isso. Algo como:

#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 );
}

Atualização C ++ 11

Objetivo: contabilizar contêineres associativos genéricos, bem como parâmetros opcionais de comparador e alocador.

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;
}

1
Ótima solução. Você pode querer adicionar alguns argumentos de modelo para que o modelo de função funcione com mapas que não usam os parâmetros de modelo padrão para comparador e alocador.
sbi

3
+1, mas para fornecer exatamente o mesmo comportamento do operator[]com o valor padrão, o valor padrão deve ser inserido no mapa dentro do if ( it == m.end() )bloco
David Rodríguez - dribeas

13
@David Estou assumindo que o OP não deseja realmente esse comportamento. Eu uso um esquema semelhante para ler as configurações, mas não quero que a configuração seja atualizada se uma chave estiver faltando.

2
Parâmetros booleanos do @GMan são considerados por alguns como sendo um estilo ruim porque você não pode dizer olhando para a chamada (em oposição à declaração) o que eles fazem - neste caso, "verdadeiro" significa "usar padrão" ou "não" t usar padrão "(ou algo totalmente diferente)? Um enum é sempre mais claro, mas é claro que é mais código. Estou dividido sobre o assunto, eu mesmo.

2
Essa resposta não funciona se o valor padrão for nullptr, mas stackoverflow.com/a/26958878/297451 sim.
Jon

45

Embora isso não responda exatamente à pergunta, contornei o problema com um código como este:

struct IntDefaultedToMinusOne
{
    int i = -1;
};

std::map<std::string, IntDefaultedToMinusOne > mymap;

1
Esta é a melhor solução para mim. Fácil de implementar, muito flexível e genérico.
acegs de

11

O padrão C ++ (23.3.1.2) especifica que o valor recém-inserido é construído por padrão, portanto map, ele próprio não fornece uma maneira de fazê-lo. Suas escolhas são:

  • Dê ao tipo de valor um construtor padrão que o inicializa com o valor que você deseja, ou
  • Envolva o mapa em sua própria classe que fornece um valor padrão e implementa operator[]para inserir esse padrão.

8
Bem, para ser preciso, o valor recém-inserido é o valor inicializado (8.5.5), então: - se T é um tipo de classe com um construtor declarado pelo usuário (12.1), então o construtor padrão para T é chamado (e a inicialização é inválida -formado se T não tiver nenhum construtor padrão acessível); - se T for um tipo de classe não-união sem um construtor declarado pelo usuário, então cada membro de dados não estático e componente da classe base de T é inicializado por valor; - se T é um tipo de array, então cada elemento é inicializado com valor; - caso contrário, o objeto é inicializado com zero
Tadeusz Kopec


6

Mais versão geral, suporte C ++ 98/03 e mais contêineres

Funciona com containers associativos genéricos, o único parâmetro de template é o próprio tipo de container.

Recipientes suportados: std::map, std::multimap, std::unordered_map, std::unordered_multimap, wxHashMap, QMap, QMultiMap, QHash, QMultiHash, etc.

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;
}

Uso:

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

Aqui está uma implementação semelhante usando uma classe wrapper, que é mais semelhante ao método get()do dicttipo em Python: 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);
}

Uso:

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

MAP :: mapped_type & não é seguro retornar, pois o nome de tipo MAP :: mapped_type & defval pode estar fora do escopo.
Joe C de

5

Não há como especificar o valor padrão - é sempre o valor construído pelo padrão (construtor de parâmetro zero).

Na verdade, operator[]provavelmente faz mais do que você espera, pois se um valor não existir para a chave fornecida no mapa, ele irá inserir um novo com o valor do construtor padrão.


2
Certo, para evitar adicionar novas entradas, você pode usar o findque retorna o iterador final se nenhum elemento existir para uma determinada chave.
Thomas Schaub

@ThomasSchaub Quanto é a complexidade de tempo findnesse caso?
ajaysinghnegi

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;

3
Em seguida, modifique-o para que funcione. Não vou me preocupar em consertar um caso para o qual esse código não foi projetado.
Thomas Eding

3

O valor é inicializado usando o construtor padrão, como dizem as outras respostas. No entanto, é útil adicionar que no caso de tipos simples (tipos integrais como int, float, ponteiro ou POD (dados antigos de plano)), os valores são inicializados por zero (ou zerados por inicialização de valor (que é efetivamente a mesma coisa), dependendo de qual versão do C ++ é usada).

De qualquer forma, o ponto principal é que os mapas com tipos simples inicializarão com zeros os novos itens automaticamente. Portanto, em alguns casos, não há necessidade de se preocupar em especificar explicitamente o valor inicial padrão.

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

Consulte Os parênteses após o nome do tipo fazem diferença com o novo? para mais detalhes sobre o assunto.


2

Uma solução alternativa é usar em map::at()vez de []. Se uma chave não existe,at lança uma exceção. Ainda melhor, isso também funciona para vetores e, portanto, é adequado para programação genérica, onde você pode trocar o mapa por um vetor.

Usar um valor personalizado para uma chave não registrada pode ser perigoso, pois esse valor personalizado (como -1) pode ser processado posteriormente no código. Com exceções, é mais fácil detectar bugs.


1

Talvez você possa fornecer a um alocador personalizado que aloque com um valor padrão que você deseja.

template < class Key, class T, class Compare = less<Key>,
       class Allocator = allocator<pair<const Key,T> > > class map;

4
operator[]retorna um objeto criado por invocação T(), não importa o que o alocador faça.
sbi

1
@sbi: O mapa não chama o constructmétodo dos alocadores ? Seria possível mudar isso, eu acho. No entanto, suspeito de uma constructfunção que não faz outra coisa senão new(p) T(t);não é bem formada. EDIT: Em retrospectiva, isso foi uma tolice, caso contrário, todos os valores seriam os mesmos: P Onde está meu café ...
GManNickG

1
@GMan: minha cópia do C ++ 03 diz (em 23.3.1.2) que operator[]retorna (*((insert(make_pair(x, T()))).first)).second. Portanto, a menos que esteja faltando alguma coisa, essa resposta está errada.
sbi

Você está certo. Mas isso parece errado para mim. Por que eles não usam a função de alocador para inserir?
VDVLeon

2
@sbi: Não, concordo que essa resposta está errada, mas por um motivo diferente. O compilador realmente faz insertcom um T(), mas dentro de inserir é quando ele usará o alocador, obterá memória para um novo e Tentão chamará constructessa memória com o parâmetro que foi fornecido, que é T(). Portanto, é realmente possível alterar o comportamento de fazer operator[]com que ele retorne algo diferente, mas o alocador não consegue diferenciar por que está sendo chamado. Portanto, mesmo que constructignorássemos seu parâmetro e usássemos nosso valor especial, isso significaria que cada elemento construído tinha esse valor, o que é ruim.
GManNickG

1

Expandindo a resposta https://stackoverflow.com/a/2333816/272642 , este modelo usa a função std::maps' key_typee mapped_typetypedefs para deduzir o tipo de keye def. Isso não funciona com contêineres sem esses 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;
}

Isso permite que você use

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

sem precisar lançar os argumentos como std::string("a"), (int*) NULL.


1

Usar std::map::insert() .

Percebi que estou bem atrasado para esta festa, mas se você estiver interessado no comportamento de operator[]padrões personalizados (ou seja, encontre o elemento com a chave fornecida, se não estiver presente, insira um elemento no mapa com um valor padrão escolhida e retornar uma referência, quer ao valor recém-inseridos ou o valor existente), já existe uma função disponível para você pré C ++ 17: std::map::insert(). insertnão irá realmente inserir se a chave já existir, mas em vez disso, retornará um iterador para o valor existente.

Digamos que você queira um mapa de string-to-int e insere um valor padrão de 42 se a chave ainda não estiver presente:

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;
}

que deve produzir 42, 43 e 44.

Se o custo de construir o valor do mapa for alto (se copiar / mover a chave ou o tipo de valor for caro), isso terá uma penalidade de desempenho significativa, que acho que seria contornada com o C ++ 17 try_emplace.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.