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;
}
Какой способ более эффективный/приемлемый/читаемый?
Ответы
Ответ 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