Подтвердить что ты не робот

Избегайте создания ключа для std:: map:: find()

Предположим, что у меня есть std::map<std::string, int>. std::string можно сравнить с строками C (const char *) без std::string временных рядов. Тем не менее, map::find(), похоже, заставляет меня создать временный std::string, который, вероятно, требует выделения памяти. Как мне избежать этого? Концептуально это легко, но STL, похоже, предотвращает это.

#include <map>

int main()
{
    std::map<std::string, int> m;
    m.find("Olaf");
}
4b9b3361

Ответ 1

Ваша забота реальна, и нет хорошего обходного пути для С++ 11.

С++ 14 исправляет эту проблему, добавляя шаблонную перегрузку std::map::find - соответствующее предложение N3657. В С++ 14 ваша программа будет выглядеть так:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <map>
#include <algorithm>

class std_string {
    char *m_s;
public:
    std_string() { m_s = nullptr; }
    std_string(const char* s) { puts("Oops! A new std_string was constructed!"); m_s = strdup(s); }
    ~std_string() { free(m_s); }
    std_string(std_string&& ss) = delete;
    std_string(const std_string& ss) = delete;
    std_string& operator=(std_string&& ss) = delete;
    std_string& operator=(const std_string& ss) = delete;

    bool operator< (const char* s) const { return strcmp(m_s, s) < 0; }
    bool operator< (const std_string& ss) const { return strcmp(m_s, ss.m_s) < 0; }
    friend bool operator< (const char* s, const std_string& ss) { return strcmp(s, ss.m_s) < 0; }
};

int main()
{
    {
        puts("The C++11 way makes a copy...");
        std::map<std_string, int> m;
        auto it = m.find("Olaf");
    }
    {
        puts("The C++14 way doesn't...");
        std::map<std_string, int, std::less<>> m;
        auto it = m.find("Olaf");
    }
}

(std::less<> - обобщенный компаратор "меньше", эквивалентный operator<. С++ 03 и С++ 11 имеют версию этого компаратора с разбитым дизайном, которая заставляет оба аргумента быть тот же тип. С++ 14, наконец, делает это правильно.)

К сожалению, Комитет, похоже, решил, что люди должны пройти весь свой код на С++ 11 и обновить каждый контейнер, чтобы использовать std::less<> в качестве компаратора - по умолчанию это происходит не просто. Нет веских оснований для этого решения; это просто Путь. (См. Мои комментарии выше о сломанном дизайне. С++ имеет плохую привычку вводить сломанные версии вещей, прежде чем вводить "настоящие" версии несколько лет спустя.)

Для С++ 11 std::map::find имеет только одну перегрузку (та, которая принимает const Key&), поэтому любое обходное решение обязательно будет включать изменение типа Key, чтобы быть менее дорогостоящим - мы не можем просто возиться с компаратор, потому что к тому времени, когда выполнение достигнет компаратора, мы уже повысили аргумент find до типа Key.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <map>
#include <algorithm>

class std_string {
    char *m_s;
public:
    std_string() : m_s(nullptr) { }
    std_string(const char* s) : m_s(strdup(s)) { puts("Oops! A new std_string was constructed!"); }
    ~std_string() { free(m_s); }
    std_string(std_string&& ss) : m_s(nullptr) { std::swap(m_s, ss.m_s); }
    std_string(const std_string& ss) : m_s(strdup(ss.data())) { puts("Oops! A new std_string was constructed!"); }
    std_string& operator=(std_string&& ss) = delete;
    std_string& operator=(const std_string& ss) = delete;

    const char* data() const { return m_s; }

    bool operator< (const char* s) const { return strcmp(m_s, s) < 0; }
    bool operator< (const std_string& ss) const { return strcmp(m_s, ss.m_s) < 0; }
    friend bool operator< (const char* s, const std_string& ss) { return strcmp(s, ss.m_s) < 0; }
};

struct string_or_ptr {
    union {
        const char* ptr;
        alignas(std_string) unsigned char str[sizeof (std_string)];
    } m_u;
    bool m_deep;

    char const* & ptr() { return m_u.ptr; }
    std_string& str() { return *reinterpret_cast<std_string*>(m_u.str); }
    char const* const & ptr() const { return m_u.ptr; }
    std_string const& str() const { return *reinterpret_cast<const std_string*>(m_u.str); }

    string_or_ptr() : m_deep(false) { ptr() = ""; }
    string_or_ptr(const char* s) : m_deep(false) { ptr() = s; }
    string_or_ptr(std_string&& s) : m_deep(true) { new ((void*)&str()) std_string(std::move(s)); }
    string_or_ptr(const std_string& s) : m_deep(true) { new ((void*)&str()) std_string(s); }
    ~string_or_ptr() { if (m_deep) str().~std_string(); }
    std_string& operator=(std_string&& ss) = delete;
    std_string& operator=(const std_string& ss) = delete;


    operator const char*() const { return m_deep ? str().data() : ptr(); }

    bool operator< (const char* s) const { return strcmp((const char*)*this, s) < 0; }
    bool operator< (const std_string& ss) const { return (const char*)*this < ss; }
    bool operator< (const string_or_ptr& sp) const { return strcmp((const char*)*this, (const char*)sp) < 0; }
    friend bool operator< (const char* s, const string_or_ptr& sp) { return strcmp(s, (const char*)sp) < 0; }
    friend bool operator< (const std_string& ss, const string_or_ptr& sp) { return ss < (const char*)sp; }
};

int main()
{
    {
        puts("The C++11 way...");
        std::map<std_string, int> m;
        auto it = m.find("Olaf");
    }
    {
        puts("The C++11 way with a custom string-or-pointer Key type...");
        std::map<string_or_ptr, int> m;
        auto it = m.find("Olaf");
    }
}

Ответ 2

На самом деле нет способа заставить find использовать оператор сравнения, отличный от того, который использовался для создания map. Если бы вы могли передать другое в find, как бы это могло гарантировать, что оба сравнения будут обеспечивать одинаковый порядок?

Вместо этого просто подумайте о случаях:

1) Вы проезжаете char* в своей программе. В этом случае просто не делайте этого. Используйте std::string вместо этого, создавая его один раз, если необходимо, как можно ближе к источнику. Тогда преобразование не требуется.

2) Вы пытаетесь найти строковые литералы. В этом случае, почему ключ a string? Вместо этого введите ключ:

enum names { OLAF };
map<names, int> m;
m.find(OLAF);

3) Вы хотите найти строки и строковые литералы. В этом случае я бы создал глобальную таблицу поиска строк, индексированных перечислением, но построенных один раз в начале main. Тогда вы назовёте что-то вроде m.find(global_strings[OLAF]);

РЕДАКТИРОВАТЬ: Вы, похоже, очень сосредоточены/обеспокоены последствиями производительности string здесь. Профилировали ли вы свое приложение и обнаружили, что выделение string значительная часть вашего времени приложения? Я бы, конечно, поверил этому в встроенные системы/устройства.

Кроме того, вы отметили свой вопрос С++, но, похоже, вы отказываетесь использовать С++, построенную в строковой функции, которая делает гораздо больше, чем "стоимость вашей производительности". Он предоставляет множество полезных функций/методов/операторов, но, самое главное, он управляет памятью для вас, поэтому вы не тратите дни или недели на поиски этих действительно коварных ошибок, которые, несомненно, будут возникать.

Если вы читаете данные переменной длины из сети, я не могу понять разницу в производительности между char* buffer = new char[needed_size]; и чем-то вроде std::string s; s.resize(needed_size);, кроме того, что с помощью string обеспечивает определенную безопасность и управление памятью для вас.

Ответ 3

Если построение строки из литерала действительно является узким местом производительности, вы можете использовать свой собственный класс вместо std::string, который содержит либо строку, либо указатель на литерал. Недостатком является некоторое дополнительное усложнение, а также добавление размера указателя на элементы, которые вы вставляете в контейнер. Обратите внимание, что значение неизменно, как требуется map, поэтому безопасно хранить результаты c_str.

class mystring
{
    std::string  str;
    const char * value;
public:
    mystring() : value(NULL)
    {
    }
    void setString(const std::string & s)
    {
        assert(value == NULL);
        str = s;
        value = str.c_str();
    }
    void setLiteral(const char * s)
    {
        assert(value == NULL);
        value = s;
    }
    bool operator<(const mystring & rhs)
    {
        return strcmp(literal, rhs.literal) < 0;
    }
};

std::map<mystring, int> m;
mystring text;
text.setString(some_key);
m.insert(std::make_pair(text, some_data));
// ...
mystring key;
key.setLiteral("Olaf");
m[key] = new_value;