【问题标题】:Setting extra bits in a bool makes it true and false at the same time在 bool 中设置额外的位使其同时为真和假
【发布时间】:2019-10-15 13:38:36
【问题描述】:

如果我得到一个 bool 变量并将其第二位设置为 1,则变量同时计算为真和假。使用带有-g 选项(gcc-v6.3.0/Linux/RHEL6.0-2016-x86_64/bin/g++ -g main.cpp -o mytest_d)的 gcc6.3 编译以下代码并运行可执行文件。你会得到以下结果。

T怎么能同时等于真假?

       value   bits 
       -----   ---- 
    T:   1     0001
after bit change
    T:   3     0011
T is true
T is false

当您以不同的语言(例如 fortran)调用函数时,可能会发生这种情况,其中真假定义与 C++ 不同。对于fortran,如果任何位不为0,则值为真,如果所有位均为零,则值为假。

#include <iostream>
#include <bitset>

using namespace std;

void set_bits_to_1(void* val){
  char *x = static_cast<char *>(val);

  for (int i = 0; i<2; i++ ){
    *x |= (1UL << i);
  }
}

int main(int argc,char *argv[])
{

  bool T = 3;

  cout <<"       value   bits " <<endl;
  cout <<"       -----   ---- " <<endl;
  cout <<"    T:   "<< T <<"     "<< bitset<4>(T)<<endl;

  set_bits_to_1(&T);


  bitset<4> bit_T = bitset<4>(T);
  cout <<"after bit change"<<endl;
  cout <<"    T:   "<< T <<"     "<< bit_T<<endl;

  if (T ){
    cout <<"T is true" <<endl;
  }

  if ( T == false){
    cout <<"T is false" <<endl;
  }


}

////////////////////////////// // 使用 ifort 编译时与 C++ 不兼容的 Fortran 函数。

       logical*1 function return_true()
         implicit none

         return_true = 1;

       end function return_true

【问题讨论】:

  • 如果行为未定义,一切皆有可能:)
  • 不完全是 Does the C++ standard allow for an uninitialized bool to crash a program? 的重复,我的回答解释说 x86-64 System V ABI 指定 bool 是 0 或 1,因此允许编译器在发射时假设这一点代码。
  • 布尔值的数学与其计算机科学表示之间存在矛盾。在数学上,布尔值有两个值,所以只有一位。问题是在 c++ 中,布尔值必须是可寻址的,但个别位是不可寻址的。该标准要求实现使所有布尔运算结果为零或一。其他任何东西都是不合规的实现。该标准还要求程序员遵循这一规则。特别是,有意设置位,以便 bool 的值既不是零也不是未定义的行为。
  • 这里是minimal example。高达 8.3 的 GCC 的行为与帖子中的相同。与 9.1 不同,但仍然令人惊讶。 (C++ 很有趣!)
  • “医生,我这样做会很痛。”

标签: c++ boolean undefined-behavior evaluation abi


【解决方案1】:

在 C++ 中,bool 的位表示(甚至大小)是由实现定义的;通常它被实现为char-sized 类型,取 1 或 0 作为可能的值。

如果您将其值设置为与允许值不同的任何值(在这种特定情况下,通过将 bool 别名为 char 并修改其位表示),您就违反了语言规则,所以任何事情都可以发生。特别是,标准中明确规定“损坏的”bool 可能同时表现为truefalse(或者既不是true 也不是false):

以本国际标准描述为“未定义”的方式使用bool 值,例如通过检查未初始化的自动对象的值,可能会导致其表现得既不是true 也不是false

(C++11,[basic.fundamental],注释 47)


在这种特殊情况下,you can see how it ended up in this bizarre situation:第一个 if 被编译为

    movzx   eax, BYTE PTR [rbp-33]
    test    al, al
    je      .L22

eax 中加载T(扩展名为零),如果全为零则跳过打印;下一个 if 是

    movzx   eax, BYTE PTR [rbp-33]
    xor     eax, 1
    test    al, al
    je      .L23

测试if(T == false) 转换为if(T^1),它只翻转低位。这对于有效的bool 来说是可以的,但对于您的“坏掉的”它并没有削减它。

请注意,这个奇怪的序列只在低优化级别生成;在更高的级别上,这通常会归结为零/非零检查,并且像您这样的序列可能会变成a single test/conditional branch。无论如何,在其他情况下,您都会遇到奇怪的行为,例如将 bool 值与其他整数相加时:

int foo(bool b, int i) {
    return i + b;
}

becomes

foo(bool, int):
        movzx   edi, dil
        lea     eax, [rdi+rsi]
        ret

dil 被“信任”为 0/1。


如果你的程序都是 C++,那么解决方案很简单:不要以这种方式破坏 bool 值,避免弄乱它们的位表示,一切都会顺利;特别是,即使您将整数分配给bool,编译器也会发出必要的代码以确保结果值是有效的bool,因此您的bool T = 3 确实是安全的,而T 将最终得到一个true

如果您需要与用其他语言编写的代码进行互操作,而这些代码可能与 bool 的概念不同,只需避免 bool 用于“边界”代码,并将其编组为适当大小的整数.它将在条件和合作中工作。一样好。


有关问题的 Fortran/互操作性方面的更新

免责声明我对 Fortran 的所有了解是我今天早上在标准文档上阅读的内容,并且我有一些带有 Fortran 列表的穿孔卡片用作书签,所以请放轻松。 p>

首先,这种语言互操作性不是语言标准的一部分,而是平台 ABI 的一部分。正如我们所说的Linux x86-64,相关文档是the System V x86-64 ABI

首先,没有指定 C _Bool 类型(在 3.1.2 注释中定义为与 C++ bool 相同)与 Fortran LOGICAL 有任何兼容性;特别是,在 9.2.2 表 9.2 中指定“普通”LOGICAL 映射到 signed int。关于TYPE*N 类型它说

TYPE*N”表示法指定TYPE 类型的变量或聚合成员应占用N 字节的存储空间。

(同上)

没有为LOGICAL*1 明确指定等效类型,这是可以理解的:它甚至不是标准的;实际上,如果您尝试在符合 Fortran 95 的模式下编译包含 LOGICAL*1 的 Fortran 程序,您会收到有关它的警告,两者都来自 ifort

./example.f90(2): warning #6916: Fortran 95 does not allow this length specification.   [1]

    logical*1, intent(in) :: x

------------^

通过 gfort

./example.f90:2:13:
     logical*1, intent(in) :: x
             1
Error: GNU Extension: Nonstandard type declaration LOGICAL*1 at (1)

所以水已经浑浊了;所以,结合上面的两条规则,为了安全起见,我会选择signed char

然而:ABI 还规定:

LOGICAL 类型的值是 .TRUE. 实现为 1 和 .FALSE. 实现为 0。

因此,如果您有一个程序在 LOGICAL 值中存储除 1 和 0 之外的任何内容,您已经超出了 Fortran 方面的规范!你说:

fortran logical*1bool 具有相同的表示形式,但在 fortran 中,如果位为 00000011,则为 true,在 C++ 中未定义。

最后这句话不正确,Fortran 标准与表示无关,而 ABI 明确表示相反。确实,您可以通过checking the output of gfort for LOGICAL comparison 轻松看到这一点:

integer function logical_compare(x, y)
    logical, intent(in) :: x
    logical, intent(in) :: y
    if (x .eqv. y) then
        logical_compare = 12
    else
        logical_compare = 24
    end if
end function logical_compare

变成

logical_compare_:
        mov     eax, DWORD PTR [rsi]
        mov     edx, 24
        cmp     DWORD PTR [rdi], eax
        mov     eax, 12
        cmovne  eax, edx
        ret

您会注意到两个值之间有一个直接的cmp,而无需先对它们进行归一化(与ifort 不同,在这方面更保守)。

更有趣的是:不管 ABI 怎么说,ifort 默认使用非标准表示 LOGICAL;这在-fpscomp logicals switch 文档中有解释,它还指定了一些关于LOGICAL 和跨语言兼容性的有趣细节:

指定非零值的整数被视为真,零值的整数被视为假。字面常量 .TRUE。整数值为 1,文字常量为 .FALSE。整数值为 0。英特尔 Fortran 版本 8.0 之前的版本和 Fortran PowerStation 使用此表示。

默认为fpscomp nologicals,指定奇整数值(低位1)被视为真,偶整数值(低位0)被视为假。

字面常量 .TRUE。整数值为 -1,文字常量为 .FALSE。具有整数值 0。Compaq Visual Fortran 使用此表示。 Fortran 标准未指定 LOGICAL 值的内部表示。 在 LOGICAL 上下文中使用整数值或将 LOGICAL 值传递给用其他语言编写的程序的程序是不可移植的,并且可能无法正确执行。英特尔建议您避免使用依赖于 LOGICAL 值的内部表示的编码实践。

(强调)

现在,LOGICAL 的内部表示通常不成问题,因为据我所知,如果您“按规则”玩游戏并且不跨越语言界限,您将不会注意到。对于符合标准的程序,INTEGERLOGICAL 之间没有“直接转换”;我看到您可以将INTEGER 推入LOGICAL 的唯一方法似乎是TRANSFER,它本质上是不可移植的并且不提供真正的保证,或者非标准的INTEGER LOGICAL分配转换。

gfort 的后一个is documented 总是导致非零 -> .TRUE.、零 -> .FALSE.you can see 在所有情况下都会生成代码来实现这一点(即使它是复杂的代码如果 ifort 使用旧版表示),因此您似乎无法以这种方式将任意整数推入 LOGICAL

logical*1 function integer_to_logical(x)
    integer, intent(in) :: x
    integer_to_logical = x
    return
end function integer_to_logical
integer_to_logical_:
        mov     eax, DWORD PTR [rdi]
        test    eax, eax
        setne   al
        ret

LOGICAL*1 的反向转换是一个直整数零扩展 (gfort),因此,为了遵守上面链接文档中的合同,它显然期望 LOGICAL 的值为 0 或 1。

但总的来说,这些转化的情况是a bita mess,所以我会远离它们。


所以,长话短说:避免将 INTEGER 数据放入 LOGICAL 值,因为即使在 Fortran 中也很糟糕,并确保使用正确的编译器标志来获得符合 ABI 的布尔表示和互操作性使用 C/C++ 应该没问题。但为了更加安全,我只会在 C++ 端使用普通的char

最后,根据我收集到的from the documentation,在 ifort 中有一些内置支持与 C 的互操作性,包括布尔值;您可以尝试利用它。

【讨论】:

  • 这就像有一条 8 车道的高速公路,所有车道都是开放的,但如果您使用第一条以外的任何车道,您肯定会发生事故。
  • bool 实际上一位,只是在底层没有以这种方式实现。使用多少位来表示是一个实现细节(ABI 的一部分),而不是由语言定义的。我不明白为什么这在现实世界中是个问题,尽管它确实是一个很棒的 Stack Overflow Q&A。我在 C++ 和其他语言的代码之间做了很多互操作,我从来没有遇到过问题。 @BY408
  • 尝试使用 gcc 和 intel fortran 编译器。有一个 fortran 函数,它返回一个 8 位 bool 的逻辑 *1 类型变量,并将其分配给 C++ bool,看看会发生什么:)
  • @Cody Gray ,fortran logical*1 具有与 bool 相同的表示,但在 fortran 中如果位为 00000011 则为真,在 C++ 中未定义。那就是问题所在。您不能让 fortran 返回逻辑 *1,并将其分配给 C++ 布尔值。 C++ bool 变得未定义,请参阅上面的代码。 (我正在操纵位来模仿 fortran 的功能以及 C++ 是如何处理它的)
  • @BY408 您似乎不知道如何正确地在 fortran 和 C++ 之间进行互操作。不要将您的无知归咎于 C++ 标准。标准存在准确地,因为没有它们,任何实现都可以做他们不会做的任何事情,互操作性将是不可能的。
【解决方案2】:

当您违反与语言和编译器的合同时,就会发生这种情况。

您可能在某处听说过“零为假”,“非零为真”。当您坚持语言的参数,将int 静态转换为bool 或反之亦然时,这就是成立的。

当您开始弄乱位表示时,它不成立。在这种情况下,您违反了合同,并进入了(至少)实现定义的行为领域。

别那样做。

bool 如何存储在内存中并不取决于您。这取决于编译器。如果您想更改bool 的值,请分配true/false,或分配一个整数并使用C++ 提供的适当转换机制。


过去的 C++ 标准实际上明确指出以这种方式使用 bool 是顽皮、坏和邪恶的(“以本文档描述的方式使用 bool 值“未定义”) ',例如通过检查未初始化的自动对象的值,可能会导致其表现得既不是 true 也不是 false。"),尽管它是 removed in C++20 for editorial reasons

【讨论】:

  • 取决于编译器。 取决于 C++ 术语中的“实现”。在大多数平台(包括 x86-64 GNU/Linux)上,编译器都遵循 ABI(x86-64 System V),它是独立于编译器的文档。 取决于编译器如何将 bool 存储在内存中,这是由 ABI 指定的(除了函数之外的任何东西都无法看到的私有 bool 对象;然后就像规则全面生效)。 “由编译器决定”是一种有用的简化,但实际上并非如此,尤其是对于 GCC 以外的编译器(因为 GCC 开发人员设计了 x86-64 System V ABI。)
  • @PeterCordes 没错。正如您所说,这是一个有用的简化,在 IMO 的上下文中完全有必要。
  • 我对您链接的未初始化布尔 UB 问题的回答涵盖了所有这些,尽管 :) +1 不要混淆 bool 的对象表示。
  • 我不会弄乱代码中的位,但是会出现这个问题,因为我有一个 fortran 函数,它返回一个逻辑类型(与 bool 大小相同)并将该返回值分配给一个 C++ 布尔值。然后 C++ 开始像上面那样行事。我添加了位操作来模拟 fortran 返回的内容,从而为您省去了需要两个编译器来重现问题的麻烦。当您返回该变量并将其分配给 C++ 布尔值时,fortran 分配给变量为 true 的内容不正确。为什么标准不接受非零位为真,这并不直观。
  • @BY408,如果您需要一种类型来处理 1 和 0 以外的值,那么您不应该使用 bool。使用 unsigned char 或 int 或其他数字变量。 if() 语句中使用的 unsigned char 的行为方式符合您的要求(零为假,非零为真)。
猜你喜欢
  • 1970-01-01
  • 2014-03-21
  • 1970-01-01
  • 2019-01-30
  • 1970-01-01
  • 2020-10-07
  • 2014-11-10
  • 1970-01-01
  • 2015-03-29
相关资源
最近更新 更多