空引用是否可行?

peo*_*oro 91 c++ null reference language-lawyer

这段代码是否有效(和定义的行为)?

int &nullReference = *(int*)0;
Run Code Online (Sandbox Code Playgroud)

这两个g ++以及铛++编译它没有任何警告,即使使用-Wall,-Wextra,-std=c++98,-pedantic,-Weffc++...

当然,引用实际上不是null,因为它无法访问(这意味着取消引用空指针),但我们可以通过检查其地址来检查它是否为null:

if( & nullReference == 0 ) // null reference
Run Code Online (Sandbox Code Playgroud)

Ste*_*sop 70

引用不是指针.

8.3.2/1:

应初始化引用以引用有效的对象或函数.[注意:特别是,在明确定义的程序中不能存在空引用,因为创建这样的引用的唯一方法是将它绑定到通过解引用空指针获得的"对象",这会导致未定义的行为.如9.6中所述,引用不能直接绑定到位字段.]

1.9/4:

本国际标准中将某些其他操作描述为未定义(例如,取消引用空指针的效果)

正如约翰内斯在一个删除的答案中所说的那样,有人怀疑"解除引用空指针"应该明确地说是未定义的行为.但这不是引起怀疑的案例之一,因为空指针肯定不指向"有效对象或函数",并且标准委员会内部不希望引入空引用.


cma*_*ter 17

答案取决于你的观点:


如果您根据C++标准判断,则无法获得空引用,因为您首先得到未定义的行为.在第一次发生未定义的行为之后,该标准允许任何事情发生.因此,如果你编写*(int*)0,你已经有了未定义的行为,从语言标准的角度来看,取消引用空指针.程序的其余部分是无关紧要的,一旦执行此表达式,您就不在游戏中了.


但是,实际上,可以从空指针轻松创建空引用,直到您实际尝试访问空引用后面的值时才会注意到.您的示例可能有点过于简单,因为任何优秀的优化编译器都会看到未定义的行为,并且只是优化掉依赖于它的任何东西(甚至不会创建空引用,它将被优化掉).

然而,优化取决于编译器来证明未定义的行为,这可能是不可能的.在文件中考虑这个简单的函数converter.cpp:

int& toReference(int* pointer) {
    return *pointer;
}
Run Code Online (Sandbox Code Playgroud)

当编译器看到此函数时,它不知道指针是否为空指针.所以它只是生成代码,将任何指针转换为相应的引用.(顺便说一句:这是一个noop,因为指针和引用在汇编程序中完全相同.)现在,如果你有另一个user.cpp带代码的文件

#include "converter.h"

void foo() {
    int& nullRef = toReference(nullptr);
    cout << nullRef;    //crash happens here
}
Run Code Online (Sandbox Code Playgroud)

编译器不知道toReference()将取消引用传递的指针,并假设它返回一个有效的引用,在实践中恰好是一个空引用.调用成功,但是当您尝试使用该引用时,程序崩溃.希望.该标准允许任何事情发生,包括ping大象的外观.

你可能会问为什么这是相关的,毕竟,内部已经触发了未定义的行为toReference().答案是调试:空引用可以像空指针那样传播和扩散.如果您不知道可以存在空引用,并学会避免创建它们,那么您可能会花费相当多的时间来试图弄清楚为什么您的成员函数在尝试读取一个普通的旧int成员时会崩溃(答案:实例)在成员的调用中是一个空引用,因此this是一个空指针,并且您的成员被计算为位于地址8).


那么检查空引用怎么样?你给了这条线

if( & nullReference == 0 ) // null reference
Run Code Online (Sandbox Code Playgroud)

在你的问题.好吧,这不起作用:根据标准,如果取消引用空指针,则有未定义的行为,并且如果不取消引用空指针则无法创建空引用,因此空引用仅存在于未定义行为的范围内.由于您的编译器可能假设您没有触发未定义的行为,因此它可以假设不存在空引用(即使它很容易发出生成空引用的代码!).因此,它看到了if()条件,得出的结论是它不可能是真的,只是抛弃了整个if()陈述.随着链接时间优化的引入,以健壮的方式检查空引用变得很明显.


TL; DR:

空引用有点可怕:

它们的存在似乎是不可能的(=标准),
但它们存在(=由生成的机器代码),
但是如果它们存在则你看不到它们(=你的尝试将被优化掉),
但它们可能无论如何都不会意识到你(=你的程序在奇怪的点崩溃,或者更糟糕).
你唯一的希望是它们不存在(=编写你的程序而不是创建它们).

我希望不会困扰你!

  • @Sapphire_Brick好吧,在您的代码示例中,您没有创建空引用,而是创建了一个**未初始化的**引用:当您初始化`union`时,您正在设置指针,而不是引用。当您在下一行中使用引用时,您将通过使用尚未初始化的联合成员来调用未定义的行为。当然,在这种情况下,您的编译器可以自由地为您提供空引用,并且几乎所有编译器都会这样做:该引用只是底层的指针,它与您设置为“nullptr”的指针共享其存储。 (2认同)
  • @Sapphire_Brick 这就是严格的别名规则出现之前的情况。现在,它与指针的类型双关一样都是未定义的行为。编译器可以自由地在写入之前安排读取。重新解释位的唯一安全方法是调用“memcpy()”。 (2认同)
  • @Sapphire_Brick `易失性` 仅强制执行精确序列,并且不会省略对易失性变量的读/写,它不提供有关其他变量的任何保证。它应该仅用于内存映射硬件寄存器。据我所知,通过类型双关或联合在“易失性”值之间进行隐式位模式转换仍然是未定义的行为。 (2认同)
  • @Sapphire_Brick 是的,这就是严格别名规则的全部要点:为了允许编译器优化,以前的标准不允许它们进行优化。当然,这破坏了现有的代码。考虑所有内存访问相等的开销在整个 C 代码库中都是可见的,但指针双关语和“联合”滥用的情况却很少。因此,严格的别名规则的积极影响被认为比现有代码的零星不当行为更重要。通过添加一些“memcpy()”调用可以轻松修复该错误行为。 (2认同)

Dav*_*Lee 8

如果你的目的是找到一种在单例对象的枚举中表示null的方法,那么(de)引用null(它是C++ 11,nullptr)是个坏主意.

为什么不按如下方式声明在类中表示NULL的静态单例对象,并添加一个返回nullptr的强制转换操作符?

编辑:更正了几个错误类型并在main()中添加了if语句来测试实际工作的强制转换操作符(我忘了......我的坏) - 2015年3月10日 -

// Error.h
class Error {
public:
  static Error& NOT_FOUND;
  static Error& UNKNOWN;
  static Error& NONE; // singleton object that represents null

public:
  static vector<shared_ptr<Error>> _instances;
  static Error& NewInstance(const string& name, bool isNull = false);

private:
  bool _isNull;
  Error(const string& name, bool isNull = false) : _name(name), _isNull(isNull) {};
  Error() {};
  Error(const Error& src) {};
  Error& operator=(const Error& src) {};

public:
  operator Error*() { return _isNull ? nullptr : this; }
};

// Error.cpp
vector<shared_ptr<Error>> Error::_instances;
Error& Error::NewInstance(const string& name, bool isNull = false)
{
  shared_ptr<Error> pNewInst(new Error(name, isNull)).
  Error::_instances.push_back(pNewInst);
  return *pNewInst.get();
}

Error& Error::NOT_FOUND = Error::NewInstance("NOT_FOUND");
//Error& Error::NOT_FOUND = Error::NewInstance("UNKNOWN"); Edit: fixed
//Error& Error::NOT_FOUND = Error::NewInstance("NONE", true); Edit: fixed
Error& Error::UNKNOWN = Error::NewInstance("UNKNOWN");
Error& Error::NONE = Error::NewInstance("NONE");

// Main.cpp
#include "Error.h"

Error& getError() {
  return Error::UNKNOWN;
}

// Edit: To see the overload of "Error*()" in Error.h actually working
Error& getErrorNone() {
  return Error::NONE;
}

int main(void) {
  if(getError() != Error::NONE) {
    return EXIT_FAILURE;
  }

  // Edit: To see the overload of "Error*()" in Error.h actually working
  if(getErrorNone() != nullptr) {
    return EXIT_FAILURE;
  }
}
Run Code Online (Sandbox Code Playgroud)


小智 6

clang ++ 3.5甚至警告它:

/tmp/a.C:3:7: warning: reference cannot be bound to dereferenced null pointer in well-defined C++ code; comparison may be assumed to
      always evaluate to false [-Wtautological-undefined-compare]
if( & nullReference == 0 ) // null reference
      ^~~~~~~~~~~~~    ~
1 warning generated.
Run Code Online (Sandbox Code Playgroud)