Converta genericamente bytes em inteiros

Aug 28 2020

Eu queria saber se havia uma maneira de torná-lo mais versátil ou mais curto/simples. Aqui está o código:

// Take only std::byte parameter and return an unsigned integral
constexpr auto bytes_to_uint(std::same_as<std::byte> auto... bytes) -> std::unsigned_integral auto {
    constexpr auto N = sizeof...(bytes);

    // Integral types large enough to hold N bytes
    using types = std::tuple<
        std::uint8_t,
        std::uint16_t,
        std::uint32_t,
        std::uint32_t,
        std::uint64_t,
        std::uint64_t,
        std::uint64_t,
        std::uint64_t
    >;
    
    using result = std::tuple_element_t<N, types>;
    
    return [&]<std::size_t... S>(std::index_sequence<S...>) {
        // Accumulate the part of the number using the bitwise or operator for each bytes
        return ((static_cast<result>(bytes) << CHAR_BIT * (N - S - 1)) | ... );
    }(std::make_index_sequence<N>{});
}

É para ser usado assim:

bytes_to_uint(std::byte{0xaa}, std::byte{0xbb}); // std::uint16_t: 0xaabb

bytes_to_uint(
    std::byte{0x11},
    std::byte{0x22},
    std::byte{0x33},
    std::byte{0x44}
); // std::uint32_t: 0x11223344

Respostas

3 Quuxplusone Sep 01 2020 at 21:09

A assinatura da função me parece difícil de ler, graças à restrição std::same_as<std::byte> auto...e ao "tipo de retorno" à direita std::unsigned_integral auto. Eu poderia escrever algo como

constexpr auto bytes_to_uint(std::initializer_list<std::byte> bytes) {

...Ah, mas então você não poderia usar bytes.size()como uma expressão constante; Eu vejo. Então, eu pensaria em escrever um conjunto de sobrecarga, assim:

constexpr std::uint8_t bytes_to_uint(std::byte a) {
    return a;
}
constexpr std::uint16_t bytes_to_uint(std::byte a, std::byte b) {
    return (a << 8) | b;
}
constexpr std::uint32_t bytes_to_uint(std::byte a, std::byte b, std::byte c, std::byte d) {
    return (a << 24) | (b << 16) | (c << 8) | d;
}

Mas acho que isso é confuso porque você precisa de 16 sobrecargas diferentes. Você não pode nem usar argumentos de função padrão, porque deseja bytes_to_uint(a,b,c)ser igual a bytes_to_uint(0,a,b,c)e não bytes_to_uint(a,b,c,0). Claro que você ainda poderia escrever

#define B std::byte
constexpr std::uint8_t bytes_to_uint(B a)
  { return bytes_to_uint(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,a); }
constexpr std::uint16_t bytes_to_uint(B a, B b)
  { return bytes_to_uint(0,0,0,0,0,0,0,0,0,0,0,0,0,0,a,b); }
constexpr std::uint32_t bytes_to_uint(B a, B b, B c)
  { return bytes_to_uint(0,0,0,0,0,0,0,0,0,0,0,0,0,a,b,c); }
constexpr std::uint32_t bytes_to_uint(B a, B b, B c, B d)
  { return bytes_to_uint(0,0,0,0,0,0,0,0,0,0,0,0,a,b,c,d); }
constexpr std::uint64_t bytes_to_uint(B a, B b, B c, B d, B e)
  { return bytes_to_uint(0,0,0,0,0,0,0,0,0,0,0,a,b,c,d,e); }
[...22 more lines...]
#undef B

mas aposto que você não quer fazer isso. Ok, vamos ficar com a coisa de modelo que você fez.


using result = std::tuple_element_t<N, types>;

Eu prefiro ver esse typedef dependente usar CamelCase(como um parâmetro de modelo) ou suffixedwith_type(como um typedef de membro STL). Chamá-lo resultfaz com que pareça muito com uma variável e torna difícil escolher o único lugar onde você o usa.

Ao invés de gastar 13 linhas e uma <tuple>dependência, prefiro apenas fazer

using ResultType = std::conditional_t<
    (N == 1), std::uint8_t, std::conditional_t<
    (N == 2), std::uint16_t, std::conditional_t<
    (N <= 4), std::uint32_t, std::uint64_t>>>;

O que me lembra, você precisa de algo como

static_assert(N <= 16);

para impedir que você tente lidar com uma lista de argumentos de 17 bytes ou mais.

E eu nem percebi até tentar no Godbolt, mas você tem um bug aqui! Você quis dizer tuple_element_t<N-1, types>. Lembre-se de que a indexação sempre começa em zero (em todos os lugares, exceto <regex>).


Se você não gostar conditional_t, outra opção é usar ifs antigos simples. Fatore a computação enquanto ela ainda está parametrizada em ResultTypee, em seguida, use uma if-elsecadeia para decidir o tipo certo para conectar para ResultTypemais tarde:

auto do_it = [&]<class ResultType, std::size_t... S>(ResultType, std::index_sequence<S...>) {
    return ((static_cast<ResultType>(bytes) << CHAR_BIT * (N - S - 1)) | ... );
};
if constexpr (N == 1) {
    return do_it(std::uint8_t{}, std::make_index_sequence<N>{});
} else if constexpr (N == 2) {
    return do_it(std::uint16_t{}, std::make_index_sequence<N>{});
} else if constexpr (N <= 4) {
    return do_it(std::uint32_t{}, std::make_index_sequence<N>{});
} else if constexpr (N <= 8) {
    return do_it(std::uint64_t{}, std::make_index_sequence<N>{});
}

Melhor ainda, permita que o compilador faça as contas em sua largura de bits preferida (que é de 64 bits em todas as plataformas de desktop que me interessam) e, em seguida, trunque-o no final. Isso produz codegen semelhante e lê melhor ainda:

std::uint64_t result = [&]<std::size_t... S>( std::index_sequence<S...>) {
    return ((static_cast<std::uint64_t>(bytes) << CHAR_BIT * (N - S - 1)) | ... );
}(std::make_index_sequence<N>{});
if constexpr (N == 1) {
    return std::uint8_t(result);
} else if constexpr (N == 2) {
    return std::uint16_t(result);
} else if constexpr (N <= 4) {
    return std::uint32_t(result);
} else {
    return std::uint64_t(result);
}

Você tem outro bug quando N == 1(além do bug off-by-one). Quando N == 1, a expressão de dobra não tem nenhuma |operação e, portanto, é apenas um uint8_tdeslocamento por zero. Essa expressão de deslocamento tem o tipo int. Que não é um tipo integral não assinado. Portanto, sua restrição de tipo de retorno falha!

Esta é apenas mais uma razão para fazer todas as contas primeiro em uint64_t, e depois descer para a uint8_tdireita antes de retornar, como mostrado no meu último exemplo acima.

Escrever qualquer caso de teste teria capturado tanto esse bug quanto o bug off-by-one. Casos de teste são sempre importantes! Especialmente quando você planeja colocar o código para revisão pública. (Ou para revisão por colegas de trabalho, para esse assunto.)


Por fim, recomendo parênteses para esclarecer a precedência de x << CHAR_BIT * y. No contexto, é óbvio o que você esperava que fosse a precedência; mas como leitor, não tenho certeza se você está certo. Coloque os parênteses para que eu não tenha que pensar nem por um segundo.

No entanto, nesse contexto, esse é um ponto muito pequeno, porque você claramente não espera que alguém leia a expressão ((static_cast<result>(bytes) << CHAR_BIT * (N - S - 1)) | ... ). É uma linha de código "confie em mim".

Também é tolice fingir que isso CHAR_BITé relevante aqui. Este código explode espetacularmente se CHAR_BITfor algo diferente de 8. Então apenas escreva 8; e se você for obrigado a trabalhar em uma referência a CHAR_BIT, faça isso escrevendo

static_assert(CHAR_BIT == 8);

no topo da função.