在成员函数中测试this指针是否合法的C++代码是否合法?

4
我有一个涉及不同类类型对象的应用程序,这些对象是由指针引用的。空指针表示关联的对象不存在。目前,调用代码很繁琐,因为每次使用对象的指针时,都要测试指针的值是否为空,并在其为空时采取适当的操作。由于在不存在的情况下需要执行的默认操作取决于对象的类型,我希望将其编码到对象本身的类中,而不是在调用程序中。这导致出现以下构造:
class C
{ ... 
  void member_func() //non-virtual !
  { if (this) { do something with the object ... }
    else { take some default action }
  }
  ...
};

显然,成员函数不能是虚函数,因为当对象不存在时,查找表也不存在,虚调用将失败。但对于非虚成员函数,这段代码是否合法的C++呢?在我尝试过的编译器上似乎可以正常工作,但我担心可能存在的不可移植性。在标准中,我找不到明确允许或明确禁止这种构造的条款。


1
合法?当然……定义清晰吗?不太确定。 - StoryTeller - Unslander Monica
@StoryTeller:在空指针上调用成员函数是未定义行为,因此答案是不确定的。 - David Rodríguez - dribeas
1
完全相关:https://dev59.com/u3E95IYBdhLWcg3wEpro - GManNickG
5个回答

12

this 在成员函数中永远不会是 null,所以您执行的检查是无用的。

正如 Matthieu M. 在评论中指出的那样,如果您在代码中执行以下操作:

C* c = 0; 
c->member();

这会导致未定义行为,这是一件不好的事情


3
或者,如果您执行 X* x = 0; x->member();,那么会触发未定义行为,一切都无法预料。 - Matthieu M.
@MatthieuM。没错。谢谢澄清!我可以把这个加到我的答案里吗? - Ivaylo Strandjev
我非常确定未定义行为包括观察到的结果,如 this == NULL ... 所以它是可能发生的。我们都同意你不应该这样做,但你的第一句话是错误的。 - Useless
4
@Useless: 关键在于,在一个其行为未定义的程序中,仅仅因为 this == NULL 的判断结果为 true,并不能够得出 this "是" null 的结论。基于这种推理方式,你可能会编写一些代码(在某些实现上)使得一个值看起来"是"两个不同的东西。没有什么是真实的,一切都是允许的,就像一个穿着有趣斗篷的家伙曾经告诉我的那样。在没有未定义行为的程序中,this 不是 null,并且显然地,关于程序行为的广泛陈述仅适用于其行为已被定义的程序。 - Steve Jessop

6

正如所指出的那样,this永远不可能是一个null指针。如果是,那么你已经引发了未定义行为。相反地,你可以创建一组重载函数,像这样:

void DoTheThing(C* cp)
{
    if (cp)
        cp->member_func();
    else
    {
        // take some default action
    }
}

void DoTheThing(B* bp)
{
    if (bp)
        bp->some_other_member_func();
    else
    {
        // take some default action
    }
}

如果每个类中要调用的函数名称相同,则可以在每个类中创建一个静态函数,执行该类的默认操作(所有函数名称相同),并创建一个模板:
```html

如果每个类中要调用的函数名称相同,则可以在每个类中创建一个静态函数,执行该类的默认操作(所有函数名称相同),并创建一个模板:

```
template<typname T>
void DoTheThing(T* tp)
{
    if (tp)
        tp->member_func();
    else
        T::default_action()
}

为什么 this 指针永远不可能为空?我以为在 C++ 中,this 是每个成员函数的隐式第一个参数。使用空指针作为参数调用函数是合法的。只有在访问数据成员时才会对 this 进行解引用:此时,如果 this 为空,则行为将变得未定义。仅调用非虚拟成员函数不应涉及对 this 的解引用,对吗? - user1958486
尽管你的论点听起来很有道理,但事实上它是未定义行为。也许本不应该这样,但现在就是这样。给我一分钟,我会找到标准参考资料。 - Benjamin Lindley
3
表达式 E1->E2 被转换为等效形式 (*(E1)).E2 -- 因此,它确实涉及对空指针的解引用。 - Benjamin Lindley
4
这是未定义行为,因为标准没有规定函数调用机制。例如,假设通过空指针进行虚函数调用是不可能的,这是相当合理的。对于非虚函数调用,我认为与其使用空指针进行调用,使调用带上“this”指针并不一定需要成为未定义行为,因为没有任何实际原因需要这样做。但正如Benjamin所说,它确实是未定义行为,因为标准规定不允许对空指针进行解引用操作。 "this是一个隐藏的第一个参数"是一种比喻,用来解释被调用的代码如何知道它被调用的对象是什么,而不是this的定义。 - Steve Jessop

1

检查 this == NULL 不是问题。通过空对象指针调用方法才是问题。

如果您想在某个地方保留这些检查,可以将其放入智能指针类中,该类可以在持有的指针为空时采取适当的操作。如果“适当的操作”由所持有的类型唯一确定,则可以使用特征类来指定它。

这样,您的NULL检查及其逻辑将被保留在一起,而不会混合到调用者或方法代码中。


// specialize this to provide behaviour per held type
template <typename T> struct MaybeNullDefaultAction {
    void null_call() { throw std::runtime_error("call through NULL pointer"); }
}

template <typename T> class MaybeNull: MaybeNullDefaultAction<T> {
    T *ptr;
public:
    explicit MaybeNull(T *p) : ptr(p) {}

    T* operator-> () {
        if (!ptr)
            null_call();
        // null_call should throw to avoid returning NULL here
        return ptr;
    }
};

很遗憾,我无法在不抛出异常的情况下完成此操作。没有办法拦截所有方法名称的函数调用,否则我将只需从operator->返回*this并在operator()中完成工作。


正是我想说的 :) - StoryTeller - Unslander Monica
不幸的是,对于空对象采取的适当操作通常不仅取决于对象的类型,还取决于调用的特定成员函数。一种解决方案可能是不让任何指针保持为空,而是在所需对象不存在时创建给定类的本地“空”实例。在某些应用程序中,对象不存在并不是错误,而是常见情况,在这种情况下,涉及throw的解决方案并不总是合适的。 - user1958486
@user1958486:听起来你需要重新考虑你的方法。为什么会有这么多无用的空实例挂在那里?也许可以进一步封装它们。 - GManNickG

1

从标准的角度来看,这段代码是不合法的,在实践中被使用(虽然这是一种不好的做法)。

事实上,我记得 MFC 在内部使用了这些检查。


你能指出标准中相关的部分吗?我印象中它只是未定义的。 - StoryTeller - Unslander Monica
@StoryTeller 非法的,是的。 - Luchian Grigore
MFC可以使用非标准代码,因为它附带了编译器。(如果你在微软工作,并给隔壁的人买了一杯啤酒,那么它突然变成了实现定义)。 - Bo Persson

0

我认为这是不允许的。你要求参考标准,我相信首先感兴趣的是9.3.1非静态成员函数,1:

非静态成员函数可以使用类成员访问语法(5.2.5、13.3.1.1)调用其类类型对象或从其类类型派生(第10条)的类对象。

其次,让我们看看5.2.5类成员访问,2:

表达式E1->E2转换为等效形式(*(E1)).E2;5.2.5的其余部分将仅涉及第一选项(点)。

因此,如果E1是nullptr,则不允许*E1。至少这是我的猜测。


谢谢。这个规则确实禁止在E1为空时使用表达式E1->E2。理论上,编译器可以以这样的方式实现成员函数,即当使用空的“this”指针调用它们时,它们不会按预期行事。 - user1958486

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