Usando char * como clave en std :: map

81

Estoy tratando de averiguar por qué el siguiente código no funciona y supongo que es un problema con el uso de char * como tipo de clave, sin embargo, no estoy seguro de cómo puedo resolverlo o por qué ocurre. Todas las demás funciones que uso (en el SDK de HL2) las utilizo, char*por lo que su uso std::stringva a causar muchas complicaciones innecesarias.

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
fuente
14
A veces, hacer lo correcto duele al principio. Cambie su código para usarlo std:stringuna vez y sea feliz después.
Björn Pollex
1
que tipo de complicaciones? hay una conversión implícita de char * a std :: string.
diez cuatro
1
No debe utilizar char*como clave de mapa. Vea mi respuesta por qué.
sbi
Esto parece ser una complicación innecesaria causada por no usar std::string.
Pedro d'Aquino
No entiendo, para usar una clave binaria, ¿no necesitaría el mapa saber si una clave es igual en lugar de saber que una clave tiene un valor 'menor que' otra?
CodeMinion

Respuestas:

140

Debe proporcionar un functor de comparación al mapa, de lo contrario, está comparando el puntero, no la cadena terminada en nulo a la que apunta. En general, este es el caso siempre que desee que la clave del mapa sea un puntero.

Por ejemplo:

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
fuente
2
en realidad, puede pasar el &std::strcmpcomo tercer parámetro de plantilla
Armen Tsirunyan
23
No, strcmpdevuelve un número entero positivo, cero o negativo. El functor del mapa debe devolver verdadero en menor que y falso en caso contrario.
aschepler
4
@Armen: No creo que funcione, ya que el tercer parámetro de la plantilla espera algo como f(a,b) = a<b, no f(a,b) = (-1 if a<b, 1 if a>b, 0 else).
kennytm
28
oh, lo siento, mi mal, no lo pensé antes de publicar. Que el comentario se quede ahí y avergüence a mi ascendencia :)
Armen Tsirunyan
2
Como probé, debe funcionar con una constante después del operador bool () (char const * a, char const * b), como bool operator () (char const * a, char const * b) const {blabla
ethanjyx
45

No puede usar a char*menos que esté absolutamente 100% seguro de que va a acceder al mapa con exactamente los mismos punteros , no cadenas.

Ejemplo:

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

Si accede al mapa con s1, obtendrá una ubicación diferente a la que accede con s2.

Pablo Santa Cruz
fuente
5
A menos que defina su propio comparador, como se explica en la respuesta aceptada.
Lukas Kalinski
23

Dos cadenas de estilo C pueden tener el mismo contenido pero estar en direcciones diferentes. Y eso mapcompara los punteros, no el contenido.

Es posible que el costo de conversión a std::map<std::string, int>no sea tanto como cree.

Pero si realmente necesita usar const char*como claves de mapa, intente:

#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
fuente
Gracias por la info. Según mi experiencia, recomiendo encarecidamente convertir a std :: string.
user2867288
8

Puede hacer que funcione std::map<const char*, int>, pero no debe usar no constpunteros (tenga en cuenta lo agregado constpara la clave), porque no debe cambiar esas cadenas mientras el mapa se refiere a ellas como claves. (Si bien un mapa protege sus claves al hacerlas const, esto solo constificaría el puntero , no la cadena a la que apunta).

Pero, ¿por qué no lo usas simplemente std::map<std::string, int>? Funciona de inmediato sin dolores de cabeza.

sbi
fuente
8

Estás comparando el uso de un char *con el uso de una cadena. Ellos no son los mismos.

A char *es un puntero a un char. En última instancia, es un tipo entero cuyo valor se interpreta como una dirección válida para a char.

Una cuerda es una cuerda.

El contenedor funciona correctamente, pero como contenedor de pares en los que la clave es a char *y el valor es an int.

Daniel Daranas
fuente
1
No es necesario que un puntero sea un número entero largo. Hay plataformas (como win64, si alguna vez ha oído hablar de él :-)) donde un entero largo es más pequeño que un puntero, y creo que también hay plataformas más oscuras donde los punteros y enteros se cargan en diferentes registros y se tratan de manera diferente en otras maneras. C ++ solo requiere que los punteros sean convertibles en algún tipo integral y viceversa; tenga en cuenta que esto no implica que pueda convertir un entero lo suficientemente pequeño como puntero, solo los que obtuvo al convertir un puntero.
Christopher Creutzig
@ChristopherCreutzig, gracias por tu comentario. Edité mi respuesta en consecuencia.
Daniel Daranas
2

Como dicen los demás, probablemente debería usar std :: string en lugar de char * en este caso, aunque en principio no hay nada de malo con un puntero como clave si eso es lo que realmente se requiere.

Creo que otra razón por la que este código no funciona es porque una vez que encuentra una entrada disponible en el mapa, intenta reinsertarla en el mapa con la misma clave (el carácter *). Dado que esa clave ya existe en su mapa, la inserción fallará. El estándar para map :: insert () define este comportamiento ... si el valor clave existe, la inserción falla y el valor mapeado permanece sin cambios. Luego se elimina de todos modos. Primero deberá eliminarlo y luego reinsertarlo.

Incluso si cambia el char * a std :: string, este problema permanecerá.

Sé que este hilo es bastante antiguo y ya lo has arreglado todo, pero no vi a nadie haciendo este punto, así que por el bien de los futuros espectadores, estoy respondiendo.

Toby Mitchell
fuente
0

Tuve dificultades para usar el char * como clave de mapa cuando trato de encontrar el elemento en varios archivos de origen. Funciona bien cuando todos los accesos / búsquedas están dentro del mismo archivo fuente donde se insertan los elementos. Sin embargo, cuando intento acceder al elemento usando buscar en otro archivo, no puedo obtener el elemento que definitivamente está dentro del mapa.

Resulta que la razón es que, como señaló Plabo , los punteros (cada unidad de compilación tiene su propio carácter constante *) NO son los mismos cuando se accede a ellos en otro archivo cpp.

Jie Xu
fuente
-5

No hay ningún problema para utilizar cualquier tipo de clave, siempre que soporta la comparación ( <, >, ==) y asignación.

Un punto que debe mencionarse: tenga en cuenta que está utilizando una clase de plantilla . Como resultado, el compilador generará dos instancias diferentes para char*y int*. Mientras que el código real de ambos será prácticamente idéntico.

Por lo tanto, consideraría usar a void*como tipo de clave y luego lanzar según sea necesario. Esta es mi opinión.

valdo
fuente
8
La clave solo necesita soporte <. Pero necesita implementar eso de una manera que sea útil. No es con punteros. Los compiladores modernos plegarán instancias de plantilla idénticas. (Sé con certeza que VC hace esto). Nunca lo usaría void*, a menos que la medición mostrara esto para resolver muchos problemas. El abandono de la seguridad de tipos nunca debe hacerse de forma prematura.
sbi