编译时间文本到数字转换 (atoi)

Posted

技术标签:

【中文标题】编译时间文本到数字转换 (atoi)【英文标题】:Compile time text to number translation (atoi) 【发布时间】:2020-04-19 11:04:19 【问题描述】:

我想在编译时实现 atoi() 函数(在 C++ 语言中,使用 C++11 或 C++14 标准)。所以它应该能够将双引号括起来的文本解析为数字,或者报告错误。更具体地说,它是更大系统的一部分,能够在编译时解析类似 printf 的格式。而且我想在单词上拆分格式字符串,如果某些特定单词可以用数字表示 - 输出数字而不是字符串(场景后面是序列化程序类,它可以比字符串更有效地序列化数字,而且更重要的是,反序列化器不应该尝试将每个字符串解析为数字,因为在格式字符串中打印的所有数字始终表示为数字,而不是字符串)...

据我所知,有两种方法可以解决该任务:

1) 使用 constexpr 函数;

2) 通过模板元编程。

哪种方式更好?我已经尝试过第一种方式,我可以看到这种方式有很多障碍:特别是与 c++11 相关的限制很少。看起来第二个可能更可取,但它需要一些技巧(您需要使用 of operator"" 将 c-string 拆分为分隔字符,从 c++14 开始的 gcc 和从 c++11 开始的 clangs 支持)。此外,完全基于 TMP 的解决方案可能太大且太复杂。

以下是我的解决方案,我很高兴听到一些建议。

http://coliru.stacked-crooked.com/a/0b8f1fae9d9b714b


#include <stdio.h>

template <typename T> struct Result

    T value;
    bool valid;

    constexpr Result(T v) : value(v), valid(true) 
    constexpr Result() : value(), valid(false) 
;

template <typename T>
constexpr Result<T> _atoi_oct(const char *s, size_t n, T val, int sign)

    return n == 0 ? Result<T>(sign < 0 ? -val : val)
        : *s >= '0' && *s <= '7' 
            ? _atoi_oct(s+1, n-1, val*T(010) + *s - '0', sign)
            : Result<T>();


template <typename T>
constexpr Result<T> _atoi_dec(const char *s, size_t n, T val, int sign)

    return n == 0 ? Result<T>(sign < 0 ? -val : val)
        : *s >= '0' && *s <= '9'
            ? _atoi_dec(s+1, n-1, val*T(10) + *s - '0', sign)
            : Result<T>();


template <typename T>
constexpr Result<T> _atoi_hex(const char *s, size_t n, T val, int sign)

    return n == 0 ? Result<T>(sign < 0 ? -val : val)
        : *s >= '0' && *s <= '9'
            ? _atoi_hex(s+1, n-1, val*T(0x10) + *s - '0', sign)
            : *s >= 'a' && *s <= 'f'
                ? _atoi_hex(s+1, n-1, val*T(0x10) + *s - 'a' + 10, sign)
                : *s >= 'A' && *s <= 'F'
                    ? _atoi_hex(s+1, n-1, val*T(0x10) + *s - 'A' + 10, sign)
                    : Result<T>();


template <typename T>
constexpr Result<T> _atoi_zero(const char *s, size_t n, int sign = 1)

    return n == 0 ? Result<T>()
        : *s >= '0' && *s <= '7'
            ? _atoi_oct(s+1, n-1, T(*s - '0'), sign)
            : *s == 'x' || *s == 'X'
                ? _atoi_hex(s+1, n-1, T(0), sign)
                : Result<T>();


template <typename T>
constexpr Result<T> _atoi_sign(const char *s, size_t n, int sign = 1)

    return n == 0 ? Result<T>()
        : *s == '0'
            ? _atoi_zero<T>(s+1, n-1, sign)
            : *s > '0' && *s <= '9'
                ? _atoi_dec(s+1, n-1, T(*s - '0'), sign)
                : Result<T>();


template <typename T>
constexpr Result<T> _atoi_space(const char *s, size_t n)

    return n == 0 ? Result<T>()
        : (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r' || *s == '\v')
            ? _atoi_space<T>(s+1, n-1)
            : *s == '-'
                ? _atoi_sign<T>(s+1, n-1, -1)
                : *s == '+'
                    ? _atoi_sign<T>(s+1, n-1)
                    : *s == '0'
                        ? _atoi_zero<T>(s+1, n-1)
                        : _atoi_dec(s, n, T(0), 1);


template <size_t N> void pstr(const char (&s)[N])

    printf("s '%.*s'\n", int(N-1), s);


template <typename Str>
__attribute__((always_inline))
void _atoi(Str s)

    constexpr auto result = _atoi_space<long>(s.cstr(), sizeof(s.cstr())-1);
    if (result.valid)
        printf("i %ld\n", result.value);
    else
        pstr(reinterpret_cast<const char (&)[sizeof(s.cstr())]>(s.cstr()));


#define atoi(STR) _atoi([]()  \
                        struct S  \
                            static constexpr const char (&cstr())[sizeof(STR)]  return STR;  \
                        ; \
                        return S();  \
                    ())

int main()

    atoi("42");
    atoi("-1");
    atoi("+1");
    atoi("010");
    atoi("-0x10");
    atoi("--1");
    atoi("x");
    atoi("3x");
    return 0;   


基本上我想问一下,如何将编译时的数字(如“42”)用双引号写成整数类型的值。我的解决方案看起来太麻烦了。

【问题讨论】:

其实我一直在寻找这样的东西。它可能在编译时用于散列字符串或某事。另一种方法可能是编译器 api 编程,但这将是特定于编译器的。 C++17 不是一个选项,对吧? 那么,您的解决方案有什么问题?你到底想知道什么?在什么意义上更好? @MarcinPoloczek 正如你提到的编译时字符串散列:也许你喜欢我前段时间在 SO 上写的这篇文章:***.com/a/47081012/8494588 更新版本,带错误检查:coliru.stacked-crooked.com/a/9f67878a7be4310c 【参考方案1】:

使用 C++14,我们可以摆脱宏和一些三元运算符。这是我的做法,代码应该是不言自明的(我还添加了一些 cmets)。下面的代码也可以在here 找到(带有一些示例)用于编译器比较。

#include <cstdint>
#include <iostream>

template <typename T>
struct Result

    T value;
    bool valid = false;

    constexpr explicit Result(T v) : value(v), valid(true) 
    constexpr Result() = default;
;

// converts upper case chars to lower case
constexpr char to_lower(char c)

    return c >= 'A' && c <= 'Z'
        ? c - 'A' + 'a'
        : c;


// converts a digit char to its numeric value (eg. 'F' -> 15)
constexpr int to_digit(char c)

    c = to_lower(c);
    return c >= 'a'
        ? c - 'a' + 10
        : c - '0';


// checks whether the given digit fits in the given base (eg. 'A' in 16 (hex) -> true, but '9' in 8 (oct) -> false)
constexpr bool is_digit(char c, int base)

    int digit = to_digit(c);
    return 0 <= digit && digit < base;


namespace detail

    // returns true if c is a sign character (+ or -), sign will hold a valid factor (1 or -1) regardless of the return value
    constexpr bool get_sign(char c, int& sign)
    
        if (c == '-')
        
            sign = -1;
            return true;
        
        else
        
            sign = 1;
            return c == '+';
        
    

    // adds a digit to the right side of the a number
    template <typename T>
    constexpr T append_digit(T value, int base, int digit)
    
        return value * base + digit;
    

    // create the actual number from the given string
    template <typename T>
    constexpr T construct_integral(const char* str, std::size_t size, int base)
    
        T value = 0;
        for (std::size_t i = 0; i < size; i++)        
            value = append_digit(value, base, to_digit(str[i]));

        return value;
    

    // how many chars are necessary to specify the base (ex. hex -> 0x -> 2) 
    constexpr std::size_t get_base_offset(int base)
    
        if (base == 8) return 1;
        if (base == 16) return 2;
        return 0;
    

    // gets the base value according to the number prefix (eg. 0x -> 16 (hex))
    constexpr int get_base(const char* str, std::size_t size)
    
        return str[0] == '0'
            ? size > 2 && to_lower(str[1]) == 'x'
                ? 16
                : 8
            : 10;
    

    // checks whether all digits in the string can fit in the given base
    constexpr bool verify_base(const char* str, std::size_t size, int base)
    
        for (std::size_t i = 0; i < size; i++)
            if (!is_digit(str[i], base))
                return false;

        return true;
    


template <typename T = int>
constexpr Result<T> to_integral(const char* str, std::size_t size)

    using namespace detail;

    // remove the sign from the string
    auto sign = 0;    
    if (get_sign(str[0], sign)) 
    
        ++str;
        --size;
    

    // get the base and remove its prefix from the string
    auto base = get_base(str, size);
    auto offset = get_base_offset(base);
    str += offset;
    size -= offset;

    // check if the string holds a valid number with respect to its base
    if (!verify_base(str, size, base))
        return ;

    // create the number and apply the sign
    auto unsigned_value = construct_integral<T>(str, size, base);
    return Result<T>(unsigned_value * sign);


template <typename T = int, std::size_t N>
constexpr Result<T> to_integral(const char(&str)[N])

    static_assert(N > 1, "Empty strings are not allowed");
    return to_integral<T>(str, N - 1);

C++17 可以通过使用std::string_view 进一步减少代码量。您的Result&lt;T&gt; 也可以替换为std::optional

【讨论】:

以上是关于编译时间文本到数字转换 (atoi)的主要内容,如果未能解决你的问题,请参考以下文章

atoi 转换以返回单个数字值

8. 字符串转换整数 (atoi)

字符串转换整数 (atoi)

字符串转换整数 (atoi)

字符串转换整数(atoi)

8. 字符串转换整数 (atoi)