Я реализую функцию под названием is_proper_integer
который проверит строку, является ли она действительным целочисленным литералом, заимствуя идею из грамматики C ++.
Сигнатура функции:
constexpr bool is_proper_integer(std::string_view str,
std::optional<std::reference_wrapper<parse_integer_errc_t>> errc = {});
Где:
- функция объявлена как
constexpr
так что функцию можно использовать во время компиляции. str
имеет простой типstd::string_view
чтобы я мог изменить его, вызвавremove_prefix
иremove_suffix
в пределах определения.errc
действует как код ошибки, чтобы сообщить причину возвратаfalse
и я сделал этоstd::optional<std::reference_wrapper<...>>
потому что параметр, очевидно, по желанию и нетstd::optional<T&>
, поэтому я оборачиваю его в шаблон классаstd::reference_wrapper
.
Вот полезные утилиты для работы с функцией:
enum class parse_integer_errc_t {
normal,
foreign_char,
adjacent_radix_sep,
invalid_prefix,
invalid_radix_sep,
invalid_binary_fmt,
invalid_octal_fmt,
invalid_decimal_fmt,
invalid_hexadecimal_fmt,
unknown
};
enum class integer_prefix_t {
binary, octal, decimal, hexadecimal, unknown
};
std::ostream& operator<<(std::ostream& os, parse_integer_errc_t errc) {
switch (errc) {
using enum parse_integer_errc_t;
case normal: return os << "normal";
case foreign_char: return os << "foreign_char";
case adjacent_radix_sep: return os << "adjacent_radix_sep";
case invalid_prefix: return os << "invalid_prefix";
case invalid_radix_sep: return os << "invalid_radix_sep";
case invalid_binary_fmt: return os << "invalid_binary_fmt";
case invalid_octal_fmt: return os << "invalid_octal_fmt";
case invalid_decimal_fmt: return os << "invalid_decimal_fmt";
case invalid_hexadecimal_fmt: return os << "invalid_hexadecimal_fmt";
case unknown: return os << "unknown";
}
return os;
}
Основная реализация:
constexpr bool is_proper_integer(std::string_view str,
std::optional<std::reference_wrapper<parse_integer_errc_t>> errc = {}) {
constexpr char valid_chars[] = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'A', 'B', 'C', 'D', 'E', 'F',
'a', 'b', 'c', 'd', 'e', 'f'
};
/// span version
auto&& span_chars = std::span{valid_chars};
/// check if it has unary '-' or '+' operator
if (str.front() == '-' || str.front() == '+') str.remove_prefix(1);
/// check if the front character is valid (after '-')
if (
auto&& span_ = span_chars.first<10>();
std::ranges::find(span_, str.front()) == span_.end()
) {
if (errc.has_value()) errc->get() = parse_integer_errc_t::invalid_prefix;
return false;
}
/// check the prefix
integer_prefix_t flag_ = integer_prefix_t::unknown;
if (str.starts_with("0x") | str.starts_with("0X")) {
str.remove_prefix(2);
flag_ = integer_prefix_t::hexadecimal;
} else if (str.starts_with("0b") || str.starts_with("0B")) {
str.remove_prefix(2);
flag_ = integer_prefix_t::binary;
} else if (str.starts_with('0')) {
str.remove_prefix(1);
flag_ = integer_prefix_t::octal;
} else {
flag_ = integer_prefix_t::decimal;
}
/// check again...
if (str.starts_with(''')) {
if (errc.has_value()) errc->get() = parse_integer_errc_t::invalid_radix_sep;
return false;
}
/// check the suffix
if (str.ends_with("ull") ||
str.ends_with("ULL") ||
str.ends_with("uLL") ||
str.ends_with("Ull")
) str.remove_suffix(3);
else if (
str.ends_with("ul") ||
str.ends_with("uL") ||
str.ends_with("Ul") ||
str.ends_with("UL") ||
str.ends_with("ll") ||
str.ends_with("LL") ||
str.ends_with("uz") ||
str.ends_with("uZ") ||
str.ends_with("Uz") ||
str.ends_with("UZ")
) str.remove_suffix(2);
else if (
str.ends_with('z') ||
str.ends_with('Z') ||
str.ends_with('u') ||
str.ends_with('U') ||
str.ends_with('l') ||
str.ends_with('L')
) str.remove_suffix(1);
/// check again...
if (str.ends_with(''')) {
if (errc.has_value()) errc->get() = parse_integer_errc_t::invalid_radix_sep;
return false;
}
/// flag for radix grouping check
bool radix_sep_state { false };
/// normal loop checking
for (char c : str) {
if (c == ''') {
if (radix_sep_state) {
if (errc.has_value()) errc->get() = parse_integer_errc_t::adjacent_radix_sep;
return false;
}
radix_sep_state = true;
continue;
}
radix_sep_state = false;
if (std::ranges::find(span_chars, c) == span_chars.end()) {
if (errc.has_value()) errc->get() = parse_integer_errc_t::foreign_char;
return false;
}
switch (flag_) {
using enum integer_prefix_t;
case binary: {
auto&& span_ = span_chars.first<2>();
if (std::ranges::find(span_, c) == span_.end()) {
if (errc.has_value()) errc->get() = parse_integer_errc_t::invalid_binary_fmt;
return false;
}
break;
}
case octal: {
auto&& span_ = span_chars.first<8>();
if (std::ranges::find(span_, c) == span_.end()) {
if (errc.has_value()) errc->get() = parse_integer_errc_t::invalid_octal_fmt;
return false;
}
break;
}
case decimal: {
auto&& span_ = span_chars.first<10>();
if (std::ranges::find(span_, c) == span_.end()) {
if (errc.has_value()) errc->get() = parse_integer_errc_t::invalid_decimal_fmt;
return false;
}
break;
}
case hexadecimal: {
if (std::ranges::find(span_chars, c) == span_chars.end()) {
if (errc.has_value()) errc->get() = parse_integer_errc_t::invalid_hexadecimal_fmt;
return false;
}
break;
}
case unknown: {
if (errc.has_value()) errc->get() = parse_integer_errc_t::unknown;
return false;
}
}
}
if (errc.has_value()) errc->get() = parse_integer_errc_t::normal;
return true;
}
Проверка во время компиляции:
static_assert(is_proper_integer("1234")); /// normal case
static_assert(is_proper_integer("+1234")); /// positive
static_assert(is_proper_integer("-1234")); /// negative
static_assert(is_proper_integer("1234u")); /// has suffix 'u'
static_assert(is_proper_integer("-1234ull")); /// has suffix 'ull' (ofc, overflow, but still valid)
static_assert(is_proper_integer("1234uz")); /// has suffix 'uz'
static_assert(is_proper_integer("0b10010")); /// ok, binary
static_assert(!is_proper_integer("0b1012")); /// '2' strayed in binary fmt
static_assert(is_proper_integer("+0B11001U")); /// has suffix 'U'
static_assert(!is_proper_integer("ajsdsad")); /// obviously, invalid chars
static_assert(is_proper_integer("0172613")); /// octal, ok
static_assert(is_proper_integer("-012112")); /// ok
static_assert(is_proper_integer("+0xabc'def")); /// hexadecimal, ok
static_assert(is_proper_integer("-0XCAFEdead")); /// ok, case-insensitive
static_assert(is_proper_integer("0xdeadbeefull")); /// ok, has suffix 'ull'
static_assert(!is_proper_integer("deadbeef")); /// invalid prefix
static_assert(!is_proper_integer("12'3''2")); /// wrong radix grouping.
static_assert(is_proper_integer("0b1111'0000'1010ull")); /// ok
static_assert(!is_proper_integer("0123;1")); /// foreign character
Проверка времени выполнения:
std::vector<std::string_view> queues {
"1234", "+1234", "-1234", "1234u",
"-1234ull", "1234uz", "0b10010",
"0b1012", "+0B11001U", "ajsdsad",
"0172613", "-012112", "+0xabc'def",
"-0XCAFEdead", "0xdeadbeefull", "deadbeef",
"12'3''2", "0b1111'0000'1010ull", "0123;1"
};
parse_integer_errc_t errcode;
std::cout << std::boolalpha;
for (const auto& str : queues) {
std::cout
<< "String: " << std::setw(23) << std::left << str
<< "Is valid?: " << std::setw(8) << std::left
<< (is_proper_integer(str, errcode) ? "yes" : "no")
<< "Reason: " << std::setw(20) << std::left << errcode << 'n';
}
Выход:
String: 1234 Is valid?: yes Reason: normal
String: +1234 Is valid?: yes Reason: normal
String: -1234 Is valid?: yes Reason: normal
String: 1234u Is valid?: yes Reason: normal
String: -1234ull Is valid?: yes Reason: normal
String: 1234uz Is valid?: yes Reason: normal
String: 0b10010 Is valid?: yes Reason: normal
String: 0b1012 Is valid?: no Reason: invalid_binary_fmt
String: +0B11001U Is valid?: yes Reason: normal
String: ajsdsad Is valid?: no Reason: invalid_prefix
String: 0172613 Is valid?: yes Reason: normal
String: -012112 Is valid?: yes Reason: normal
String: +0xabc'def Is valid?: yes Reason: normal
String: -0XCAFEdead Is valid?: yes Reason: normal
String: 0xdeadbeefull Is valid?: yes Reason: normal
String: deadbeef Is valid?: no Reason: invalid_prefix
String: 12'3''2 Is valid?: no Reason: adjacent_radix_sep
String: 0b1111'0000'1010ull Is valid?: yes Reason: normal
String: 0123;1 Is valid?: no Reason: foreign_char
Могу ли я что-нибудь упростить?
3 ответа
функция объявлена как constexpr, чтобы ее можно было использовать во время компиляции.
хороший!
str имеет простой тип std :: string_view, чтобы я мог изменить его, вызвав remove_prefix и remove_suffix в определении.
хороший! это хорошая возможность для такого разбора.
errc действует как код ошибки, чтобы сообщить причину, по которой он возвращает false, и я сделал его std :: optionalstd :: reference_wrapper <...>, потому что параметр явно необязательный и нет std :: optional
, поэтому я оберните его внутри шаблона класса std :: reference_wrapper.
Вы можете рассмотреть две перегруженные формы с и без error_code&
out, как видно с <filesystem>
заголовок. Форма без — это всего лишь однострочный файл, который передает локальную переменную в обычную форму, по сути, отбрасывая ее. В функции нет сложного кода для работы с необязательным.
стандартные коды ошибок
Вы должны определить категория ошибки класс, который имеет правильную функцию для поиска сообщения об ошибке, а не operator<<
у вас есть.
Ваш автономный parse_integer_errc_t
должен быть установлен как error_code
.
повторные звонки
У вас длинный список .ends_with(xxx)
с разными струнами. Вы можете поместить все разрешенные суффиксы в простой массив и использовать цикл, который также автоматически подбирает длину суффикса, вместо того, чтобы использовать разные регистры.
Если вы сделаете чек, он скорее сложит футляр, чем .ends_with
вам не понадобится степень двойки длины суффикса для количества проверок!
constexpr string_view suffixes[]= {"ull","ul","l","uz","z","u"};
for (auto sfx : suffixes) {
if case-insentive-match of the end of the input,
remove the suffix and break
}
альтернативы
Гораздо более простой (но, вероятно, более медленный) способ проверки — использовать регулярное выражение. Существует библиотека Compile Time RegEx (работа в процессе), которая выглядит довольно красиво, что позволяет вам проверять строки времени компиляции.
Или вы можете просто позвонить from_chars
и посмотрите, есть ли у вас ошибка или нет, игнорируя полученное проанализированное целое число.
Могу ли я что-нибудь упростить?
Я собираюсь ответить на этот вопрос конкретно, а не давать общий обзор кода. И они отвечают: да … эта функция способ слишком большой, и способ слишком сложно. Это требует рефакторинга.
Простого разбиения функции на более мелкие логические блоки было бы достаточно само по себе; никто не хочет поддерживать 100-строчную функцию (или даже 50-строчную; если ваша функция длиннее 10 строк, это запах; если ваша единственная функция создает полосу прокрутки в CodeReview, это В самом деле запах). Но есть полдюжины вещей, которые делает эта функция, которые были бы полезны сами по себе. Их устранение не только принесет пользу будущим проектам, но и позволит тщательно протестировать эти более мелкие функции по отдельности, что было бы лучше для is_proper_integer()
слишком. (Например, он, вероятно, сразу же обнаружил бы ошибку в части проверки знаков.)
Например:
auto has_proper_integer_sign(std::string_view);
auto has_proper_integer_prefix(std::string_view);
auto has_proper_integer_suffix(std::string_view);
auto has_proper_integer_binary_digits(std::string_view);
auto has_proper_integer_octal_digits(std::string_view);
auto has_proper_integer_decimal_digits(std::string_view);
auto has_proper_integer_hexadecimal_digits(std::string_view);
С такими функциями гораздо проще написать и понять основную функцию:
auto is_proper_integer(std::string_view s)
{
return has_proper_integer_sign(s)
and has_proper_integer_prefix(s)
and has_proper_integer_suffix(s)
// and so on...
}
Это одно на самом деле не тот много помощи, поэтому я бы также предложил преобразовать каждый тест в функцию, которая действительно выполняет синтаксический анализ. Например:
enum class integer_parse_errc
{
// all the error codes you'll need for parsing integers
};
// of course, add all the machinery to make the above a legit system_error
// error code (like a category, is_error_code_enum, etc.)
enum class integer_sign_type
{
none,
positive,
negative
};
struct parse_integer_sign_result_t
{
integer_sign_type type = integer_sign_type::none;
std::string_view sign = {};
std::string_view rest = {};
integer_parse_errc ec = {};
};
constexpr auto parse_integer_sign(std::string_view s) noexcept -> parse_integer_sign_result_t
{
if (not s.empty())
{
auto const c = s.front();
auto const sign = s.substr(0, 1);
auto const rest = s.substr(1);
if (c == '+')
return {integer_sign_type::positive, sign, rest};
if (c == '-')
return {integer_sign_type::negative, sign, rest};
}
// or maybe you could consider being empty an error condition
//
// that's up to you
return {integer_sign_type::none, {}, s};
}
Теперь ваша функция может выглядеть примерно так:
constexpr auto is_proper_integer(std::string_view s)
{
// First parse the sign.
if (auto const res = parse_integer_sign(s); res.ec != integer_parse_errc{})
return false;
else
s = res.rest;
// Now parse the prefix
if (auto const res = parse_integer_prefix(s); res.ec != integer_parse_errc{})
return false;
else
s = res.rest;
// ... and so on
}
Более того, вы, вероятно, могли бы написать функцию, которая анализирует целое число на его компоненты, а затем основывает is_proper_integer()
на что:
struct parse_integer_result_t
{
// the parsed components:
sign_t sign = sign_t::none; // or positive/negative
prefix_t prefix = prefix_t::decimal; // or binary/octal/hexadecimal
suffix_t suffix = suffix_t::none; // or l/ll/z/u/ul/ull/uz
// the unparsed components:
std::string_view sign_part = {};
std::string_view prefix_part = {};
std::string_view suffix_part = {};
std::string_view number_part = {};
integer_parse_errc = {};
};
constexpr auto parse_integer_components(std::string_view s) noexcept
{
// for each component XXX, basically do:
// auto XXX_res = parse_integer_XXX(s);
// whittling down the string as you go
// if any return an error code, you're done, just return the error code
// if none of them return an error code, construct the full parse result
// from the component results you already have
}
constexpr auto is_proper_integer(std::string_view s) noexcept
{
return parse_integer_components(s).ec == integer_parse_errc{};
}
В parse_integer_components()
функция была бы полезна сама по себе, потому что вы могли бы взять информацию в компонентах и фактически проанализировать число, если хотите, или просто сделать что-то вроде того, чтобы убедиться, что число неотрицательно, или в шестнадцатеричной форме, или как угодно пожалуйста.
Это еще не все, что вы могли бы отказаться от этой функции. Еще одна функция, которую вы постоянно используете и которая также была бы полезна сама по себе, — это сравнения без регистра. Например, у вас есть:
if (str.ends_with("ull") ||
str.ends_with("ULL") ||
str.ends_with("uLL") ||
str.ends_with("Ull")
Это не просто уродливо, это неполно. Ты забыл llu
, LLu
, llU
, и LLU
.
Не лучше ли было бы написать:
if (ends_with_ci(str, "ull") or ends_with_ci(str, "llu"))
И ends_with_ci()
это функция, которую вы могли бы использовать и во многих других местах.
Возможно, имеет смысл написать:
constexpr auto starts_with_ci(std::string_view, char) noexcept -> bool;
constexpr auto starts_with_ci(std::string_view, std::string_view) noexcept -> bool;
constexpr auto ends_with_ci(std::string_view, char) noexcept -> bool;
constexpr auto ends_with_ci(std::string_view, std::string_view) noexcept -> bool;
constexpr auto equals_ci(char, char) noexcept -> bool;
constexpr auto equals_ci(std::string_view, char) noexcept -> bool;
constexpr auto equals_ci(char, std::string_view) noexcept -> bool;
constexpr auto equals_ci(std::string_view, std::string_view) noexcept -> bool;
// and maybe a find function that returns the found position or end,
// and a contains function that just uses the find function and returns bool.
//
// maybe also a rfind function.
//
// up to you
Эти функции делают ваши функции синтаксического анализа много проще, и есть и другое применение.
Вот краткое изложение моих предложений:
- Рефакторинг ВСЕ. Найдите биты, которые можно использовать повторно и которые полезны сами по себе, и вытащите их в свои собственные функции. Это включает в себя не только подпроцессы синтаксического анализа (знаки синтаксического анализа, префиксы синтаксического анализа, суффиксы синтаксического анализа и т. Д.), Но также алгоритмы, которые вы используете повсюду (сравнения без регистра). И, конечно же, все тщательно протестируйте. Тестирование более мелких компонентов более крупных функций делает вас более уверенными в более крупных функциях.
- Не используйте параметры out; нет даже необязательных параметров out. Здесь я категорически не согласен с @ JDługosz: делать НЕТ скопировать что
<filesystem>
сделал. Это была катастрофа только потому, что у нас не былоstd::expected
илиstd::error
или структурированные привязки… не повторяйте ошибку. Вернуть результатstruct
s для любых функций, которые возвращают что-либо, кроме одной «вещи». Для любых функций синтаксического анализа это будет по меньшей мере что было проанализировано, остальное, что было нет проанализирован (чтобы вы могли продолжить синтаксический анализ с другими функциями) и код ошибки, если произошел сбой синтаксического анализа. Возвращениеstruct
s это много более эргономичен, чем параметры out — даже необязательные параметры out — особенно со структурированными привязками. - Не возвращайте ненужную информацию. Если ваша функция
is_proper_integer()
, то нужно только вернутьbool
. Оно делает нет необходимо вернуть код ошибки (и, конечно, не код ошибки необязательной ссылки … который, кстати, действительно должен бытьparse_integer_errc_t* ec = nullptr
(хотя, что еще лучше, должны быть две разные функции, потому что необязательные аргументы — отстой), и вы должны сделатьif (ec != nullptr) *ec = whatever-error;
). Если вопрос: «Целое ли это число?», Это вопрос типа «да» или «нет». Если вопрос «почему это нет целое число? » это не тот же вопрос, что «это целое число?», поэтому это должна быть другая функция.is_proper_integer()
отвечает на вопрос «да» или «нет»;parse_integer_components()
отвечает на более сложный вопрос со всеми деталями, которые вам могут понадобиться.
В произвольном порядке:
enum class parse_integer_errc_t { normal, ...
Я думаю, если мы назовем это кодом ошибки (errc
), потомno_error
было бы более ясным именем, чемnormal
. С другой стороны, если целью является категоризация строки, то перечисление не следует называть «кодом ошибки».unknown
Думаю, эту категорию можно было бы удалить (как по коду ошибки, так и по типу префикса).Намного проще всегда требовать
parse_integer_errc_t&
параметр. Мы также могли бы предоставить перегруженную версию, принимающую только один параметр, который вызывает первый и внутренне отбрасывает код ошибки.auto&& span_chars = std::span{valid_chars};
В&&
не требуется (как в других объявлениях ниже).ошибка:
if (str.front() == '-' || str.front() == '+') str.remove_prefix(1);
Нам нужно сначала проверить, что строка не пуста !!! (Даже дляstd::string
это неопределенное поведение для пустой строки, иstd::string_view
не имеет таких же гарантий завершающего нуля).str.ends_with("ull")
Я думаю, это зависит от того, какой формат вы используете, ноllu
(и варианты регистра) также действительны для литералов C ++: https://en.cppreference.com/w/cpp/language/integer_literalauto&& span_ = span_chars.first<2>(); if (std::ranges::find(span_, c) == span_.end()) { if (errc.has_value()) errc->get() = parse_integer_errc_t::invalid_binary_fmt; return false; }
Если мы определим диапазон допустимых символов (и код ошибки) вне цикла, нам не понадобится здесь оператор switch и дублирование.