0

I have seen similar patter in running code and tried on my own, and all is working fine. The particular point of interest here is that I expect (*(T*) nullptr) () to fail at runtime, but it does not. Is there Undefined Behaviour here and why it works?

template<typename T>
inline static int var = (*(T*) nullptr) ();

template<typename T>
int getvar (const T&)
{
    return var<T>;
}

int main ()
{
    int x = getvar ([]() { return 5; });
    return 0;
}

Thanks!

Rado
  • 766
  • 7
  • 14

1 Answers1

4

Yes, this program has undefined behavior. The result of a lambda expression is an rvalue of unnamed class type [expr.prim.lambda.closure]/1 that has an overloaded function call operator [expr.prim.lambda.closure]/3. Thus, what you're essentially doing in your example here

(*(T*) nullptr) ()

is you're invoking the operator () of your lambda type (which is a non-static member function) on an object that does not exist. You can find more details on why this is UB here: When does invoking a member function on a null instance result in undefined behavior?

While you definitely are invoking undefined behavior, this undefined behavior will almost certainly not manifest itself in a crash in your particular example. The reason for this is simple: the body of your lambda does not access any members of the class, so even if that operator () is called with a nullptr for this, it doesn't ever attempt to access any memory based off that pointer. The type of the lambda does not even have any members since your lambda does not capture anything. Furthermore, in an optimized build, one would expect the compiler to simply optimize away x from this example program since nothing about it is observable behavior…

If you make your var template constexpr, the compiler will refuse to compile it because the expression used to initialize the variable is not a constant expression because it invokes undefined behavior[expr.const]/2.6

If we modify your example a bit such that the lambda has a member and we access that member in the lambda body

template<typename T>
int getvar (const T&)
{
    return (*(T*) nullptr) ();
}

int main ()
{
    return getvar ([x = 42]() { return x; });
}

you will very likely end up with a program that actually crashes…

Michael Kenzel
  • 15,508
  • 2
  • 30
  • 39
  • So you are saying that while it is UB, as it does not use `this` in practice it is okay? I have tried this even with gcc's `-fsanitize=undefined` but got no complain. – Rado Dec 14 '19 at 19:56
  • @Rado No, he says you are likely unlucky enough that it will likely *seem* to work *for now*. More optimisation, a different phase of the moon, anything could end your reprieve. – Deduplicator Dec 14 '19 at 20:03
  • @Rado There is no such thing as "UB but OK in practise". Undefined behavior means that the behavior of your program is not defined, i.e., you don't know what it will do and, in particular, you cannot rely on it doing something specific… – Michael Kenzel Dec 14 '19 at 20:05
  • @Deduplicator Yes, I'm aware. Its not my code at all, but is used in production code. The risks are minimised due to being stateless and assumptions were made. Key points. – Rado Dec 14 '19 at 20:06
  • @MichaelKenzel I well understand what is UB and that this is such case, but I missed the last 10% confidence. Also I was unable to figure out why it worked, you explained why. Thank you very much for your elaborated answer. – Rado Dec 14 '19 at 20:09
  • 1
    @Rado I added a modified version of your example above that will actually trigger a crash. UB does not necessarily result in a crash. But just because a program does not crash doesn't mean the program behaves correctly. If UB were guaranteed to result in abnormal program termination, then that would be a very specific behavior, not undefined behavior… – Michael Kenzel Dec 14 '19 at 20:12
  • 1
    @Rado I hope this confidence you speak of is not confidence in this working in practice. When a compiler compiles your program, it is allowed to assume that your program does not invoke UB. If your program nevertheless does invoke UB, that means your program enters a state that the machine code the compiler generated was not designed to deal with. It is impossible to predict how your program will behave then without going over all of the generated machine code every time you compile that program… – Michael Kenzel Dec 14 '19 at 20:20
  • @MichaelKenzel Not sure about what confidence you are talking about? I'm saying I know what UB means, my basic problem with that code up was I didn't realised why it worked. Instead of hurrying over the generated assembler I had to investigate it better. You explained it pretty well though. Now you assume certain features of me which I do not understand and continue to explain what UB means. I will repeat, I know - thank you really. That's enough :) – Rado Dec 15 '19 at 06:23