是否存在一个平台或情况,其中解除引用(但不使用)空指针以使空引用将表现不佳?

Mik*_*ham 25 c++ null reference undefined-behavior

我目前正在使用一个使用代码的库

T& being_a_bad_boy()
{
    return *reinterpret_cast<T*>(0);
}
Run Code Online (Sandbox Code Playgroud)

在没有实际存在T的情况下引用T.这是未定义的行为,特别指出标准不支持,但它不是闻所未闻的模式.

我很好奇是否有任何示例或平台或用法表明在实践中这可能会导致问题.谁能提供一些?

Ray*_*hen 87

传统上,编译器将"未定义的行为"视为不检查各种类型错误的借口,而只是"让它无论如何都要发生".但是当代编译器开始使用未定义的行为来指导优化.

考虑以下代码:

int table[5];
bool does_table_contain(int v)
{
    for (int i = 0; i <= 5; i++) {
        if (table[i] == v) return true;
    }
    return false;
}
Run Code Online (Sandbox Code Playgroud)

经典编译器不会注意到您的循环限制被错误地写入并且最后一次迭代读取了数组的末尾.它只会尝试读取数组的末尾,true如果超过数组末尾的值恰好匹配则返回.

另一方面,后经典编译器可能会执行以下分析:

  • 通过循环的前五次,函数可能会返回true.
  • 何时i = 5,代码执行未定义的行为.因此,案件i = 5可以视为无法到达.
  • 案例i = 6(循环运行完成)也无法访问,因为为了实现这一目标,您首先必须这样做i = 5,我们已经证明这是无法访问的.
  • 因此,所有可到达的代码路径都返回true.

然后编译器将此函数简化为

bool does_table_contain(int v)
{
    return true;
}
Run Code Online (Sandbox Code Playgroud)

查看此优化的另一种方法是编译器在心理上展开循环:

bool does_table_contain(int v)
{
    if (table[0] == v) return true;
    if (table[1] == v) return true;
    if (table[2] == v) return true;
    if (table[3] == v) return true;
    if (table[4] == v) return true;
    if (table[5] == v) return true;
    return false;
}
Run Code Online (Sandbox Code Playgroud)

然后它意识到评估table[5]是未定义的,所以过去那一切都无法达到:

bool does_table_contain(int v)
{
    if (table[0] == v) return true;
    if (table[1] == v) return true;
    if (table[2] == v) return true;
    if (table[3] == v) return true;
    if (table[4] == v) return true;
    /* unreachable due to undefined behavior */
}
Run Code Online (Sandbox Code Playgroud)

然后观察所有可到达的代码路径返回true.

使用未定义行为来指导优化的编译器会发现通过该being_a_bad_boy函数的每个代码路径都会调用未定义的行为,因此该being_a_bad_boy函数可以简化为

T& being_a_bad_boy()
{
    /* unreachable due to undefined behavior */
}
Run Code Online (Sandbox Code Playgroud)

然后,此分析可以反向传播到以下所有呼叫者being_a_bad_boy:

void playing_with_fire(bool match_lit, T& t)
{
    kindle(match_lit ? being_a_bad_boy() : t);
} 
Run Code Online (Sandbox Code Playgroud)

由于我们知道being_a_bad_boy由于未定义的行为而无法访问,因此编译器可以断定match_lit必须永远不会true导致这种行为

void playing_with_fire(bool match_lit, T& t)
{
    kindle(t);
} 
Run Code Online (Sandbox Code Playgroud)

现在无论比赛是否点燃,一切都在火上浇油.

您可能在当前的编译器中看不到这种类型的未定义行为引导优化,但是像Web浏览器中的硬件加速一样,它开始变得更加主流只是时间问题.

  • @bdow查看[我链接到的系列的第3部分](http://blog.llvm.org/2011/05/what-every-c-programmer-should-know_21.html),特别是标题为"为什么"的部分基于未定义的行为进行优化时,你不能警告吗?" (12认同)
  • @bdow这是旧的,但一个大问题是C和C++程序员非常习惯于编写具有未定义行为但动态无法访问的代码(这是完全合法的),特别是在宏,模板等中......编译器可以在不解决停机问题的情况下,可靠地过滤掉这些警告 (7认同)
  • 通常,如果编译器检测到未定义的行为,那么他们至少会发出一个警告.我怀疑他们会故意将这个隐藏起来. (2认同)

Mar*_*som 19

这段代码最大的问题并不在于它可能会破坏 - 它违背了程序员对引用的隐含假设,即它们始终有效.当不熟悉"约定"的人遇到此代码时,这只是一个问题.

这也存在潜在的技术故障.由于引用只允许引用没有未定义行为的有效变量,并且没有变量的地址为NULL,因此允许优化编译器优化任何对null的检查.我实际上没有看到这样做,但它是可能的.

T &bad = being_a_bad_boy();
if (&bad == NULL)  // this could be optimized away!
Run Code Online (Sandbox Code Playgroud)

编辑:我将从@mcmcc的评论中无耻地窃取,并指出这个常见的习惯用法可能会崩溃,因为它使用了无效的引用.根据墨菲定律,它将在最糟糕的时刻,当然也不会在测试期间.

T bad2 = being_a_bad_boy();
Run Code Online (Sandbox Code Playgroud)

我也从个人经验中知道,无效引用的影响可以远离引用生成的地方传播,使得调试纯粹地狱.

T &bad3 = being_a_bad_boy();
bad3.do_something();

T::do_something()
{
    use_a_member_of_T();
}

T::use_a_member_of_T()
{
    member = get_unrelated_value(); // crash occurs here, leaving you wondering what happened in get_unrelated_value
}
Run Code Online (Sandbox Code Playgroud)

  • “……程序员对引用的隐含假设是它们永远有效。” 我希望我不是在扮演语言律师(我没有资格),但是引用不能是“NULL”,在本例中似乎是这样。这肯定违反了引用的语义。我不明白“NULL”引用如何有效。 (2认同)

Edw*_*per 0

我希望在大多数平台上,编译器会将所有引用转换为指针。如果这个假设成立,那么这与仅传递 NULL 指针相同,只要您从不使用它就可以。那么问题是是否有编译器以某种方式处理引用,而不仅仅是将它们转换为指针。我不知道有这样的编译器,但我想它们可能存在。