C++ is all to happy to make a construct work, so long as it can (within certain constraints) find a valid conversion sequence to types that work in the given context.
In this case, the compiler sees an if
statement, which expects an operator of type bool
. It will therefore attempt to form a bool
value from sPtr
. A conversion sequence can be formed from at most one user-defined conversion, but may contain more standard conversions.
In this case, the first conversion in the sequence is the user defined one to boolean_type
. Following that, we can perform the following standard conversion
[conv.bool] (emphasis mine)
1 A prvalue of arithmetic, unscoped enumeration, pointer, or pointer-to-member type can be converted to a prvalue of type bool
. A zero value, null pointer value, or null member pointer value is converted to false
; any other value is converted to true
.
boolean_type
is a pointer-to-member type. And so can be converted to the bool
we need for the if
statement to work.
The reason such code may have been written in the past, is in the first paragraph to this answer. C++ can be too conversion happy. Had the user-defined conversion been a plain operator bool
, one could end up with silliness such as this
sPtr * 2; // What is multiplying a pointer even mean!? Why?
foo(sPtr); // But foo expects an int!
The conversion to bool
permits using sPtr
in contexts that require an integral type, because, as it were, bool
is also an integral type in the C++ type system! We don't want that. So the above trick is employed to prevent the compiler from considering our user-defined conversion in more contexts than we want.
Nowadays, such tricks are not required. Since C++11, we have the notion of type being contextually converted and explicit
conversion operators. Basically if we write instead
explicit operator bool()
This operator will only be called in very specific contexts (such as the condition in an if
statement), but not implicitly in the problematic examples listed above.