Zadanie

Przedstawiony kod źródłowy nie kompiluje się. Wyjaśnij dlaczego i przedstaw rozwiązanie.

#include <iostream>
#include <string>
#include <map>

void foo(std::map<int, std::string> const & m)
{
   // ...
   std::cout << "name: " << m[4] << '\n';
   // ...
}

int main()
{
   std::map<int, std::string> m { { 1, "Rafal"  },
                                  { 2, "Adam"   },
                                  { 3, "Artur"  },
                                  { 4, "Bjarne" } };
                                 
   foo(m);
}

Rozwiązanie

Kompilacja powyższego kodu źródłowego zakończy się następującym błędem:

main.cpp: In function ‘void foo(const std::map<int, std::basic_string<char> >&)’:
main.cpp:8:20: error: passing ‘const std::map >’ as ‘this’ argument discards qualifiers [-fpermissive]
    std::cout << m[4] << '\n';
                    ^
In file included from /usr/include/c++/6/map:61:0,
                 from main.cpp:3:
/usr/include/c++/6/bits/stl_map.h:494:7: note:   in call to ‘std::map<_Key, _Tp, _Compare, _Alloc>::mapped_type& std::map<_Key, _Tp, _Compare, _Alloc>::operator[](std::map<_Key, _Tp, _Compare, _Alloc>::key_type&&) [with _Key = int; _Tp = std::basic_string; _Compare = std::less; _Alloc = std::allocator > >; std::map<_Key, _Tp, _Compare, _Alloc>::mapped_type = std::basic_string; std::map<_Key, _Tp, _Compare, _Alloc>::key_type = int]’
       operator[](key_type&& __k)
       ^~~~~~~~

Przyczyną błędu jest wywołanie metody std::map::operator[] czyli operator ”[]” będący składową kontenera std::map (plik nagłówkowy map). Metoda ta nie jest oznaczona jako const, a więc nie możemy jej wywołać poprzez referencję do stałego obiektu std::map. Oznacza to, że metoda std::map::operator[]  może modyfikować kontener nawet w przypadku, gdy naszą intencją jest jedynie czytanie wartości dla danego klucza. Tak też jest w naszym przypadku – obiekt typu std::map jest przekazywany przez referencję do stałej do funkcji foo, w której czytamy wartość o określonym kluczu.

W efekcie powstały błąd kompilacji chroni nas przed przypadkową modyfikacją kontenera std::map. Aby się o tym przekonać do funkcji foo przekażemy nasz obiekt typu std::map poprzez referencję do niestałego obiektu (bez słowa kluczowego const):

#include <iostream>
#include <string>
#include <map>

void foo(std::map<int, std::string> & m)
{
   // ...
   std::cout << "name: " << m[0] << '\n';
   // ...
}

int main()
{
   std::map<int, std::string> m { { 1, "Rafal"  },
                                  { 2, "Adam"   },
                                  { 3, "Artur"  },
                                  { 4, "Bjarne" } };
                                 
   foo(m);
}

Uruchom w edytorze

name:

Jak widać przypadkowo odwołujemy się do elementu, który nie istnieje (w naszym przypadku o kluczu równym ”0”). Efekt jest niezgodny z naszą intencją bowiem tworzymy nowy element z kluczem równym ”0” i wartością domyślną, która w przypadku typu std::string (plik nagłówkowy string) jest pustym łańcuchem tekstowym.

W dalszej części artykułu przedstawię dwie metody rozwiązania tego problemu, odpowiednio za pomocą metody std::map::find oraz std::map::at.

Metoda std::find

Metoda std::map::find umożliwia wyszukiwanie elementów o określonym kluczu. W przypadku znalezienia elementu zwracany jest wskazujący na niego iterator. W przeciwnym wypadku zwracany jest iterator końcowy std::end. Wartość reprezentowana jest przez pole second (klucz przez pole first), bowiem element przechowywany jest bezpośrednio w obiekcie typu std::pair (plik nagłówkowy utility). Zobaczmy jak takie rozwiązanie sprawdza się w praktyce:

#include <iostream>
#include <string>
#include <map>

void foo(std::map<int, std::string> const & m)
{
   auto it = m.find(4);
   if (it != std::end(m))
   {
      // ...
      std::cout << "name: " << it->second << '\n';
      // ...
   }
   else
   {
      // ...
      std::cout << "Invalid key" << '\n';
      // ...
   }
}

int main()
{
   std::map<int, std::string> m { { 1, "Rafal"  },
                                  { 2, "Adam"   },
                                  { 3, "Artur"  },
                                  { 4, "Bjarne" } };
                                 
   foo(m);
}

Uruchom w edytorze

name: Bjarne

W przypadku odwołania się do elementu, który nie istnieje (na przykład dla klucza o wartości ”0”), otrzymamy następujący output:

Invalid key

Metoda std::at

Metoda std::map::at pojawiła się w standardzie C++11. Umożliwia ona bezpośredni odczyt wartości (zwracanej przez referencję) o zadanym kluczu, czyli to o co chodziło nam podczas wywoływania metody std::map::operator[]. W przeciwieństwie do metody std::map::operator[], metoda std::map::at występuje w dwóch wersjach, zarówno bez jak i ze słowem kluczowym const. To właśnie wersja ze słowem kluczowym const umożliwia nam odwołanie się do naszego obiektu (przekazanego przez referencję do stałego obiektu) z wnętrza funkcji foo. W przypadku gdy wskazany element nie istnieje rzucony zostanie wyjątek typu std::out_of_range. Zobaczmy jak takie rozwiązanie sprawdza się w praktyce:

#include <iostream>
#include <string>
#include <map>

void foo(std::map<int, std::string> const & m)
{
   try
   {
      // ...
      std::cout << "name: " << m.at(4) << '\n';
      // ...
   }
   catch (std::out_of_range const & e)
   {
      // ...
      std::cout << e.what() << " : invalid key" << '\n';
      // ...
   }
}

int main()
{
   std::map<int, std::string> m { { 1, "Rafal"  },
                                  { 2, "Adam"   },
                                  { 3, "Artur"  },
                                  { 4, "Bjarne" } };
                                 
   foo(m);
}

Uruchom w edytorze

name: Bjarne

W przypadku odwołania się do elementu, który nie istnieje (na przykład dla klucza o wartości ”0”), łapiemy wyjątek typu std::out_of_range otrzymując następujący output:

map::at : invalid key