Проверка целого числа с помощью аффиксов во время компиляции или выполнения

Я реализую функцию под названием 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 ответа
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
    

    Эти функции делают ваши функции синтаксического анализа много проще, и есть и другое применение.

    Вот краткое изложение моих предложений:

    1. Рефакторинг ВСЕ. Найдите биты, которые можно использовать повторно и которые полезны сами по себе, и вытащите их в свои собственные функции. Это включает в себя не только подпроцессы синтаксического анализа (знаки синтаксического анализа, префиксы синтаксического анализа, суффиксы синтаксического анализа и т. Д.), Но также алгоритмы, которые вы используете повсюду (сравнения без регистра). И, конечно же, все тщательно протестируйте. Тестирование более мелких компонентов более крупных функций делает вас более уверенными в более крупных функциях.
    2. Не используйте параметры out; нет даже необязательных параметров out. Здесь я категорически не согласен с @ JDługosz: делать НЕТ скопировать что <filesystem> сделал. Это была катастрофа только потому, что у нас не было std::expected или std::error или структурированные привязки… не повторяйте ошибку. Вернуть результат structs для любых функций, которые возвращают что-либо, кроме одной «вещи». Для любых функций синтаксического анализа это будет по меньшей мере что было проанализировано, остальное, что было нет проанализирован (чтобы вы могли продолжить синтаксический анализ с другими функциями) и код ошибки, если произошел сбой синтаксического анализа. Возвращение structs это много более эргономичен, чем параметры out — даже необязательные параметры out — особенно со структурированными привязками.
    3. Не возвращайте ненужную информацию. Если ваша функция 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_literal

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

        Если мы определим диапазон допустимых символов (и код ошибки) вне цикла, нам не понадобится здесь оператор switch и дублирование.

      Добавить комментарий

      Ваш адрес email не будет опубликован. Обязательные поля помечены *