何时可以违反别名规则?

4
我收到了这个警告。我希望它有明确定义的行为,但我想保留这个代码。什么时候我可以破坏别名规则?

警告:取消引用类型混合指针将会破坏严格别名规则 [-Wstrict-aliasing]

String是我的自定义字符串,它是一个POD(平凡标量类型)。这段代码由C调用。S可能是一个int。String基本上是struct String { RealString* s; },但使用了模板和辅助函数。我做了一个静态断言来确保String是一个pod,大小为4字节,int也是4字节。我还编写了一个断言,检查所有指针是否≥ NotAPtr。它在我的new/malloc重载中。如果你建议的话,我也可以把那个断言放在String里。
考虑到我遵循的规则(主要是字符串是一个pod,并且始终与int大小相同),如果我打破别名规则,会怎样?这是少数情况之一吗?
void func(String s) {
    auto v=*(unsigned int*)&s;
    myassert(v);
    if(v < NotAPtr) {
        //v is an int
    }
    else{
        //v is a ptr
    }
}

1
"我可以开多快的车而不被开罚单?" 微软在 MAKEINTRESOURCE 中做了类似的事情,但他们也编写了编译器并添加扩展。普通人无法做到这一点。而且在 C++ 中,他们可以添加一个重载函数。" - Bo Persson
3
你正在走在一条很危险的路上。你真的有充足的理由不去创建分别处理 func_i 和 func_s 函数吗? - Per Johansson
1
@PerJohansson:我在项目开始时这样做了。现在这个原因已经不存在了,所以我改为写了2个函数。+1 - user34537
违反别名规则从来都不是“可以接受的”,因为这会导致编译器将整个程序删除或变得无法识别,这是未定义行为。即使我们可以看到它并说“是的,在99%的平台上使用一个非反复无常的编译器可以工作”,但标准并不要求这样的编译器存在。 - underscore_d
4个回答

4

memcpy被完全支持。将数据强制转换为char*也是支持的(然后可以使用std::copy等函数)。


我不确定C++,但在C99中,memcpy函数仅在目标操作数具有声明类型的情况下保证安全进行类型转换。规范花了很多功夫来防止使用memcpy安全地将一个类型的对象复制到没有声明类型的存储器中,以便作为不同类型访问。 - supercat
@supercat:我看到string.h有以下规则,因此适用于memcpy--“除非在本子句中特定函数的描述中明确说明,否则在这样的调用上的指针参数仍应具有有效值,如7.1.4所述。” 你认为7.1.4中的问题是什么?你怎么会得到“没有声明类型的存储”?为什么对于所描述的情况(即int i; memcpy(&i, &s, sizeof i);)很重要呢? - Ben Voigt
在C99中(我认为在C11中没有改变),给定int x = 1234; long *p = malloc(sizeof(long)); *p = 5678; memcpy(p, &x, sizeof x;),即使intlong具有相同的表示形式,尝试将*p读取为long也会调用未定义行为,因为memcpy将设置*p的有效类型为int - supercat
根本问题在于,为了获得良好的性能,编译器需要知道memcpy和memmove的源或目标可能被别名引用的内容,但这些函数是在编译器不关心的时候定义的;因此,它们没有提供指示类型的方法。该问题可以通过要求这些函数假定源和目标具有最坏情况下的别名,并添加新函数来解决,让程序员指定源和/或目标类型是否已知,或者目标类型是否已知匹配未知源类型。 - supercat
相反,标准的作者们将memcpy/memmove搞砸了,以至于在源类型已知且目标类型未知的情况下,编译器可以假定目标将使用与之前用于写入源的相同类型来读取。这在假设正确的情况下可能有所帮助,但是让编译器做出这样一个糟糕的假设是不明智的,因为memcpy的主要用途之一历来就是执行类型转换。 - supercat
显示剩余3条评论

1
标准规定了一组最基本的操作,所有符合规范的实现必须以可预测的方式处理这些操作,除非它们遇到翻译限制(此时一切皆不确定)。它并未试图定义实现必须支持的所有操作,以适用于任何特定目的。相反,对于超出规定的操作的支持被视为实现质量问题。作者承认,一个实现可以符合规范,但是质量如此之低,以至于毫无用处。
像你这样的代码应该可用于面向低级编程的高质量实现,并以所期望的方式在内存中表示事物。不应该期望在其他类型的实现上使用它,包括那些将“实现质量”问题解释为尝试以低质量但符合规范的方式行事的实现。

1

如果您无法按照建议更改代码为2个函数,为什么不这样做(需要C99编译器,因为使用了uintptr_t,对于较旧的MSVC版本,您需要自己定义它,2008/2010应该没问题):

void f(RealString *s) {
    uintptr_t int = reinterpret_cast<uintptr_t>(s);
    assert(int);
}

-2

将变量视为两种不同类型的安全方法是将其转换为联合。联合的一部分可以是指针,另一部分可以是整数。

struct String
{
    union
    {
        RealString*s;
        int i;
    };
};

1
从技术上讲,联合体可能不适用于类型玩弄,如果有一个存储器到s读取自i是未定义的(例如它可以始终为0)。然而,在这种情况下,由于奇怪的转换而存在严格别名违规 - 而不是任何“真正”的违规,因此可以将指针简单地转换为int。 - Maciej Piechotka
1
@BenVoigt:实际上,从除最后一个写入的 union 成员之外的成员读取是未指定 的行为,而不是未定义 的。 - jamesdlin
1
@james:你能指出标准中的那个部分吗?据我所知,读取 union 的非活动成员涉及(1)成员访问表达式,它是一个左值,以及(2)左值到右值转换。由于左值引用的对象不是左值类型的对象(也不是任何子类),任何此类程序都会根据第4.1节([conv.lval])调用未定义行为。 - Ben Voigt
1
@BenVoigt 你需要记住这是读取指针而不是解引用指针。 - user34537
1
@acidzombie24:成员访问也是lvalue。在 String t; t.s = new RealString("literal"); auto v = t.i; 中,最后一个语句需要进行lvalue-to-rvalue转换,使用了与实际存在的对象类型(RealString*)不同的类型(int)。 - Ben Voigt
显示剩余6条评论

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