Qual é a maneira preferencial / idiomática de inserir em um mapa?


113

Eu identifiquei quatro maneiras diferentes de inserir elementos em um 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));

Qual dessas é a forma idiomática preferida? (E há outra maneira que eu não pensei?)


26
Seu mapa deve ser chamado de "respostas", não "função"
Vincent Robert

2
@Vincent: Hm? Uma função é basicamente um mapa entre dois conjuntos.
fredoverflow

7
@FredOverflow: parece que o comentário de Vincent é meio que piada sobre certo livro ...
Victor Sorokin

1
Parece contradizer o original - 42 não pode ser simultaneamente a resposta para (a) a vida, o universo e tudo, e (b) nada. Mas então como você expressa a vida, o universo e tudo como um int?
Stuart Golodetz

19
@sgolodetz Você pode expressar tudo com um int grande o suficiente.
Yakov Galka

Respostas:


90

Em primeiro lugar, operator[]e insertfunções membro não são funcionalmente equivalentes:

  • O operator[]irá procurar a chave, inserir um valor padrão construído se não for encontrado e retornar uma referência à qual você atribui um valor. Obviamente, isso pode ser ineficiente se o mapped_typepuder se beneficiar de ser inicializado diretamente, em vez de construir e atribuir o padrão. Este método também torna impossível determinar se uma inserção realmente ocorreu ou se você apenas substituiu o valor de uma chave inserida anteriormente
  • A insertfunção de membro não terá efeito se a chave já estiver presente no mapa e, embora seja freqüentemente esquecida, retorna um std::pair<iterator, bool>que pode ser de interesse (principalmente para determinar se a inserção foi realmente feita).

De todas as possibilidades de chamada listadas insert, todas as três são quase equivalentes. Como um lembrete, vamos dar uma olhada na insertassinatura no padrão:

typedef pair<const Key, T> value_type;

  /* ... */

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

Então, como as três chamadas são diferentes?

  • std::make_pairdepende da dedução do argumento do modelo e pode (e neste caso irá ) produzir algo de um tipo diferente do que o real value_typedo mapa, o que exigirá uma chamada adicional para o std::pairconstrutor do modelo a fim de converter para value_type(ou seja: adicionar consta first_type)
  • std::pair<int, int>também exigirá uma chamada adicional para o construtor do modelo de std::pairpara converter o parâmetro para value_type(ou seja: adicionar consta first_type)
  • std::map<int, int>::value_typenão deixa absolutamente nenhum lugar para dúvidas, pois é diretamente o tipo de parâmetro esperado pela insertfunção de membro.

No final, eu evitaria usar operator[]quando o objetivo fosse inserir, a menos que não houvesse nenhum custo adicional na construção e atribuição de default mapped_type, e não me importo em determinar se uma nova chave foi efetivamente inserida. Ao usar insert, construir um value_typeé provavelmente o caminho a percorrer.


a conversão de Key para const Key em make_pair () realmente requer outra chamada de função? Parece que uma conversão implícita seria suficiente para que o compilador ficasse feliz em fazê-lo.
galactica

99

A partir do C ++ 11, você tem duas opções adicionais principais. Primeiro, você pode usar insert()a sintaxe de inicialização de lista:

function.insert({0, 42});

Isso é funcionalmente equivalente a

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

mas muito mais conciso e legível. Como outras respostas observaram, isso tem várias vantagens em relação às outras formas:

  • A operator[]abordagem requer que o tipo mapeado seja atribuível, o que nem sempre é o caso.
  • A operator[]abordagem pode sobrescrever elementos existentes e não oferece nenhuma maneira de saber se isso aconteceu.
  • As outras formas insertlistadas envolvem uma conversão de tipo implícita, o que pode tornar seu código mais lento.

A principal desvantagem é que esse formulário costumava exigir que a chave e o valor fossem copiáveis, portanto, não funcionaria com, por exemplo, um mapa com unique_ptrvalores. Isso foi corrigido no padrão, mas a correção pode não ter atingido a implementação da biblioteca padrão ainda.

Em segundo lugar, você pode usar o emplace()método:

function.emplace(0, 42);

Isso é mais conciso do que qualquer uma das formas de insert(), funciona bem com tipos de movimento apenas como unique_ptr, e teoricamente pode ser um pouco mais eficiente (embora um compilador decente deva otimizar a diferença). A única grande desvantagem é que pode surpreender um pouco seus leitores, já que os emplacemétodos geralmente não são usados ​​dessa forma.


8
há também o novo insert_or_assign e try_emplace
sp2danny

11

A primeira versão:

function[0] = 42; // version 1

pode ou não inserir o valor 42 no mapa. Se a chave 0existir, ele atribuirá 42 a essa chave, sobrescrevendo qualquer valor que ela tivesse. Caso contrário, ele insere o par chave / valor.

As funções de inserção:

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

por outro lado, não faça nada se a chave 0já existir no mapa. Se a chave não existir, ele insere o par chave / valor.

As três funções de inserção são quase idênticas. std::map<int, int>::value_typeé o typedefpara std::pair<const int, int>e, std::make_pair()obviamente, produz uma std::pair<>mágica de dedução via template. O resultado final, no entanto, deve ser o mesmo para as versões 2, 3 e 4.

Qual devo usar? Eu pessoalmente prefiro a versão 1; é conciso e "natural". Claro, se seu comportamento de substituição não for desejado, então eu preferiria a versão 4, uma vez que requer menos digitação do que as versões 2 e 3. Não sei se existe uma única maneira de fato de inserir pares de chave / valor em um std::map.

Outra maneira de inserir valores em um mapa por meio de um de seus construtores:

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

Se você deseja substituir o elemento com a chave 0

function[0] = 42;

De outra forma:

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

5

Já que C ++ 17 std::map oferece dois novos métodos de inserção: insert_or_assign()e try_emplace(), como também mencionado no comentário de sp2danny .

insert_or_assign()

Basicamente, insert_or_assign()é uma versão "aprimorada" do operator[]. Em contraste com operator[], insert_or_assign()não exige que o tipo de valor do mapa seja construtível por padrão. Por exemplo, o código a seguir não é compilado, porque MyClassnão tem um construtor padrão:

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

No entanto, se você substituir myMap[0] = MyClass(1);pela linha a seguir, o código será compilado e a inserção ocorrerá conforme o esperado:

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

Além disso, semelhante a insert(), insert_or_assign()retorna a pair<iterator, bool>. O valor booleano é truese uma inserção ocorreu e falsese uma atribuição foi feita. O iterador aponta para o elemento que foi inserido ou atualizado.

try_emplace()

Semelhante ao anterior, try_emplace()é uma "melhoria" de emplace(). Em contraste com emplace(), try_emplace()não modifica seus argumentos se a inserção falhar devido a uma chave já existente no mapa. Por exemplo, o código a seguir tenta substituir um elemento por uma chave que já está armazenada no mapa (consulte *):

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

Saída (pelo menos para VS2017 e Coliru):

pMyObj não foi inserido
pMyObj foi modificado de qualquer maneira

Como você pode ver, pMyObjnão aponta mais para o objeto original. No entanto, se você substituir auto [it, b] = myMap2.emplace(0, std::move(pMyObj));pelo código a seguir, a saída parecerá diferente, porque pMyObjpermanece inalterada:

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

Resultado:

pMyObj não foi inserido
pMyObj pMyObj.m_i = 2

Código em Coliru

Observação: tentei manter minhas explicações o mais curtas e simples possível para encaixá-las nessa resposta. Para uma descrição mais precisa e abrangente, recomendo a leitura deste artigo sobre Fluent C ++ .


3

Tenho feito algumas comparações de tempo entre as versões acima mencionadas:

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

Acontece que as diferenças de tempo entre as versões de inserção são mínimas.

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

Isso dá respectivamente para as versões (executei o arquivo 3 vezes, daí as 3 diferenças de tempo consecutivas para cada uma):

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

2.198 ms, 2.078 ms, 2.072 ms

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

2.290 ms, 2.037 ms, 2.046 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)));

2.234 ms, 2.031 ms, 2.027 ms

Conseqüentemente, os resultados entre diferentes versões de inserção podem ser desprezados (embora não execute um teste de hipótese)!

A map_W_3[it] = Widget(2.0);versão leva cerca de 10-15% mais tempo para este exemplo devido a uma inicialização com o construtor padrão para Widget.


2

Em suma, o []operador é mais eficiente para atualizar valores porque envolve chamar o construtor padrão do tipo de valor e, em seguida, atribuir a ele um novo valor, embora insert()seja mais eficiente para adicionar valores.

O trecho citado de STL Efetivo: 50 Maneiras Específicas de Melhorar Seu Uso da Biblioteca de Modelos Padrão por Scott Meyers, Item 24 pode ajudar.

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

Você pode decidir escolher uma versão genérica-livre de programação disso, mas o ponto é que eu acho esse paradigma (diferenciar 'adicionar' e 'atualizar') extremamente útil.


1

Se você quiser inserir um elemento em std :: map - use a função insert (), e se você quiser encontrar o elemento (por chave) e atribuir algum a ele - use o operador [].

Para simplificar a inserção, use boost :: assign library, assim:

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

Acabei de mudar um pouco o problema (mapa de strings) para mostrar outro interesse de 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

o fato de que o compilador não mostra nenhum erro em "rancking [1] = 42;" pode ter um impacto devastador!


Compiladores não mostram um erro para o primeiro porque std::string::operator=(char)existe, mas mostram um erro para o último porque o construtor std::string::string(char)não existe. Não deve produzir um erro porque C ++ sempre interpreta livremente qualquer literal de estilo inteiro como char, então este não é um bug do compilador, mas sim um erro do programador. Basicamente, estou apenas dizendo que se isso introduz ou não um bug em seu código, é algo que você deve observar por si mesmo. BTW, você pode imprimir rancking[0]e um compilador usando ASCII irá imprimir *, que é (char)(42).
Keith M

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.