8

这是一个简单的函数,它试图从大端缓冲区中读取一个通用的二进制补码整数,我们假设std::is_signed_v<INT_T>

template<typename INT_T>
INT_T read_big_endian(uint8_t const *data) {
    INT_T result = 0;
    for (size_t i = 0; i < sizeof(INT_T); i++) {
        result <<= 8;
        result |= *data;
        data++;
    }
    return result;
}

不幸的是,这是未定义的行为,因为最后一个<<=转移到符号位。


所以现在我们尝试以下方法:

template<typename INT_T>
INT_T read_big_endian(uint8_t const *data) {
    std::make_unsigned_t<INT_T> result = 0;
    for (size_t i = 0; i < sizeof(INT_T); i++) {
        result <<= 8;
        result |= *data;
        data++;
    }
    return static_cast<INT_T>(result);
}

但是我们现在在 中调用实现定义的行为static_cast,从无符号转换为有符号。


在“定义明确”的领域中如何做到这一点?

4

3 回答 3

3

首先将字节组装成一个无符号值。除非您需要组装 9 个或更多八位字节的组,否则保证符合 C99 的实现具有足够大以容纳所有字节的类型(C89 实现将保证具有足够大的无符号类型以容纳至少四个)。

在大多数情况下,如果您想将一个八位字节序列转换为一个数字,您就会知道需要多少个八位字节。如果数据被编码为 4 个字节,那么无论intand的大小如何,都应该使用 4 个字节long(可移植函数应该返回 type long)。

unsigned long octets_to_unsigned32_little_endian(unsigned char *p)
{
  return p[0] | 
    ((unsigned)p[1]<<8) |
    ((unsigned long)p[2]<<16) |
    ((unsigned long)p[3]<<24);
}
long octets_to_signed32_little_endian(unsigned char *p)
{
  unsigned long as_unsigned = octets_to_unsigned32_little_endian(p);
  if (as_unsigned < 0x80000000)
    return as_unsigned;
  else
    return (long)(as_unsigned^0x80000000UL)-0x40000000L-0x40000000L;
}

请注意,减法是作为两部分完成的,每部分都在有符号长整数的范围内,以允许系统可能LNG_MIN为 -2147483647。尝试在这样的系统上转换字节序列 {0,0,0,0x80} 可能会产生未定义的行为 [因为它会计算值 -2147483648] 但代码应该以完全可移植的方式处理所有在范围内的值“长”。

于 2017-10-12T15:37:42.047 回答
1

不幸的是,这是未定义的行为,因为最后一个 <<= 转移到符号位。

实际上,在C++17中,左移具有负值的有符号整数是未定义的行为。将具有正值的有符号整数左移到符号位是实现定义的行为。也可以看看:

2 的值为E1 << E2E1 左移 E2 位位置;空出的位用零填充。如果 E1 具有无符号类型,则结果的值为 ,以E1 × 2**E2比结果类型中可表示的最大值大一为模减少。否则,如果 E1 具有带符号类型和非负值,并且E1 × 2**E2可以在结果类型的相应无符号类型中表示,则转换为结果类型的该值就是结果值;否则,行为未定义。

C++17 最终工作草案,第 8.8 节移位运算符 [expr.shift],第 2 段,第 132 页 - 强调我的)


使用C++20,转移到符号位从定义的实现更改为定义的行为:

2的值E1 << E2是与 一致的唯一值E1 × 2**E2 modulo 2**N,其中 N 是结果类型的宽度。[注:E1为左移E2位;空出的位用零填充。——尾注]

C++20 最新工作草案,第 7.6.7 节移位运算符 [expr.shift],第 2 段,第 129 页)

例子:

int i = 2147483647;  // here: 2**31-1 == INT_MAX, sizeof(int) = 32
int j = i << 1;      // i.e. -2

断言:-22147483647 * 2 % 2**32

查看:

        a ≡ b (mod n)      | i.e. there exists an integer k:
<=> a - b = k * n
 => -2 - 2147483647 * 2 = k * 2**32
<=> -4294967296 = k * 2**32
<=> k = -1                 | i.e. there is an integer!

该值-2是唯一的,因为在域中没有其他值[INT_MIN .. INT_MAX]满足这种同余关系。


这是 C++20 强制有符号整数类型的二进制补码表示的结果:

3 [..] 对于有符号整数类型的每个值 x,对应于 x 模 2 N 的无符号整数类型的值在其值表示中具有相同的相应位值。41) 这也称为二进制补码表示。[..]

C++20 最新工作草案,第 6.8.1 节基本类型 [basic.fundamental],第 3 段,第 66 页)


这意味着使用 C++20,您的原始示例按原样调用定义的行为。


附加说明:这并不能证明什么,而是 GCC/Clang 未定义的行为清理程序(-fsanitize=undefined用):

#include <stdio.h>
#include <limits.h>

int main(int argc, char **argv)
{
    int i = INT_MAX - 1 + argc;
    int j = i << 1;
    int k = j << 1;

    printf("%d %d %d\n", i, j, k);

    return 0;
}

示例会话(在 Fedora 31 上):

$ g++ -std=c++17 -Wall -Og sign.cc -o sign -fsanitize=undefined
$ ./sign                                                       
sign.cc:8:15: runtime error: left shift of negative value -2
2147483647 -2 -4
$ g++ -std=c++2a -Wall -Og sign.cc -o sign -fsanitize=undefined 
$ ./sign
2147483647 -2 -4
于 2020-03-15T11:20:07.303 回答
0

要提出替代解决方案,复制位和避免 UB 的最佳方法是通过memcpy

template<typename INT_T>
INT_T read_big_endian(uint8_t const *data) {
    std::make_unsigned_t<INT_T> tmp = 0;
    for (size_t i = 0; i < sizeof(INT_T); i++) {
        tmp <<= 8;
        tmp |= *data;
        data++;
    }
    INT_T result;
    memcpy(&result, &tmp, sizeof(tmp));
    return result;
}

有了这个,您将不会通过将无符号类型转换为有符号类型来获得 UB,并且通过优化,它可以编译为与您的示例完全相同的程序集。

#include <cstdint>
#include <cstring>
#include <type_traits>

template<typename INT_T>
INT_T read_big_endian(uint8_t const *data) {
    std::make_unsigned_t<INT_T> tmp = 0;
    for (std::size_t i = 0; i < sizeof(INT_T); i++) {
        tmp <<= 8;
        tmp |= *data;
        data++;
    }   
    return static_cast<INT_T>(tmp);
}

template<typename INT_T>
INT_T read_big_endian2(uint8_t const *data) {
    std::make_unsigned_t<INT_T> tmp = 0;
    for (std::size_t i = 0; i < sizeof(INT_T); i++) {
        tmp <<= 8;
        tmp |= *data;
        data++;
    }   
    INT_T res;
    memcpy(&res, &tmp, sizeof(res));
    return res;
}

// Just to manifest the template expansions.
auto read32_1(uint8_t const *data) {
  return read_big_endian<int32_t>(data);
}
auto read32_2(uint8_t const *data) {
  return read_big_endian2<int32_t>(data);
}
auto read64_1(uint8_t const *data) {
  return read_big_endian<int64_t>(data);
}
auto read64_2(uint8_t const *data) {
  return read_big_endian2<int64_t>(data);
}

编译clang++ /tmp/test.cpp -std=c++17 -c -O3为:

_Z8read32_1PKh:  # read32_1
        movl    (%rdi), %eax
        bswapl  %eax
        retq

_Z8read32_2PKh:  # read32_2
        movl    (%rdi), %eax
        bswapl  %eax
        retq

_Z8read64_1PKh:  # read64_1
        movzbl  (%rdi), %eax
        shlq    $8, %rax
        movzbl  1(%rdi), %ecx
        orq     %rax, %rcx
        shlq    $8, %rcx
        movzbl  2(%rdi), %eax
        orq     %rcx, %rax
        shlq    $8, %rax
        movzbl  3(%rdi), %ecx
        orq     %rax, %rcx
        shlq    $8, %rcx
        movzbl  4(%rdi), %eax
        orq     %rcx, %rax
        shlq    $8, %rax
        movzbl  5(%rdi), %ecx
        orq     %rax, %rcx
        shlq    $8, %rcx
        movzbl  6(%rdi), %edx
        orq     %rcx, %rdx
        shlq    $8, %rdx
        movzbl  7(%rdi), %eax
        orq     %rdx, %rax
        retq

_Z8read64_2PKh:  # read64_2
        movzbl  (%rdi), %eax
        shlq    $8, %rax
        movzbl  1(%rdi), %ecx
        orq     %rax, %rcx
        shlq    $8, %rcx
        movzbl  2(%rdi), %eax
        orq     %rcx, %rax
        shlq    $8, %rax
        movzbl  3(%rdi), %ecx
        orq     %rax, %rcx
        shlq    $8, %rcx
        movzbl  4(%rdi), %eax
        orq     %rcx, %rax
        shlq    $8, %rax
        movzbl  5(%rdi), %ecx
        orq     %rax, %rcx
        shlq    $8, %rcx
        movzbl  6(%rdi), %edx
        orq     %rcx, %rdx
        shlq    $8, %rdx
        movzbl  7(%rdi), %eax
        orq     %rdx, %rax
        retq

在 x86_64-linux-gnu 上使用clang++ v8.

大多数情况下,memcpy优化将编译为与您想要的完全相同的程序集,但没有 UB 的额外好处。


更新正确性:OP 正确地指出这仍然是无效的,因为有符号的 int 表示不需要是二进制补码(至少在 C++20 之前),这将是实现定义的行为。

AFAICT,直到 C++20,实际上似乎没有一种简洁的 C++ 方式来对 int 执行位级操作,而实际上不知道有符号 int 的位表示,这是实现定义的。话虽如此,只要您知道您的编译器会将 C++ 整数类型表示为二进制补码,那么在 OP 的第二个示例中usingmemcpy或 the都应该有效。static_cast

C++20 专门将有符号整数表示为二进制补码的部分主要原因是因为大多数现有编译器已经将它们表示为二进制补码GCCLLVM(以及 Clang)都已经在内部使用了二进制补码。

这似乎不是完全可移植的(如果这不是最佳答案,这是可以理解的),但我想你知道你将使用什么编译器来构建你的代码,所以你可以在技术上包装这个或你的第二个例子检查您是否使用了适当的编译器。

于 2020-04-28T22:51:04.157 回答