解决解引用类型转换指针会破坏严格别名规则的问题

53

我正在尝试使用GCC编译一个特定程序时,修复两个警告。这两个警告是:

警告:解引用类型转换指针将破坏严格别名规则[-Wstrict-aliasing]。

这两个问题的罪魁祸首是:

unsigned int received_size = ntohl (*((unsigned int*)dcc->incoming_buf));

并且

*((unsigned int*)dcc->outgoing_buf) = htonl (dcc->file_confirm_offset);

incoming_bufoutgoing_buf的定义如下:

char                    incoming_buf[LIBIRC_DCC_BUFFER_SIZE];

char                    outgoing_buf[LIBIRC_DCC_BUFFER_SIZE];

这似乎与我一直在查看的该警告的其他示例略有不同。我更愿意修复问题而不是禁用严格别名检查。

有许多建议使用 union - 对于这种情况,可能有什么合适的 union?


1
有趣... strict-aliasing 不应该适用于 char*。或者我漏掉了什么? - Mysticial
5
@Mysticial,你所忽略的是当以T2类型的左值访问T1类型的对象,并且T2char类型时不存在别名违规问题,但当T1char类型且T2不是带符号/无符号的char变量时,就会存在别名违规。 - ouah
@Mysticial:你搞错了! - Kerrek SB
9个回答

70

首先,让我们来看一下为什么您会收到别名冲突警告。

别名规则简单地说,只允许您通过其自身类型、有符号/无符号变体类型或字符类型(charsigned charunsigned char)访问对象。

C语言规定违反别名规则将触发未定义行为(所以不要这样做!)。

在程序的这行中:

unsigned int received_size = ntohl (*((unsigned int*)dcc->incoming_buf));
尽管 incoming_buf 数组的元素是 char 类型,但你却使用 unsigned int 访问它们。实际上,在表达式 *((unsigned int*)dcc->incoming_buf) 中,解引用运算符的结果是 unsigned int 类型,这是一种违反别名规则的行为。因为你只有通过(请参见上面的规则摘要!)charsigned charunsigned char 访问 incoming_buf 数组的元素的权利。

请注意,在你的第二个问题中也存在完全相同的别名问题:

*((unsigned int*)dcc->outgoing_buf) = htonl (dcc->file_confirm_offset);

你通过 unsigned int 访问 outgoing_bufchar 元素,因此这是一种别名违规。

提出的解决方案

为了解决这个问题,你可以尝试直接定义数组元素的类型来访问:

unsigned int incoming_buf[LIBIRC_DCC_BUFFER_SIZE / sizeof (unsigned int)];
unsigned int outgoing_buf[LIBIRC_DCC_BUFFER_SIZE / sizeof (unsigned int)];

顺便提一下,unsigned int的宽度是由实现定义的,因此如果您的程序假定unsigned int是32位,则应考虑使用uint32_t

这样,您就可以在数组中存储unsigned int对象,而不会通过访问char类型的元素来违反别名规则,例如:

*((char *) outgoing_buf) =  expr_of_type_char;
或者
char_lvalue = *((char *) incoming_buf);

编辑:

我已经全面修订了我的回答,特别是解释了程序为什么会收到编译器的别名警告。


警告可以通过通过void *进行双重转换来消除。请参见我的答案。 - yanychar

34
为了解决问题,不要使用游戏和别名!读取类型T的唯一“正确”方法是分配类型T并在需要时填充其表示形式:
uint32_t n;
memcpy(&n, dcc->incoming_buf, 4);

简而言之:如果你想要一个整数,你需要创建一个整数。在语言规范的范围内,没有任何欺骗方法。
唯一允许的指针转换(通常用于I/O目的)是将类型为T的现有变量的地址视为char*,或者更确切地说,作为大小为sizeof(T)的char数组的第一个元素的指针。

1
我不确定sizeof(uint32_t)保证为4,因此您可能需要调整您的memcpy - Героям слава
1
@OmarL:正确。uint32_t保证恰好有32个值位和没有填充位,但是如果unsigned char有16位,则sizeof(uint32_t)可以为2,甚至如果unsigned char有32位,则可以为1。该示例应更改为memcpy(&n, dcc->incoming_buf, sizeof(n)); - chqrlie

6
union
{
    const unsigned int * int_val_p;
    const char* buf;
} xyz;

xyz.buf = dcc->incoming_buf;
unsigned int received_size = ntohl(*(xyz.int_val_p));

简要解释: 1. c++标准规定您应该尝试自行对齐数据,g++会在这方面生成警告。 2. 只有当您完全理解您的架构/系统和代码内部的数据对齐(例如上面的代码在Intel 32/64;对齐1;Win/Linux/Bsd/Mac上是确定的)时,才应该尝试它。 3. 使用上述代码的唯一实际原因是避免编译器警告,前提是您知道自己在做什么。


3

如果我可以发表个人意见,对于这种情况,问题在于ntohl和htonl以及相关函数API的设计。它们不应该被编写为有数字参数和数字返回值。(是的,我理解宏优化点)它们应该被设计为“n”端作为指向缓冲区的指针。当这样做时,无论主机是哪种字节序,整个问题都会消失,并且例程是准确的。 例如(没有尝试进行优化):

inline void safe_htonl(unsigned char *netside, unsigned long value) {
    netside[3] = value & 0xFF;
    netside[2] = (value >> 8) & 0xFF;
    netside[1] = (value >> 16) & 0xFF;
    netside[0] = (value >> 24) & 0xFF;
};

如果标准包括一组标准的大端和小端“获取”和“存储”例程,这些例程可以有用地定义八位字节的行为,即使在CHAR_BIT不是八位的机器上,从而增强网络代码在这些机器上的可移植性。 - supercat

2
如果您有不能改变源对象类型的原因(就像我遇到的情况一样),并且您绝对确信代码正确并且它执行了 char 数组的预期操作,为了避免警告,您可以采取以下措施:最初的回答。
unsigned int* buf = (unsigned int*)dcc->incoming_buf;
unsigned int received_size = ntohl (*buf);

1
忽略警告是不好的。这段代码没有处理违反严格别名规则的情况,也可能会违反对齐限制。 - Andrew Henle

1

我最近将一个项目从GCC 6升级到GCC 9,并开始看到这个警告。该项目在32位微控制器上运行,我创建了一个结构体来访问32位机器寄存器的各个字节:

struct TCC_WEXCTRL_t
{
    byte    OTMX;
    byte    DTIEN;
    byte    DTLS;
    byte    DTHS;
};

然后进行编码:
((TCC_WEXCTRL_t *)&TCC0->WEXCTRL)->DTLS = PwmLoDeadTime;

新编译器产生了警告。我发现可以通过将我的结构体与原始类型组合在联合中来消除警告:

union TCC_WEXCTRL_t
{
    TCC_WEXCTRL_Type std;
    struct  
    {
        byte    OTMX;
        byte    DTIEN;
        byte    DTLS;
        byte    DTHS;
    };    
};

其中TCC_WEXCTRL_Type是制造商头文件中提供的WEXCTRL成员的类型。

我不确定这是否被认为是完全符合规范的修复,或者是GCC没有捕获它。如果这个方法没有奏效(或在另一个GCC升级中被捕获),我将继续使用指针类型的联合体,如Real Name在这个主题中所描述的那样。


-1

如果你确定自己知道在做什么,那么就执行以下操作:

void *tmp = dcc->incoming_buf;
unsigned int received_size = ntohl (*((unsigned int*) tmp));

或者只是:

unsigned int received_size = ntohl (*((unsigned int*) ((void *) dcc->incoming_buf)));

2
我发现即使通过(void*)进行双重转换,使用参数std=c++11-Wallg++编译器仍然会为我产生这个警告。老实说,如果编译器维护者坚持指责人们利用字节级优化,这些优化是使像C这样的低级语言首先具有吸引力的,他们实际上只是降低了使用像C这样的语言的价值。另一个解决方案是不使用-Wall ;) - Username Obfuscation

-1

使用 C 强制类型转换无法解决问题,但 reinterpret_cast<> 在类似的情况下帮了我大忙。


-3

将指针转换为无符号整数,然后再转回指针。

unsigned int received_size = ntohl(*((unsigned *)((unsigned)dcc->incoming_buf)));


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接