Compilar tradução de texto em tempo para número (atoi)

8

Eu quero implementar a função atoi () em tempo de compilação (na linguagem C ++, usando o padrão C ++ 11 ou C ++ 14). Portanto, ele deve poder analisar o texto entre aspas duplas como número ou relatar um erro. Mais especificamente, é parte de um sistema maior, capaz de analisar o formato semelhante ao printf em tempo de compilação. E eu quero dividir seqüências de caracteres de formato em palavras e se alguma palavra específica pode ser representada por número - número de saída em vez da sequência de caracteres (nos bastidores é a classe do serializador, que pode serializar números com mais eficiência do que as seqüências de caracteres, e que é mais importante, o desserializador não deve tentar analisar cada sequência como um número, porque todos os números impressos dentro da sequência de formato são sempre representados como números, mas não como sequências) ...

Como sei duas, pode haver duas abordagens para resolver a tarefa:

1) usando funções constexpr;

2) por metaprogramação de modelos.

Qual caminho pode ser melhor? Eu tentei pela primeira vez e posso ver que existem muitos obstáculos dessa maneira: especialmente poucas limitações relacionadas ao c ++ 11. Parece que o segundo pode ser preferível, mas exige alguns truques (você precisa dividir a string c para separar caracteres usando o operador "", que é suportado no gcc a partir do c ++ 14 e em clangs a partir do c ++ 11 ) Além disso, a solução completamente baseada em TMP pode ser muito grande e emaranhada.

Abaixo está minha solução, fico feliz em ouvir algumas sugestões sobre isso.

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

Basicamente, quero perguntar, como posso transformar no número de tempo de compilação (como "42") escrito em aspas duplas no valor do tipo integral. Minha solução parece muito complicada.

fk0
fonte
Na verdade, eu tenho procurado algo assim. Pode ter seus usos com seqüências de hash em tempo de compilação ou sth. Outra abordagem poderia ser a programação da API do compilador, mas isso seria específico do compilador.
Marcin Poloczek
C ++ 17 não é uma opção, certo?
Timo
Então, o que há de errado com sua solução? E o que você quer saber exatamente? Melhor em que sentido?
Florestan
@ MarcinPoloczek como você mencionou o hash de cadeia de caracteres em tempo de compilação: talvez você goste deste post que escrevi há algum tempo aqui no SO: stackoverflow.com/a/47081012/8494588
florestan
Versão atualizada, com verificação de erro: coliru.stacked-crooked.com/a/9f67878a7be4310c
fk0

Respostas:

0

Com o C ++ 14, podemos nos livrar da macro e de alguns operadores ternários. Aqui está como eu faria isso, o código deve ser autoexplicativo (também adicionei alguns comentários). O código abaixo também pode ser encontrado aqui (com alguns exemplos) para comparação do compilador.

#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);
}

O C ++ 17 poderia reduzir ainda mais a quantidade de código usando std::string_view. Você Result<T>também pode ser substituído por std::optional.

Timo
fonte