Today was the next dispute with colleagues. They argued that in this code there are no problems, and everything will work the same everywhere:

#include <iostream> struct S{ int a; void foo(){ std::cout << "hello"; } }; int main(){ S *p = nullptr; p->foo(); //hello } 

They say that we don’t refer to the data => In memory at 0 we don’t climb => There are no problems. I argued with them at my mouth that if you call any non-static method on nullptr this is immediately an indefinite behavior, and no matter what happens in this method.

Questions:

  • Who is right?
  • Where is it written in the standard?
  • Is there something in the standard about how this should be implemented?
  • Will work until the first virtual method. - MrBin
  • @MrBin; Speech about such code. Without data and virtual methods. - yrHeTaTeJlb
  • one
    But why might want to write such code? That's the catch. Because so can a person? badly. - KoVadim
  • one
    if so, then this is all at the mercy of the compiler implementation. But passing in the first parameter is a simple logical way. And only in the methods of c ++ there would be no :) and if a person said it is “gets into the methods”, then you can notably troll. - KoVadim
  • one
    A lot of things will work if laid on a specific implementation of a specific compiler on a specific platform. This does not mean that it should be done. And without frills full of errors in programs. :-) - pepsicoca1

2 answers 2

In the “classic” C ++ (C ++ 98) situation is unambiguous - dereferencing the null pointer leads to undefined behavior . Accordingly, invoking a non-static object method through a null pointer results in undefined behavior . It does not matter whether this method performs access to class members or does not. This is the position of the language specification. From this point of view, you are absolutely right, and the arguments of your opponents on the topic “everything will work everywhere” are no more than a consequence of “street education” from the category of “I look into an assembler book , I see a fig.”

At the same time, attempts to form a more flexible / thin specification in this matter have been made for quite some time. In particular

DR # 232: Is indirection through a null pointer undefined behavior?

However, work in this direction has permanently hung in a state of drafting since 2005. To be honest, one gets the impression that no one can give any intelligible interpretation of the text of the current standard on this topic, perhaps precisely because the topic is still “suspended”.

As you understand, the standard will not get a separate specification for your particular case. And as soon as we move to a more general case, then immediately there are situations such as the conversion of the this pointer when the method is called under [multiple] inheritance conditions.

 struct A { int a; }; struct B { int b; void foo() { // К данным мы не обращаемся // Но чему здесь равно `this`??? if (this == nullptr) ; // ??? } }; struct C : A, B { }; int main() { C *c = nullptr; c->foo(); } 

It is not clear whether the compiler, when converting this pointer, in the process of calling the base class method, follows the rule "null is converted to null"? It was precisely because of such subtleties that it was originally decided to ban calls to non-static methods through a null pointer. What will be (and is) now and what are the intentions of the authors of the language - we must wait and understand.

Note, by the way, that 8.5.1.2/4 requires that the hidden parameter this when calling a class method is initialized using explicit type conversion . That is, in the above example with multiple inheritance in the c->foo() call, the B type this pointer must be initialized as (B *) c . Such a conversion works by the rule of null-in-null and the result (B *) c will also be a null pointer. However, in GCC and Clang this inside foo during the call will be 0x4 . That is, these compilers did not fulfill the requirements of 8.5.1.2/4. This immediately suggests that GCC and Clang still interpret this challenge as unspecified behavior.

PS In this case, in C, dereferencing a null pointer is strictly prohibited.

  • >> Вызов нестатического метода объекта через нулевой указатель приводит к неопределенному поведению. << here on this reference would be credible ... - Fat-Zer
  • It’s good that both gcc and clang don't really check that the pointer is zero and work fine with it (in bare metall) - avp
  • @avp, they do not believe, but they assume that it is not null. this == nullptr simply replaced with false on O3 . - yrHeTaTeJlb
  • @yrHeTaTeJlb: This is another confirmation that these compilers regard the call through the null pointer as undefined behavior. - AnT

Code

 struct S{ int a; void foo(){ std::cout << "hello"; } }; 

identical to that

 void foo(s * this){ std::cout << "hello"; } 

Thus calling

 S *p = nullptr; p->foo(); //hello 

equivalent to that

 foo(nullptr) 

To check, just look at the CPU window. Both calls will generate the same code.

 mov eax, 0 push eax call foo 

those. as long as you do not address this there are no problems at all. this will be addressed in two cases.

  1. You refer to a specific field (maybe even from another method)
  2. You will call a virtual function, and the program will read VMT at zero pointers.

And in static functions this pointer is not passed at all.

  • So they told me the same thing. But for some reason it seems to me that according to this, here is UB. After all, at the address 0 there will definitely not be the object we need. - yrHeTaTeJlb
  • one
    @yrHeTaTeJlb, the function already has an address. You can call it. You do not think that for each object its own functions are created? - MrBin 2:07 pm
  • one
    @yrHeTaTeJlb Will not. But you don't need an object. Any class consists of two parts. 1. A set of methods, 2 - a data set. To bind them into methods, this is passed - a pointer to the data. Actually all objects are ordinary pointers to data. No data - and figs with it. Methods from this will not go anywhere - Anton Shchyrov
  • four
    @AntonShchyrov what you write is the implementation details of the specific compilers. Yes, in some cases, compiler developers can add UB, intentionally or accidentally. But this makes no guarantees regarding other compilers or future versions of the compiler. - Pavel Mayorov
  • one
    In general, calling S *p = nullptr; p->foo(); S *p = nullptr; p->foo(); NOT equivalent to calling foo(nullptr); . You can easily see this (including in practice) in my example with multiple inheritance: in the first case the this pointer will cease to be null, and in the second it is converted according to the rule null-in-null. Some kind of "equivalence" is possible only on naive-primitive examples, but the question is more general. - AnT