Używanie char * jako klucza w std :: map

81

Próbuję dowiedzieć się, dlaczego poniższy kod nie działa i przypuszczam, że jest to problem z użyciem znaku * jako typu klucza, jednak nie jestem pewien, jak mogę go rozwiązać i dlaczego tak się dzieje. Wszystkie inne funkcje, których używam (w HL2 SDK) używam, char*więc użycie std::stringspowoduje wiele niepotrzebnych komplikacji.

std::map<char*, int> g_PlayerNames;

int PlayerManager::CreateFakePlayer()
{
    FakePlayer *player = new FakePlayer();
    int index = g_FakePlayers.AddToTail(player);

    bool foundName = false;

    // Iterate through Player Names and find an Unused one
    for(std::map<char*,int>::iterator it = g_PlayerNames.begin(); it != g_PlayerNames.end(); ++it)
    {
        if(it->second == NAME_AVAILABLE)
        {
            // We found an Available Name. Mark as Unavailable and move it to the end of the list
            foundName = true;
            g_FakePlayers.Element(index)->name = it->first;

            g_PlayerNames.insert(std::pair<char*, int>(it->first, NAME_UNAVAILABLE));
            g_PlayerNames.erase(it); // Remove name since we added it to the end of the list

            break;
        }
    }

    // If we can't find a usable name, just user 'player'
    if(!foundName)
    {
        g_FakePlayers.Element(index)->name = "player";
    }

    g_FakePlayers.Element(index)->connectTime = time(NULL);
    g_FakePlayers.Element(index)->score = 0;

    return index;
}
Josh Renwald
źródło
14
Czasami właściwe postępowanie na początku boli. Zmień swój kod na std:stringjednorazowy i ciesz się później.
Björn Pollex
1
jakie komplikacje? istnieje niejawna konwersja z char * do std :: string.
tenfour
1
Nie możesz używać char*jako klucza mapy. Zobacz moją odpowiedź dlaczego.
sbi
To wydaje się być niepotrzebna komplikacja spowodowana przez nie używając std::string.
Pedro d'Aquino
Nie rozumiem, czy aby użyć klucza binarnego, czy Mapa nie musiałaby wiedzieć, czy klucz jest równy, zamiast wiedzieć, że klucz ma wartość „mniejszą niż” inny?
CodeMinion,

Odpowiedzi:

140

Musisz dać mapie funktor porównawczy, w przeciwnym razie porównuje on wskaźnik, a nie ciąg zakończony znakiem null, na który wskazuje. Generalnie dzieje się tak zawsze, gdy chcesz, aby klucz mapy był wskaźnikiem.

Na przykład:

struct cmp_str
{
   bool operator()(char const *a, char const *b) const
   {
      return std::strcmp(a, b) < 0;
   }
};

map<char *, int, cmp_str> BlahBlah;
GWW
źródło
2
właściwie może po prostu przekazać &std::strcmpjako trzeci parametr szablonu
Armen Tsirunyan
23
Nie, strcmpzwraca dodatnią, zerową lub ujemną liczbę całkowitą. Funktor mapy musi zwracać wartość true w przypadku mniejszego niż i fałsz w przeciwnym razie.
aschepler
4
@Armen: Myślę, że to nie działa, ponieważ trzeci parametr szablonu oczekuje czegoś takiego f(a,b) = a<b, że nie f(a,b) = (-1 if a<b, 1 if a>b, 0 else).
kennytm
28
och, przepraszam, moja wina, nie pomyślałem przed wysłaniem. Niech komentarz tam pozostanie i przyniesie wstyd moim przodkom :)
Armen Tsirunyan
2
Jak przetestowałem, musi działać ze stałą po operatorze bool () (char const * a, char const * b), jak operator bool () (char const * a, char const * b) const {blabla
ethanjyx
45

Nie możesz używać, char*chyba że masz absolutną 100% pewność, że uzyskasz dostęp do mapy z dokładnie tymi samymi wskaźnikami , a nie ciągami znaków.

Przykład:

char *s1; // pointing to a string "hello" stored memory location #12
char *s2; // pointing to a string "hello" stored memory location #20

Jeśli uzyskasz dostęp do mapy za pomocą s1, uzyskasz inną lokalizację niż dostęp do niej za pomocą s2.

Pablo Santa Cruz
źródło
5
Chyba że zdefiniujesz własny komparator, jak wyjaśniono w przyjętej odpowiedzi.
Lukas Kalinski
23

Dwa łańcuchy w stylu C mogą mieć taką samą zawartość, ale znajdować się pod różnymi adresami. I to mapporównuje wskaźniki, a nie zawartość.

Koszt konwersji na std::map<std::string, int>może nie być taki, jak myślisz.

Ale jeśli naprawdę potrzebujesz używać const char*jako kluczy mapy, spróbuj:

#include <functional>
#include <cstring>
struct StrCompare : public std::binary_function<const char*, const char*, bool> {
public:
    bool operator() (const char* str1, const char* str2) const
    { return std::strcmp(str1, str2) < 0; }
};

typedef std::map<const char*, int, StrCompare> NameMap;
NameMap g_PlayerNames;
aschepler
źródło
Dzięki za informację. Na podstawie mojego doświadczenia bardzo polecam konwersję na std :: string.
user2867288
8

Możesz sprawić, by działał z std::map<const char*, int>, ale nie możesz używać constwskaźników innych niż wskaźniki (zwróć uwagę na dodane constdla klucza), ponieważ nie możesz zmieniać tych ciągów, gdy mapa odnosi się do nich jako do kluczy. (Podczas gdy mapa chroni swoje klucze, tworząc je const, spowoduje to tylko utworzenie wskaźnika , a nie ciąg, na który wskazuje).

Ale dlaczego po prostu nie używasz std::map<std::string, int>? Działa po wyjęciu z pudełka bez bólów głowy.

sbi
źródło
8

Porównujesz użycie a char *do użycia ciągu. One nie są takie same.

A char *jest wskaźnikiem do znaku. Ostatecznie jest to typ całkowity, którego wartość jest interpretowana jako prawidłowy adres dla char.

Ciąg to ciąg.

Kontener działa poprawnie, ale jako kontener dla par, w których klucz to a, char *a wartość to int.

Daniel Daranas
źródło
1
Nie ma wymogu, aby wskaźnik był długą liczbą całkowitą. Istnieją platformy (takie win64, jeśli kiedykolwiek o nim słyszałeś :-)), na których długa liczba całkowita jest mniejsza niż wskaźnik, i uważam, że są też bardziej niejasne platformy, na których wskaźniki i liczby całkowite są ładowane do różnych rejestrów i traktowane inaczej w inaczej. C ++ wymaga tylko, aby wskaźniki były konwertowane na typ całkowity iz powrotem; zauważ, że nie oznacza to, że możesz rzutować dowolną wystarczająco małą liczbę całkowitą na wskaźnik, tylko te, które otrzymałeś z konwersji wskaźnika.
Christopher Creutzig
@ChristopherCreutzig, dziękuję za komentarz. Odpowiednio zredagowałem odpowiedź.
Daniel Daranas,
2

Jak mówią inni, prawdopodobnie powinieneś użyć std :: string zamiast char * w tym przypadku, chociaż w zasadzie nie ma nic złego w stosowaniu wskaźnika jako klucza, jeśli jest to naprawdę wymagane.

Myślę, że innym powodem, dla którego ten kod nie działa, jest to, że po znalezieniu dostępnego wpisu na mapie próbujesz ponownie wstawić go do mapy za pomocą tego samego klucza (znak *). Ponieważ ten klucz już istnieje w twojej mapie, wstawianie nie powiedzie się. Standard dla map :: insert () definiuje to zachowanie ... jeśli istnieje wartość klucza, funkcja wstawiania kończy się niepowodzeniem i odwzorowana wartość pozostaje niezmieniona. Następnie i tak zostanie usunięty. Najpierw musisz go usunąć, a następnie włożyć ponownie.

Nawet jeśli zmienisz znak * na std :: string ten problem pozostanie.

Wiem, że ten wątek jest dość stary i już to wszystko naprawiłeś, ale nie widziałem nikogo, kto by to zauważył, więc dla dobra przyszłych widzów odpowiadam.

Toby Mitchell
źródło
0

Trudno mi było używać znaku * jako klucza mapy, gdy próbowałem znaleźć element w wielu plikach źródłowych. Działa dobrze, gdy wszystkie uzyskiwanie dostępu / znajdowanie w tym samym pliku źródłowym, w którym wstawiane są elementy. Jednak gdy próbuję uzyskać dostęp do elementu za pomocą funkcji find w innym pliku, nie jestem w stanie pobrać elementu, który zdecydowanie znajduje się wewnątrz mapy.

Okazuje się, że powód jest taki, jak wskazał Plabo , wskaźniki (każda jednostka kompilacji ma swój własny stały znak *) NIE są w ogóle takie same, gdy uzyskuje się do niego dostęp w innym pliku cpp.

Jie Xu
źródło
0

std::map<char*,int>użyje wartości domyślnej std::less<char*,int>do porównania char*kluczy, co spowoduje porównanie wskaźników. Ale możesz określić własną klasę porównania w następujący sposób:

class StringPtrCmp {
    public:
        StringPtrCmp() {}

    bool operator()(const char *str1, const char *str2) const   {
        if (str1 == str2)
            return false; // same pointer so "not less"
        else
            return (strcmp(str1, str2) < 0); //string compare: str1<str2 ?
    }
};

std::map<char*, YourType, StringPtrCmp> myMap;

Pamiętaj, że musisz się upewnić, że wskaźnik char * jest prawidłowy. Radziłbym użyć std::map<std::string, int>mimo wszystko.

user1686153
źródło
-5

Nie ma problemu, aby użyć dowolnego klucza typ tak długo, jak obsługuje porównania ( <, >, ==) i przypisanie.

Jedna kwestia, o której należy wspomnieć - weź pod uwagę, że używasz klasy szablonu . W rezultacie kompilator wygeneruje dwie różne instancje dla char*i int*. Podczas gdy rzeczywisty kod obu będzie praktycznie identyczny.

Dlatego - rozważyłbym użycie a void*jako typu klucza, a następnie rzutowanie w razie potrzeby. To jest moja opinia.

valdo
źródło
8
Kluczowe muszą tylko wspierać < . Ale musi to zaimplementować w pomocny sposób. Nie ma wskaźników. Nowoczesne kompilatory będą składać identyczne instancje szablonów. (Wiem na pewno, że VC to robi.) Nigdy bym tego nie używał void*, chyba że pomiar wykazałby to, aby rozwiązać wiele problemów. Nigdy nie należy rezygnować z bezpieczeństwa typu przedwcześnie.
sbi