Використання char * як ключа в std :: map


81

Я намагаюся зрозуміти, чому наступний код не працює, і я припускаю, що це проблема з використанням символу char * як типу ключа, однак я не впевнений, як я можу його вирішити або чому це відбувається. Всі інші функції, якими я користуюся (у HL2 SDK), char*тому їх використання std::stringспричинить багато зайвих ускладнень.

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

14
Іноді спочатку боляче робити правильно, боляче. Змініть свій код на використання std:stringодин раз, і будьте щасливі після цього.
Björn Pollex

1
що за ускладнення? відбувається неявне перетворення символу char * у std :: string.
десять чотири

1
Ви не повинні використовувати char*як ключ карти. Дивіться мою відповідь чому.
sbi

Це здається непотрібним ускладнення викликано НЕ використовуючи std::string.
Педро д'Аквіно,

Я не розумію, для того, щоб використовувати двійковий ключ, чи не потрібно Карті знати, чи рівний ключ, замість того, щоб знати, що ключ має значення "менше" іншого?
CodeMinion

Відповіді:


140

Вам потрібно дати функтор порівняння з картою, інакше він порівнює покажчик, а не рядок із нульовим закінченням, на який вказує. Загалом, це трапляється будь-коли, коли ви хочете, щоб ключ вашої карти був вказівником.

Наприклад:

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

map<char *, int, cmp_str> BlahBlah;

2
насправді він може просто передати &std::strcmpтретій параметр шаблону
Армен Цирунян

23
Ні, strcmpповертає додатне, нульове або від’ємне ціле число. Функтор карти повинен повернути true на менше, ніж і false в іншому випадку.
aschepler

4
@Armen: Я не думаю, що це працює, оскільки параметр 3-го шаблону очікує щось подібне f(a,b) = a<b, ні f(a,b) = (-1 if a<b, 1 if a>b, 0 else).
kennytm

28
ой, вибачте, мій поганий, не думав перед публікацією. Нехай коментар залишиться там і принесе ганьбу моєму походженню :)
Армен Цирунян

2
Як я тестував, він повинен працювати з const після оператора bool () (char const * a, char const * b), як оператор bool () (char const * a, char const * b) const {blabla
ethanjyx

45

Ви не можете використовувати, char*якщо ви не на 100% впевнені, що збираєтесь отримати доступ до карти з однаковими вказівниками , а не рядками.

Приклад:

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

Якщо ви отримаєте доступ до карти разом, s1ви отримаєте інше місце, ніж доступ до неї s2.


5
Якщо ви не визначите власний компаратор, як це пояснюється у прийнятій відповіді.
Лукас Калінський

23

Два рядки у стилі С можуть мати однаковий вміст, але бути за різними адресами. І це mapпорівнює покажчики, а не зміст.

Вартість конвертації в std::map<std::string, int>може бути не такою, як ви думаєте.

Але якщо вам справді потрібно використовувати const char*ключі карти, спробуйте:

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

Дякую за інформацію. Виходячи зі свого досвіду, я настійно рекомендую перейти на std :: string.
user2867288

8

Ви можете з ним працювати std::map<const char*, int>, але не повинні використовувати constнепоказки (зверніть увагу на доданий constключ), оскільки ви не повинні змінювати ці рядки, поки карта посилається на них як на ключі. (Хоча карта захищає свої ключі, роблячи їх const, це лише узгоджує покажчик , а не рядок, на який вона вказує.)

Але чому ви просто не використовуєте std::map<std::string, int>? Це працює нестандартно без головного болю.


8

Ви порівнюєте за допомогою a char *до використання рядка. Вони не однакові.

A char *- вказівник на символ. Зрештою, це цілий тип, значення якого інтерпретується як дійсна адреса для char.

Рядок - це рядок.

Контейнер працює коректно, але як контейнер для пар, у яких ключ - a, char *а значення - an int.


1
Немає вимоги, щоб вказівник мав довге ціле число. Є платформи (наприклад, win64, якщо ви коли-небудь чули про це :-)), де довге ціле число менше, ніж вказівник, і я вважаю, що є також більш незрозумілі платформи, де покажчики та цілі числа завантажуються в різні регістри та по-різному обробляються інші способи. С ++ вимагає лише, щоб покажчики були конвертовані в якийсь інтегральний тип і назад; зауважте, це не означає, що ви можете передати будь-яке досить мале ціле число у покажчик, лише те, що ви отримали від перетворення вказівника.
Крістофер Кройціг

@ChristopherCreutzig, дякую за ваш коментар. Відповідно я відредагував свою відповідь.
Даніель Даранас,

2

Як кажуть інші, вам, мабуть, слід використовувати std :: string замість char *, хоча в принципі немає нічого поганого з покажчиком як ключем, якщо це те, що насправді потрібно.

Я думаю, ще одна причина, через яку цей код не працює, полягає в тому, що як тільки ви знайдете доступний запис на карті, ви намагаєтесь знову вставити його на карту тим же ключем (символом *). Оскільки цей ключ вже існує на вашій карті, вставка не вдасться. Стандарт поведінки map :: insert () визначає цю поведінку ... якщо значення ключа існує, вставка не вдається, і відображене значення залишається незмінним. Потім він все одно видаляється. Спочатку його потрібно видалити, а потім знову вставити.

Навіть якщо ви зміните char * на std :: string, ця проблема залишиться.

Я знаю, що ця тема досить стара, і ви вже все це виправили, але я не бачив, щоб хтось робив це, тому заради майбутніх глядачів відповідаю.


0

Мені було важко використовувати char * як ключ карти, коли я намагаюся знайти елемент у декількох вихідних файлах. Це чудово працює, коли всі доступ / пошук у тому самому вихідному файлі, куди вставляються елементи. Однак, коли я намагаюся отримати доступ до елемента за допомогою find в іншому файлі, я не можу отримати елемент, який точно знаходиться всередині карти.

Виявляється, причина в тому, як зазначив Плабон , вказівники (кожен блок компіляції має свій власний константний символ *) взагалі НЕ однакові, коли до нього звертаються в іншому файлі cpp.


0

std::map<char*,int>використовуватиме за замовчуванням std::less<char*,int>для порівняння char*ключів, що зробить порівняння покажчика. Але ви можете вказати власний клас порівняння таким чином:

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;

Майте на увазі, що ви повинні переконатися, що покажчик char * є дійсним. Я б порадив std::map<std::string, int>все-таки використовувати .


-5

Там немає ніяких проблем , щоб використовувати будь-який тип ключа до тих пір , як вона підтримує порівняння ( <, >, ==) і призначення.

Один момент, який слід згадати - врахуйте, що ви використовуєте клас шаблону . Оскільки компілятор результатів генерує дві різні інстанції для char*і int*. Тоді як фактичний код обох буде практично однаковим.

Отже - я б подумав про використання void*ключа як типу ключа, а потім за необхідності приведення. Це моя думка.


8
Вони ключові лише для підтримки <. Але це потрібно реалізувати корисним чином. Це не з покажчиками. Сучасні компілятори складатимуть однакові екземпляри шаблонів. (Я точно знаю, що VC це робить.) Я б ніколи не використовував void*, якщо вимірювання не показали цього, щоб вирішити багато проблем. Покидання безпеки типу ніколи не слід робити передчасно.
sbi
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.