C ++成员函数中的“ if(!this)”有多糟糕?


74

如果我遇到if (!this) return;了应用程序中的旧代码,这有多严重的风险?它是危险的滴答定时炸弹,需要立即在应用程序范围内进行搜索并破坏工作,还是更像是一种可以悄悄放置的代码气味?

当然,我不打算编写执行此操作的代码。相反,我最近在一个旧的核心库中发现了一些东西,供我们的应用程序的许多部分使用。

想象一个CLookupThingy类具有非虚拟 CThingy *CLookupThingy::Lookup( name )成员函数。显然,在那个牛仔时代,一位程序员遇到过多次崩溃,这些崩溃CLookupThingy *是从函数传递NULL的,而不是修复数百个调用站点,而是安静地修复了Lookup():

CThingy *CLookupThingy::Lookup( name ) 
{
   if (!this)
   {
      return NULL;
   }
   // else do the lookup code...
}

// now the above can be used like
CLookupThingy *GetLookup() 
{
  if (notReady()) return NULL;
  // else etc...
}

CThingy *pFoo = GetLookup()->Lookup( "foo" ); // will set pFoo to NULL without crashing

我本周早些时候发现了这颗宝石,但现在在是否应该修复它方面发生了冲突。这位于我们所有应用程序使用的核心库中。这些应用程序中有几个已经交付给数百万个客户,并且看起来运行良好。该代码没有崩溃或其他错误。删除if !this查找功能将意味着修复成千上万个可能传递NULL的呼叫站点。不可避免地会遗漏一些,引入新的错误,这些错误会在下一年的开发中随机弹出。

因此,除非绝对必要,否则我倾向于将其保留。

鉴于这是技术上未定义的行为,if (!this)实际上有多危险?是否值得花几个星期的时间进行修复,还是可以依靠MSVC和GCC来安全返回?

我们的应用程序在MSVC和GCC上编译,并在Windows,Ubuntu和MacOS上运行。与其他平台的可移植性无关。保证所讨论的功能永远不会是虚拟的。

编辑:我正在寻找的客观答案是这样的

  • “ MSVC和GCC的当前版本使用ABI,其中非虚拟成员实际上是带有隐式'this'参数的静态变量;因此,即使'this'为NULL,它们也将安全地分支到函数中”或
  • “即将发布的GCC版本将更改ABI,以便即使非虚拟函数也需要从类指针中加载分支目标”或
  • “当前的GCC 4.5具有不一致的ABI,有时它将非虚拟成员编译为带有隐式参数的直接分支,有时又编译为类偏移函数指针。”

前者表示代码很臭,但不太可能破坏;第二是在编译器升级后要测试的东西;后者甚至需要付出高昂代价立即采取行动。

显然,这是一个等待发生的潜在错误,但是现在我只关心减轻特定编译器的风险。


2
那真是太糟糕了,但是鉴于您的情况,鉴于庞大的部署基础,您恐怕对此无能为力。
Alex B

19
我认为它属于DailyWTF
DOK 2012年

8
在C ++中对NULL的调用是未定义的行为,因此保留它不是一个好主意。
Joachim Isaksson

8
这个问题可能是主观的而不是建设性的-它不是!它确实有一个非常客观且明确的答案-“删除,这是一个定时的炸弹”。它还具有多个主观答案,具体取决于用户对旧的旧式代码库的体验...
Xeo 2012年

4
在非常底层的类实现中并不少见。认为字符串类。严格的故障分析结果,您希望在哪里提出异常?您是否想在经过全面测试的课程中怪罪自己,并在炸弹爆炸时接听电话?还是将崩溃委托给了一个更接近用户代码的类,从而使他很明显地烦恼了一个参数?当然,这会给实践带来积极的影响,让它一点都不产生异常,这是邪恶的傻瓜式代码。
汉斯·帕桑

Answers:


39

我不会理会它。作为SafeNavigationOperator的老式版本,这可能是一个有意的选择。正如您所说,在新代码中,我不推荐这样做,但是对于现有代码,我将不予理会。如果您确实要修改它,那么我将确保测试对其进行了全面覆盖。

编辑添加:您可以选择仅通过以下方式在代码的调试版本中将其删除:

CThingy *CLookupThingy::Lookup( name ) 
{
#if !defined(DEBUG)
   if (!this)
   {
      return NULL;
   }
#endif
   // else do the lookup code...
}

因此,它不会破坏生产代码的任何内容,同时使您有机会在调试模式下对其进行测试。


14
我刚刚添加了一个断言,它会立即在调试模式下跳闸,因此,至少在内部测试中出现的所有情况下,我们至少可以逐步捕获并修复这些情况。
Crashworks 2012年

2
@Crashworks:只需确保在发布模式下定义NDEBUG。这并不总是一个给定的:stackoverflow.com/questions/5354314/...
本·霍金

20

像所有未定义的行为

if (!this)
{
   return NULL;
}

一颗正在爆炸的炸弹。如果它可以与您当前的编译器一起使用,那么您会很幸运,很不幸!

相同编译器的下一个发行版可能会更具攻击性,并将其视为无效代码。由于this永远不能为null,因此可以“安全地”删除代码。

我认为删除它会更好!


8
@Doug:UB以任何方式取消引用空指针,并且要this使其为空,必须在null上调用成员函数CLookupThingy*。即,if (!this)仅当您已经在UB-land中时,才会评估为true。
ildjarn

6
即测试本身不是UB,但是在没有UB的情况下它不会做任何事情。在存在UB的情况下,它的作用当然是不确定的。
MSalters 2012年

1
@ildjarn:我的印象是,通过实例指针调用成员函数不会立即取消引用该指针。我的思维模型是将指针简单地作为第一个“隐藏”参数传递给“普通”函数。我错了吗?
2012年

2
@Ben从CPU的角度来看,您是正确的。在x86调用约定中,“ this”指针成为该函数的隐式第一个参数。在尝试访问该类的数据成员之一之前,它实际上并没有用作加载操作码中的地址。就C ++标准而言,这当然是所有“未定义的”。这是x86 ABI的实现细节。
Crashworks 2012年

5
@Ben-要调用成员函数,您需要一个该函数所属的对象。如果指针为空,没有对象。x86可能无法始终检测到这一点,这只是使其未定义的原因之一。如果所有系统始终崩溃(或不崩溃),则将定义行为!
Bo Persson 2012年

12

如果您有许多GetLookup函数返回NULL,那么最好修复使用NULL指针调用方法的代码。一,更换

if (!this) return NULL;

if (!this) {
  // TODO(Crashworks): Replace this case with an assertion on July, 2012, once all callers are fixed.
  printf("Please mail the following stack trace to myemailaddress. Thanks!");
  print_stacktrace();
  return NULL;
}

现在,继续进行其他工作,但在滚动工作时进行修复。请替换:

GetLookup(x)->Lookup(y)...

convert_to_proxy(GetLookup(x))->Lookup(y)...

conver_to_proxy确实返回的指针不变,除非它为NULL,在这种情况下,它返回一个FailedLookupObject,就像我的另一个答案一样。


8
那可怜的罩衫又如何呢?当他试图做一些有用的事情,例如处理重要的订单或在价格下跌之前卖掉他的股票时,却得到了这个错误消息!给理科毕业生的信息-编写程序的目的是不要被别人欣赏。
詹姆斯·安德森

1
@JamesAnderson:该程序照常运行。理想情况下,有人会立即修复所有呼叫。如果那不可能发生,那么下一个最好的办法就是在发现问题时修复问题。这与程序虚荣无关。
Neil G

7
一些可怜的最终用户在遇到此消息时应该怎么做!他可能会得出结论,该程序将无法运行,并恢复到某些手动过程,直到找到其他供应商的替代软件为止。您正在引入真实的错误,以捕获虚构的错误!
詹姆斯·安德森

1
用一些自动将stacktrace发送到某处的函数替换它。不要把它扔到用户脸上。
CodingBarfield 2012年

1
然后@Crashworks设置客户端/服务器,以便仅在服务器不忙且仅在这种情况下才偶尔上传一次错误积压。你甚至可以推出这一关键用户的百分比,以便上传量小,你可以使用相同的组进行beta测试等
CodingBarfield

9

在大多数编译器中它可能不会崩溃,因为非虚拟函数通常以“ this”为参数进行内联或转换为非成员函数。但是,该标准特别指出,在对象的生存期之外调用非静态成员函数是未定义的,并且将对象的生存期定义为在分配了对象的内存并且构造函数完成时开始(如果有)非平凡的初始化。

该标准仅对在构造或销毁过程中对象本身进行的调用提供了此规则的例外,但是即使这样,也必须小心,因为虚拟调用的行为可能不同于对象生命周期中的行为。

TL:DR:即使要花很长时间清理所有呼叫站点,我还是要用火杀死它。


8

在形式上未定义行为的情况下,编译器的未来版本可能会更积极地进行优化。我不会担心现有的部署(您知道编译器实际实现的行为),但是如果您使用其他编译器或不同版本,则应在源代码中对其进行修复。


3
首先评估新编译器的行为,然后根据结果做出决定如何?
罗伯特·哈维

1
@Robert:考虑到它何时会中断,您可能会建议这样做,这很可能是在非常特定的情况下进行的(可能在优化时出现内联成员函数)。
Ben Voigt 2012年

1
当然,通过使用所需的优化使其与预先存在的单元测试套件相对应。或者,通过在现场进行烟雾测试。
罗伯特·哈维

5
@RobertHarvey:如果有现有的测试定好,这些各种各样的bug不会在下滑。
本·福格特

6

这就是所谓的“聪明又丑陋的骇客”。注意:聪明!=明智。

在没有任何重构工具的情况下查找所有呼叫站点应该很容易;以某种方式破坏GetLookup()使其不编译(例如,更改签名),以便您可以静态识别误用。然后添加一个名为DoLookup()的函数,该函数可以立即执行所有这些黑客操作。


5

在这种情况下,我建议从成员函数中删除NULL检查并创建一个非成员函数

CThingy* SafeLookup(CLookupThing *lookupThing) {
  if (lookupThing == NULL) {
    return NULL;
  } else {
    return lookupThing->Lookup();
  }
}

然后,应该很容易找到对Lookup成员函数的每个调用,并将其替换为安全的非成员函数。


4

如果今天有事困扰您,那么一年后会困扰您。正如您所指出的那样,对其进行更改几乎肯定会引入一些错误-但您可以从保留return NULL功能开始,添加一些日志记录,让它在野外运行数周,然后查找甚至遭到攻击的次数?


8
更改它几乎肯定会引入一些错误”不,这些错误都是存在的-更改它只会使这些错误可见,而不是将它们扫在地毯下并假装它们不存在。
ildjarn

2

今天可以通过返回失败的查找对象来安全地解决此问题

class CLookupThingy: public Interface {
  // ...
}

class CFailedLookupThingy: public Interface {
 public:
  CThingy* Lookup(string const& name) {
    return NULL;
  }
  operator bool() const { return false; }  // So that GetLookup() can be tested in a condition.
} failed_lookup;

Interface *GetLookup() {
  if (notReady())
    return &failed_lookup;
  // else etc...
}

该代码仍然有效:

CThingy *pFoo = GetLookup()->Lookup( "foo" ); // will set pFoo to NULL without crashing

1
问题在于,不只有一个GetLookup()函数返回CLookupThingys。它们来自(字面上)上千种不同的来源,包括DLL边界另一端的函数。我将不得不修复所有这些位置以返回failed_lookup类型,这意味着不可避免地会丢失一些错误并引入了错误。
Crashworks 2012年

@Crashworks:您有很多GetLookup函数吗?还是GetLookup的许多调用者?
Neil G

许多很多GetLookup()函数。(或者更确切地说,许多不同的功能,它会返回NULL可能* CLookupThingy)
Crashworks

2

仅当您对规范的措辞不屑一顾时,这才是“炸弹”。但是,无论如何,这是一种糟糕的建议,因为它掩盖了程序错误。仅出于这个原因,即使需要进行大量工作,我也删除。这不是立即(甚至中期)的风险,但这并不是一个好的方法。

这样的错误隐藏行为实际上也不是您要依赖的东西。假设您依赖此行为(即,不管我的对象是否有效,它都可以正常工作!),然后由于某种危险,编译器会优化该行为。if在特定情况下语句,因为它可以证明this不是空指针。这不仅对某些假想的未来编译器,而且对于非常实际的当前编译器,都是合理的优化。
但当然,因为没有很好地形成你的程序,它恰好有时在20个角附近将其传递为null 。邦,你死了。 诚然,这是非常人为的,并且不会发生,但是您不能100%确信它仍然不可能发生。this

请注意,当我大声喊“删除!”时,这并不意味着必须将它们全部删除。 立即或在一个大量的人力操作。您可以在遇到这些检查时将它们逐个删除(无论如何,如果您在同一文件中更改某些内容,避免重新编译),或者仅进行文本搜索(最好是在高度使用的函数中),然后将其删除。

由于您使用的是GCC,因此您可能会感兴趣 __builtin_return_address,这可能有助于您没有大量人力的情况下删除这些检查,并且完全破坏了整个工作流程并使该应用程序完全无法使用。
删除支票之前,请将其修改为输出呼叫者的地址,并addr2line告诉您源中的位置。这样,您应该能够快速识别应用程序中行为不正确的所有位置,以便可以对其进行修复。

所以代替

if(!this) return 0;

一次将一个位置更改为以下内容:

if(!this) { __builtin_printf("!!! %p\n", __builtin_return_address(0)); return 0; }

这样一来,您就可以为此更改识别无效的呼叫站点,同时仍然让程序“按预期工作”(如果需要,您还可以查询呼叫者的呼叫者)。修复每个不良行为的位置,一个接一个。该程序仍将正常运行。
一旦没有更多的地址出现,一起删除所有检查。如果不幸的话,您可能仍然必须修复一个或另一个崩溃(因为它在测试时未显示),但这应该是一件非常罕见的事情。无论如何,它应该防止您的同事对您大喊大叫。
每周删除一张或两张支票,最终将一无所有。同时,生活还在继续,没人注意到您在做什么。

TL; DR
至于“ GCC的当前版本”,您可以使用非虚拟功能,但是当然没有人可以说出将来的版本可以做什么。我认为,将来的版本不太可能导致您的代码中断。现有项目中很少有这种检查的(我记得我们有时在Code :: Blocks中确实有数百个这样的检查,不要问我为什么!)。编译器制造商可能不想让数十/数百个主要项目维护者故意不满意,只是为了证明一个观点。
此外,请考虑最后一段(“从逻辑观点出发”)。即使此检查将在以后的编译器中崩溃并刻录,无论如何它也将崩溃并刻录。

if(!this) return;语句this在结构良好的程序中永远不可能是空指针(这意味着您在空指针上调用了成员函数),因此该语句有些用处。当然,这并不意味着它不可能发生。但是在这种情况下,程序应该崩溃或断言。在任何情况下,此类程序都不应默默继续。
另一方面,完全有可能在碰巧不是null无效对象上调用成员函数。检查是否this空指针显然不能捕获这种情况,但它是完全相同的UB。因此,除了隐藏错误的行为,此检查还仅检测出一半的问题案例。

如果您按照具体的措辞行事,则使用this(包括检查是否为空指针)是未定义的行为。在严格意义上讲,这是“定时炸弹”。但是,我不会合理地无论从实际角度还是从逻辑角度认为这个问题。

  • 实际的角度来看,只要您不取消引用指针,是否读取无效的指针就无关紧要。是的,严格按照字母,这是不允许的。是的,从理论上讲,可能有人会构建一个CPU,该CPU将在加载无效指针时对其进行检查并出错。las,如果您是真实的,情况并非如此。
  • 逻辑的角度来看,假设检查爆炸,那么仍然不会发生。为了执行该语句,必须调用成员函数,并且(是否为虚拟的,是否为内联的)使用的是invalid this,它可以在函数体内使用。如果一种非法使用this炸毁,另一种也会。因此,由于程序早已崩溃,检查已被废弃。


nb:此检查与“安全删除惯用语”非常相似,后者在将其nullptr删除后(使用宏或模板化safe_delete函数)将指针设置为该指针。据推测,这是“安全的”,因为它不会崩溃两次删除同一指针。有些人甚至添加了多余的内容if(!ptr) delete ptr;
如您所知,operator delete保证在空指针上是无操作的。这意味着通过将指针设置为null指针,您已经成功地消除了检测重复删除的唯一机会(这是需要修复的程序错误!)。它并不是“更安全”,而是隐藏了不正确的程序行为。如果您两次删除对象,则该程序应会严重崩溃。
您应该要么保留已删除的指针,要么,如果您坚持要篡改,请将其设置为非空无效指针(例如特殊的“无效”全局变量的地址,或者是一个神奇的值,例如-1-但当发生崩溃时,您不应尝试欺骗并隐藏崩溃)。


1

我个人认为,您应该尽早失败以提醒您遇到问题。在这种情况下,我会毫不客气地删除if(!this)我能找到的每一次出现。


7
那么,您如何证明花费时间修复以前可以正常使用的库,而现在这个库对于每个用户来说都会爆炸性增长?这真的值得冒险,付出时间和精力吗?
罗伯特·哈维

1
OP明确指出这是生产代码。您不能仅仅因为它闻起来很臭就破掉(公认的不好)错误检查。
埃里克·安德列斯

3
@Robert:对于“工作”的某种定义...如果!this评估为真,则意味着在其他地方存在一个错误,这是最重要的。我提倡修复真正的错误,并且我认为这也是我们的答案。
ildjarn 2012年

3
@ildjarn:问题是“工作”的定义。if(!this!)几乎无处不在,无处不在会导致图书馆无法使用。
罗伯特·哈维

3
我不想忍受模棱两可。这是一个无声的故障发生。您需要尽快找到它。
Mooing Duck 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.