在布尔值中设置额外位会使其同时为真和假。

43
如果我获得一个布尔变量并将其第二位设置为1,则变量同时评估为true和false。使用gcc6.3的-g选项编译以下代码(gcc-v6.3.0/Linux/RHEL6.0-2016-x86_64/bin/g++ -g main.cpp -o mytest_d),并运行可执行文件,你会得到以下结果。
如何让T同时等于true和false?
       value   bits 
       -----   ---- 
    T:   1     0001
after bit change
    T:   3     0011
T is true
T is false

当你调用不同语言(比如Fortran)编写的函数时,可能会出现这种情况,因为Fortran中true和false的定义与C++不同。在Fortran中,如果任何位不为0,则该值为true;如果所有位都为零,则该值为false。

#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编译时,该Fortran函数与C++不兼容。

       logical*1 function return_true()
         implicit none

         return_true = 1;

       end function return_true

69
有了未定义行为,任何事情都有可能发生 :) - Jeremy Friesner
7
这并不完全是Does the C++ standard allow for an uninitialized bool to crash a program?的重复,我的答案解释了x86-64 System V ABI规定bool只能为0或1,因此编译器在生成代码时可以做出这样的假设。 - Peter Cordes
10
布尔运算的数学概念和计算机科学中的表示方式之间存在紧张关系。从数学上讲,布尔值有两种取值,即一个位。问题在于,在C++中,布尔值必须是可寻址的,但单个位不可寻址。标准要求实现使所有布尔运算结果为零或一。其他结果都是非兼容实现。标准还要求程序员遵循此规则。特别地,故意设置位,使得布尔值既不是零也不是一的行为是未定义的。 - David Hammen
4
这是一个简短的示例。GCC 8.3及以下版本与文章中描述相同。但在9.1版本中有所不同,仍然令人惊讶。(C++很有趣!) - Alexander Malakhov
3
医生,我这样做时很疼。 - Dennis Williamson
显示剩余2条评论
2个回答

67
在C++中,bool的位表示(甚至大小)是由具体实现定义的;通常它实现为一个以1或0作为可能值的char型大小的类型。如果将其值设置为任何与允许的值不同的值(在这种情况下,通过别名将bool传递给char并修改其位表示),则会违反语言规则,因此可能发生任何事情。特别地,在标准中明确指定“破损”的bool可能同时表现为truefalse(或者既不是true也不是false)。使用一个被描述为“未定义”的bool值进行操作,例如检查未初始化自动对象的值,可能会导致其表现得好像既不是true也不是false。(C++11,[basic.fundamental],注47)
在这种情况下,你可以看到它是如何陷入这种奇怪的情况:第一个if被编译为。
    movzx   eax, BYTE PTR [rbp-33]
    test    al, al
    je      .L22

它将T加载到eax中(带有零扩展),如果全部为零,则跳过打印;相反,下一个是

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

测试if(T == false)被转换为if(T^1),它只翻转低位。这对于有效的bool来说是可以的,但对于您的“损坏”的bool则不行。请注意,这种奇怪的序列仅在低优化级别下生成;在更高的级别下,这通常会归结为零/非零检查,并且像您的序列一样的序列可能会变成单个测试/条件分支。在其他情况下(例如将bool值与其他整数相加时),您仍然会获得奇怪的行为:
int foo(bool b, int i) {
    return i + b;
}

变成

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清单的穿孔卡片,我用它们作为书签,所以请对我宽容。

首先,这种语言互操作性的东西并不是语言标准的一部分,而是平台ABI的一部分。因为我们在谈论Linux x86-64,所以相关文档是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 个字节的存储空间。

(ibid.)

对于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明确说明了相反的情况。实际上,您可以通过检查gfort对LOGICAL比较的输出轻松看到这一点:
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开关文档中有解释,该文档还指定了一些关于LOGICAL和跨语言兼容性的有趣细节:
指定非零值的整数被视为真,零值的整数被视为假。字面常量`.TRUE.`的整数值为1,字面常量`.FALSE.`的整数值为0。此表示法由Intel Fortran 8.0版本之前的发布版和Fortran PowerStation使用。
默认值为`fpscomp nologicals`,它指定奇数值(低位为1)被视为真,偶数值(低位为0)被视为假。
字面常量`.TRUE.`的整数值为-1,字面常量`.FALSE.`的整数值为0。此表示法由Compaq Visual Fortran使用。LOGICAL值的内部表示未由Fortran标准规定。在LOGICAL上下文中使用整数值,或将LOGICAL值传递给其他语言编写的过程的程序是不可移植的,并且可能无法正确执行。Intel建议避免依赖LOGICAL值的内部表示的编码实践。

现在,一个 LOGICAL 的内部表示通常不应该是问题,因为据我所知,如果你遵循“规则”并且不跨语言边界,你不会注意到。对于一个标准兼容的程序,在 INTEGERLOGICAL 之间没有“直接转换”;我唯一看到的方式是使用 TRANSFER,它本质上是不可移植的,并且没有真正的保证,或者在赋值时进行非标准的 INTEGER <-> LOGICAL 转换。

后者已被gfort文档化,总是会导致非零 -> .TRUE.,零 -> .FALSE.,并且你可以看到在所有情况下都会生成代码使其发生(即使在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。
但总的来说,这些转换的情况有点混乱,所以最好远离它们。
所以,长话短说:避免将INTEGER数据放入LOGICAL值中,即使在Fortran中也很糟糕,并确保使用正确的编译器标志以获取ABI兼容的布尔表示,并且与C/C++的互操作性应该很好。但为了更加安全,我会在C++端使用普通的char

最后,据我所知从文档,在ifort中有一些内置支持与C的互操作性,包括布尔值;您可以尝试利用它。


1
这就像拥有一条8车道高速公路,所有车道都是开放的,但如果你使用除第一车道以外的任何车道,你将会发生事故。 - BY408
9
bool 本质上实际上只是一个比特位,只是在底层实现时不是这样实现的。用于表示的比特数是实现细节(ABI 的一部分),并不是语言定义的。我不明白为什么这在现实世界中成为了问题,尽管它确实可以成为 Stack Overflow 上很好的问答题目。我经常在 C++ 和其他语言之间进行交互,并且从未遇到过问题。@BY408 - Cody Gray
1
尝试使用gcc和intel fortran编译器。有一个Fortran函数返回一个逻辑*1类型变量,它是一个8位布尔值,并将其分配给C++布尔值,看看会发生什么 :) - BY408
1
@Cody Gray,Fortran的logical1与bool具有相同的表示方式,但在Fortran中,如果位为00000011,则为true,在C++中则未定义。这就是问题所在。您不能让Fortran返回一个logical1,并将其分配给C++ bool。C++ bool变得未定义,请参见我上面的代码。(我正在操作位以模仿Fortran的操作和C++对其的处理方式) - BY408
7
看起来你不知道如何正确地在Fortran和C++之间进行交互。不要把你的无知归咎于C++标准。标准存在的精确原因是,如果没有它们,任何实现都可以为所欲为,互操作性将是不可能的。 - Giacomo Alzetta
显示剩余11条评论

23
这就是当你违反语言和编译器的合约时会发生的事情。
你可能在某个地方听说过“零为假”,“非零为真”。当你遵循语言参数,静态将int转换为bool或反之时,这是正确的。
但当你开始操纵位表示时,这个规则就不再适用了。在这种情况下,你打破了合约,并进入了至少实现定义行为的领域。
所以,不要这样做。
一个bool在内存中的存储方式并不取决于你,而是取决于编译器。如果你想改变一个bool的值,要么赋值为true/false,要么赋一个整数并使用C++提供的适当转换机制。

C++标准曾经明确指出,以这种方式使用bool是不好的,甚至是邪恶的("在本文档描述为'未定义'的方式中使用bool值,例如检查未初始化自动对象的值,可能会导致它表现得既不是true也不是false。"),但由于编辑原因,它已被移除C ++20


5
这取决于编译器。在 C++ 中,这取决于“实现”。在大多数平台上(包括 x86-64 GNU/Linux),编译器都遵循一个 ABI(x86-64 System V),它是编译器以外的一个独立文档。一个 bool 在内存中如何存储并不由编译器决定,而是由 ABI 规定的(除了私有的 bool 对象,它们在函数外部无法被访问;这时完全按照 as-if 规则进行)。"取决于编译器" 是一个有用的简化说法,但它并不完全准确,尤其是对于除 GCC 之外的编译器(因为 GCC 开发人员设计了 x86-64 System V ABI)。 - Peter Cordes
1
@PeterCordes 没错。正如你所说,这是一个有用的简化,在我看来在这种情况下完全合理。 - Lightness Races in Orbit
2
我在你链接的未初始化布尔 UB问题上的回答已经涵盖了所有内容 :) +1 不要干扰bool的对象表示。 - Peter Cordes
4
如果您需要处理除1和0以外的值,则不应使用bool类型。可以使用无符号字符型、整型或其他数值变量。在if()语句中使用无符号字符型可以实现您想要的行为(零为假,非零为真)。 - JPhi1618
如果标准规定每次读取一个已经被赋予非零偶数的布尔值时,可以独立地将其视为产生任何整数值,并且规定实现不需要允许代码获取__bool对象的地址,那么这将使该类型与许多实现的现有位类型兼容,并且将允许编译器在许多目前无法生成最佳代码的情况下生成最佳代码。 - supercat
显示剩余8条评论

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