Is this undefined behaviour in C++ calling a funct

2019-03-01 01:03发布

A question came up here on SO asking "Why is this working" when a pointer became dangling. The answers were that it's UB, which means it may work or not.

I learned in a tutorial that:

#include <iostream>

struct Foo
{
    int member;
    void function() { std::cout << "hello";}

};

int main()
{
    Foo* fooObj = nullptr;
    fooObj->member = 5; // This will cause a read access violation but...
    fooObj->function(); // Because this doesn't refer to any memory specific to
                        // the Foo object, and doesn't touch any of its members
                        // It will work.
}

Would this be the equivalent of:

static void function(Foo* fooObj) // Foo* essentially being the "this" pointer
{
    std::cout << "Hello";
    // Foo pointer, even though dangling or null, isn't touched. And so should 
    // run fine.
}

Am I wrong about this? Is it UB even though as I explained just calling a function and not accessing the invalid Foo pointer?

3条回答
Animai°情兽
2楼-- · 2019-03-01 01:17

Compiler isn't obliged by standard to implement member function by passing it a pointer to the class instance. Yes, there is pseudo-pointer "this", but it is unrelated element, guaranteed to be "understood".

nullptr pointer doesn't point on any existing object, and -> () calls a member of that object. From standard's view, this is nonsense and result of such operation is undefined (and potentially, catastrophic).

If function() would be virtual, then call is allowed to fail, because address of function would be unavailable (vtable might be implemented as part of object and doesn't exist if object doesn't).

if the member function (method) behaves like that and meant to be called like that it should be a static member function (method). Static method doesn't access non-static fields and doesn't call non-static methods of class. If it is static, the call could look like this as well:

Foo::function(); 
查看更多
够拽才男人
3楼-- · 2019-03-01 01:24

In practice this is usually how major compilers implement member functions, yes. This means that your test program would probably appear to run "just fine".

Having said that, dereferencing a pointer pointing to nullptr is undefined behavior which means that all bets are off and the whole program and it's output is meaningless, anything could happen.

You can never rely on this behavior, optimizers in particular could mess all of this code up because they're allowed to assume that fooObj is never nullptr.

查看更多
贪生不怕死
4楼-- · 2019-03-01 01:28

You're reasoning about what happens in practice. Undefined behavior is allowed to do the thing you expect... but it is not guaranteed.

For the non-static case, this is straightforward to prove using the rule found in [class.mfct.non-static]:

If a non-static member function of a class X is called for an object that is not of type X, or of a type derived from X, the behavior is undefined.

Note that there's no consideration about whether the non-static member function accesses *this. The object is simply required to have the correct dynamic type, and *(Foo*)nullptr certainly does not.


In particular, even on platforms which use the implementation you describe, the call

fooObj->func();

gets converted to

__assume(fooObj); Foo_func(fooObj);

and is optimization-unstable.

Here's an example which will work contrary to your expectations:

int main()
{
    Foo* fooObj = nullptr;
    fooObj->func();
    if (fooObj) {
        fooObj->member = 5; // This will cause a read access violation!
    }
}

On real systems, this is likely to end up with an access violation on the commented line, because the compiler used the fact that fooObj can't be null in fooObj->func() to eliminate the if test following it.

Don't do things that are UB even if you think you know what your platform does. Optimization instability is real.


Also, the Standard is even more restrictive that you might think. This will also cause UB:

struct Foo
{
    int member;
    void func() { std::cout << "hello";}
    static void s_func() { std::cout << "greetings";}
};

int main()
{
    Foo* fooObj = nullptr;
    fooObj->s_func(); // well-formed call to static member,
         // but unlike Foo::s_func(), it requires *fooObj to be a valid object of type Foo
}

The relevant portions of the Standard are found in [expr.ref]:

The expression E1->E2 is converted to the equivalent form (*(E1)).E2

and the accompanying footnote

If the class member access expression is evaluated, the subexpression evaluation happens even if the result is unnecessary to determine the value of the entire postfix expression, for example if the id-expression denotes a static member.

This means that the code in question definitely evaluates (*fooObj), attempting to create a reference to a non-existent object. There have been several proposals to make this allowed and only forbid allowing lvalue->rvalue conversion on such a reference, but those have been rejected this far; even forming the reference is illegal in all versions of the Standard to date.

查看更多
登录 后发表回答