使用reinterpret_cast进行有符号别名化

13

请看以下代码

#include <iostream>

void func() {
    int i = 2147483640;
    while (i < i + 1)
    {
        std::cerr << i << '\n';
        ++i;
    }

    return;
}

int main() {
    func(); 
}

这段代码显然是错误的,因为 while 循环只有在带符号 int i 溢出时才能终止,这是未定义行为(UB),因此编译器可能会将其优化为无限循环(Clang 在 -O3 上确实会这样做),或者执行其他奇怪的操作。我现在的问题是:根据我对 C++ 标准的理解,等效于带符号性的类型可能会别名(即指针 int*unsigned* 可能会别名)。为了进行一些奇怪的带符号“包装”,下面的代码是否具有未定义行为?

#include <iostream>

static int safe_inc(int a)
{
    ++reinterpret_cast<unsigned&>(a);
    return a;
}

void func() {
    int i = 2147483640;
    while (i < safe_inc(i))
    {
        std::cerr << i << '\n';
        ++i;
    }

    return;
}

int main() {
    func(); 
}

我已经使用Clang 8和GCC 9在-O3-Wall -Wextra -Wpedantic -O3 -fsanitize=address,undefined参数下尝试了上述代码,没有出现任何错误或警告,并且循环在回绕到INT_MIN后终止。

cppreference.com告诉我:

类型别名

每当尝试通过AliasedType类型的glvalue读取或修改DynamicType类型对象的存储值时,除非以下条件之一成立,否则行为未定义:

  • AliasedType是DynamicType的(可能带有cv限定符的)有符号或无符号变体。

据我所知,这意味着就类型别名而言,符号性不被考虑,使用 reinterpret_cast 的代码具有良好定义的语义(虽然仍然有点作弊)。


3
@LightnessRacesinOrbit 在《基本概念》一章中的第11条款[基本.lval]/11中规定了访问的有效性。缺失的是,通过引用其带符号/无符号版本来修改无符号/带符号对象的行为是什么。但我没有看到禁止这样做的声明。 - Nicol Bolas
2
@SergeyA:那么请指出规范中哪一行说明了通过对无符号对象的引用写入有符号对象时实际发生了什么。因为我可以指出规范中关于通过基类指针/引用调用派生类成员函数时会发生什么的语句。但是对于有符号/无符号,没有类似的语句存在。转换是合法的;访问是合法的,但是发生了什么在规范中根本没有说明。 - Nicol Bolas
2
确实。我一直认为这种事情的规范说明不够详细。这是标准中似乎假定在某些地方需要接近底层位逻辑的少数领域之一。 - Lightness Races in Orbit
2
@SergeyA:“可能是未指定的吗?” 是的,这是规范中的缺陷。而且随着C++20中的二进制补码更改,它可以以完全定义良好的方式解决。只需要在某个地方加入措辞即可。 - Nicol Bolas
1
@NicolBolas:当标准被编写时,作者们期望某些实现可以并且有用地支持某些操作,但并不一定在所有实现中都有意义,即使标准没有规定,这些操作也将继续在有意义的实现中得到支持。如果标准存在缺陷,那么问题不在于未能强制执行行为,而在于它未能表明它从未旨在是详尽无遗的。 - supercat
显示剩余14条评论
2个回答

6

这里的别名是完全合法的。请参见http://eel.is/c++draft/expr.prop#basic.lval-11.2

如果程序尝试通过glvalue访问存储对象的值,而其类型与以下类型之一不相似([conv.qual]),则行为未定义:53

(11.1) 对象的动态类型,

(11.2) 与对象的动态类型相对应的带符号或无符号类型

我认为,值得谈论实际的溢出问题,这不一定需要reinterpret_cast。同样的效果可以通过隐式整数转换实现。

 unsigned x = i;
 ++x;
 i = x; // this would serve you just fine.

在 C++20 之前,此代码的实现是未定义的,因为您将从无法由目标类型表示的值进行转换。

C++20 以后,这段代码将是良构的。

请参见 https://en.cppreference.com/w/cpp/language/implicit_conversion

另外,如果您想要整数溢出语义,最好使用无符号类型。


3
这并没有回答有关别名的问题,不是吗? - Lightness Races in Orbit
1
@JonasMüller,你的问题是关于递增还是别名? - SergeyA
1
如果 aINT_MAX,那么 x 将无法适应 a,这将导致实现定义行为。 - NathanOliver
1
我同意,这里有第二个隐含的问题(涉及溢出部分),但主要问题是在这里使用reinterpret_cast是否有效。 - Jonas Müller
@LightnessRacesinOrbit 还添加了别名部分,这是合法的。 - SergeyA
显示剩余16条评论

5

你的代码完全合法,cpp reference是一个非常好的来源。你可以在标准[basic.lval]/11中找到相同的信息。

如果程序尝试通过类型与以下类型之一不相似([conv.qual])的glvalue访问对象的存储值,则行为未指定:

  • 对象的动态类型,

  • 与对象的动态类型对应的有符号或无符号类型,[...]


2
@LightnessRacesinOrbit http://eel.is/c++draft/basic.fundamental#2。Cpp参考手册更喜欢使用简单英语,其目的肯定是为了比标准更容易阅读。 - Oliv
1
@curiousguy 哦,你在引用旧版的C++标准。你所提到的规则来自于C语言。在C语言中,这个规则是必要的,以定义成员访问。在C++中,它已被删除,因为不再需要,在标准的其他地方已经定义了成员访问。请参见核心语言问题[#2051](http://open-std.org/JTC1/SC22/WG21/docs/cwg_active.html#2051)。 - Oliv
1
@curiousguy 值的解释取决于表达式类型,intro.object 对于其他对象,其中找到的值的解释取决于用于访问它们的表达式([expr.compound])的类型。 - Oliv
1
@curiousguy 六个月前,我实际上也在问自己同样的问题。这个规则有什么意义:https://dev59.com/SFQJ5IYBdhLWcg3wwYz7。经过几个问题和评论中的讨论,有人向我解释说这只是C语言的遗留问题,并且C++委员会内部正在讨论这个问题。显然,他们决定删除这个规则。 - Oliv
@LanguageLawyer 我相信语言可以被修复,因为在创建C和C++时,我们还没有发现描述程序的正确抽象。混淆的主要原因是无法将引用类型视为常规类型。其次是认为类型是内存属性而不是访问属性。这在Rust中得到了优雅的解决,还有其他许多修复。你应该尝试一下这种语言。对我来说,C++属于遗留代码。 - Oliv
显示剩余9条评论

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