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

Static const std:: map <строка, int> vs if-elseif

Я пишу функцию, которая должна преобразовать строку в число. Я вижу два возможных варианта:

int convert(const std::string input) {
    if (input == "one") {
        return 1;
    } else if (input == "two") {
        return 2;
    }
    // etc.
    return 0;
}

или

int convert(const std::string input) {
    static const map<string, int> table = {
        {"one", 1},
        {"two", 2}
        // etc.
    }

    const auto result = table.find(input);

    if (result == table.end())
    {
        return 0;
    }

    return result->second;
}

Какой способ более эффективный/приемлемый/читаемый?

4b9b3361

Ответ 1

Ответ в значительной степени зависит от того, сколько различных строк вы будете поддерживать этим.

Несколько строк: перейдите к if-else. Усилия, необходимые для понимания кода позже, мало.

Много строк. Создайте карту. Усиление понимания кода мало по сравнению с усилием, читающим огромную конструкцию if-else. Возможно, вам придется часто распространять этот список. Добавление данных требует меньше ввода.

Я не уверен, как смарт-карта С++ использует строки как ключи. В худшем случае обе имеют одинаковую производительность. Если список становится действительно огромным, вы можете подумать о создании хэш-значения строк и использовать его в качестве ключа. Это может значительно повысить производительность. Вы должны будете убедиться, что столкновений не произойдет. (Хороший алгоритм хеширования и размер бита 64 бит должны быть достаточными.) Возможно, что современные реализации карт делают это уже.

Ответ 2

Для небольшого набора текста я бы использовал простую таблицу поиска:

struct LookupTable {
    const char* text;
    int value;
};
const LookupTable table[] = {
    { "one", 1 },
    { "two", 2 }
};
int convert(const char* text) {
    if (!text) return 0;
    for (int i=0; i<sizeof(table)/sizeof(LookupTable); i++) {
        if (strcasecmp(text, table[i].text) == 0 ) {
            return table[i].value;
        }
    }
    return 0;
}

Для большого набора текста я бы рассмотрел использование std::unordered_map<std::string,int> и, возможно, пользовательскую хэш-функцию (хеш bkdr или хэш эльфа хорош для слов).


ИЗМЕНИТЬ: Как заметил Давид в комментарии, если вы не хотите уродливого sizeof, используйте современный for-loop:

int convert(const char* text) {
    if (!text) return 0;
    for (auto& entry: table) {
        if (strcasecmp(text, entry.text) == 0 ) {
            return entry.value;
        }
    }
    return 0;
}

Ответ 3

An if-else (или switch, если они доступны вам) хороши для небольших случаев, и вы также можете контролировать порядок тестов, если наиболее распространенные тесты могут быстро вырезать поиск, вы можете сначала проверьте их.

Во многих случаях a switch намного лучше, чем список if-else s. Оба легче читать и, возможно, быстрее. Хотя switch не лучший выбор с string.

Однако вы можете использовать enum вместо использования строк; это, безусловно, лучший подход, за исключением map.

A map или std::unordered_map намного лучше для большого количества возможностей или когда вам нужны эти возможности, обновленные во время выполнения.

Ответ 4

Для небольшого числа возможных входных значений я бы предпочел решение 1, которое было бы простым и, вероятно, имело бы лучшую производительность.

Если список значений становится слишком большим, то вам действительно нужен конвертер между целыми числами и записанными числами, и это действительно другая история (см. библиотеку "Гуманизатор", на которую ссылается комментарий NathanOliver

Ответ 5

Я предлагаю map. Основная причина заключается в том, что он масштабируется лучше, в обоих возможных значениях слова.

Если вам нужно добавить дополнительные условия в будущем, что, скорее всего, более удобно и удобно использовать карту. Кроме того, он позволяет изменять временную версию таблицы поиска, что может быть очень полезно в некоторых контекстах.

Мне приходилось сталкиваться с подобным вопросом в том, что я разрабатываю, где подобный поиск должен быть модифицирован дочерними классами. Я решил, что карты предлагают большую гибкость. Карты позволяют мне определить виртуальную функцию, например getLookup(), которая возвращает таблицу поиска. В этой функции я могу сохранить статическую карту (которую я настроил так, как мне нужно, при первом вызове), специфичный для этого типа класса. Если вы рассматриваете подобное приложение, то я настоятельно рекомендую использовать карты для цепей. Если цепочки полностью неуправляемы в наследовании. Вы начнете спрашивать: "Как мне изменить то, что разрешает X?" рано или поздно, и будет очень мало практического ответа, кроме спагетти.

Еще один комментарий: рассмотрите unordered_map. Итерация диапазона представляется маловероятной для этого случая использования.

Ответ 6

Поиск if-else имеет сложность O (n), а поиск карты O (log n). Кроме того, когда список будет длиннее, инструкции if-else станут нечитаемыми. Поэтому карта лучше.

С другой стороны, относительно аргумента в объявлении функции:

int convert(const std::string input)

Я бы изменил его на pass-by-constant-reference вместо pass-by-constant-copy, чтобы быть более эффективным:

int convert(const std::string& input)

Ответ 7

Какой способ более эффективный/приемлемый/читаемый?

Решение if/else является наиболее эффективным, если у вас есть только несколько значений, и, безусловно, довольно просто, особенно для людей, не привыкших к стандартной библиотеке, однако быстро переходит в беспорядок.

Таким образом, как только вы достигнете 5 или более предметов, переключитесь на использование контейнера.

Предостережение: к сожалению, std::string_view, что позволит избежать выделения памяти, по-прежнему не является стандартным; для простоты я использую std::string, хотя, если выделение памяти является проблемой, std::string_view или пользовательский класс CStr будут лучше.

Существует 3 допустимых варианта:

  • std::map<std::string, int> и std::unordered_map<std::string, int> - самые интуитивные варианты, непонятно, что будет быстрее
  • std::vector<std::pair<std::string, int>> (отсортированный) всегда будет более эффективным, чем std::map<std::string, int>

Таким образом, если эффективность является проблемой:

int convert(std::string const& name) {
    static std::vector<std::pair<std::string, int>> const Table = []() {
        std::vector<std::pair<std::string, int>> result = {
            { "one", 1 },
            { "two", 2 },
            { "three", 3 },
            { "four", 4 }
        };
        std::sort(result.begin(), result.end());
        return result;
    }();

    auto const it =
        std::lower_bound(Table.begin(), Table.end(), std::make_pair(name, 0));

    if (it != Table.end() and it->first == name) {
        return it->second;
    }
    return 0;
}

Сортированный массив - это, в конце концов, самый эффективный способ выполнить бинарный поиск из-за лучшего поведения кэша. Он также должен превзойти std::unordered_map на небольших входах по тем же причинам.

Конечно, это немного менее читаемо.

Ответ 8

Я сделал некоторые грубые измерения из многих разных ответов здесь, а также пару моих собственных идей и для случая чисел "один" до девяти" на GCC обнаружил, что это было самым быстрым:

int convert(const std::string& input) {
    static const std::array<std::string, 9> numbers 
        = {"one", "two", "three", "four", "five", "six", "seven", "eight", "nine"};
    auto find_result = std::find(numbers.begin(), numbers.end(), input);
    if (find_result == numbers.end())
        return 0;
    return std::distance(numbers.begin(), find_result) + 1;
}

Мне кажется, что это также разумно "приемлемо" и "читаемо".

В любом из предложений нет большой разницы в производительности.

Результаты были похожи на Clang. Интересно, что совсем немного для Visual Studio 2015.

Ответ 9

Это одна из вещей макросы X отлично подходят для:

Это похоже на метод таблицы поиска @Calvin без необходимости отслеживать несколько наборов данных в нескольких местах.

//alphabetically sorted by string X macro

#define MAP_AS_ENUM(e,v,s) MYENUM_##e,
#define MAP_AS_STRING(e,v,s) s,
#define MAP_AS_VALUE(e,v,s) v,
#define MYMAP(OP) \
  OP(NONE,  -1,"") \
  OP(FIVE,  5, "five") \
  OP(FOUR,  4, "four") \
  OP(ONE,   1, "one") \
  OP(THREE, 3, "three") \
  OP(TWO,   2, "two") \
  OP(ZERO,  0, "zero")

enum myenums{ MYMAP(MAP_AS_ENUM) };
char *mystrings[] = { MYMAP(MAP_AS_STRING) };
char myvalues[]={ MYMAP(MAP_AS_VALUE) };

//now you can use a binary search on mystrings to get the index
//which will correspond to the associated enum