"Niklas Norrthon" <
[email protected]> wrote in message
[
[email protected]], probably --
(e-mail address removed)'s software seems to have
omitted the message ID]
[undefined-behavior C stuff snipped]
Are you sure? The following C program is defined ...
[this time, I snipped the C code]
He was referring to C++ code. In C++, given a class or struct
type T, one can -- just as in C -- do:
T *p = NULL;
Unlike C, however, C++ has a convenient (if slightly gimmicky) bit
of syntax to say "using object p's type, locate a type-T-specific
function and call it, passing p's value as a secret, hidden argument".
That syntax is:
p->func()
(which is of course in conflict with the same syntax that has a
quite different meaning in C; but hold that thought).
As Niklas Norrthon and numerous others have noted, the effect in
C++ of making such a call is defined only when p is a valid value
of type T, and is not NULL. In practice, real C++ compilers will
resolve the call to the T-specific function "T::func()" at compile
time if and only if T::func is not marked "virtual". So -- defined
behavior or not -- non-"virtual" T::func()s get called even if p
is NULL. When T::func() is marked "virtual", however, the link
between source syntax and actual called function is deferred until
runtime, and looked up at that point, so that if p is NULL, the
call tends to fail.
(I outlined the actual mechanisms, and a way to achieve them in
"raw" C, earlier.)
Note that if we do this, in C:
struct S {
void (*func)(void);
};
void f(struct S *p) {
p->func();
}
we have just used the exact same syntax that C++ uses for "member
function" calls to do something quite different. So when you are
reading C++ code and see:
p->func();
you can never be sure what it means without first inspecting the
data members of the class or struct type that was used to declare
"p". The following (rather wretched) C++ code illustrates the
situation:
struct S1 {
void (*func)(void);
};
struct S2 {
int x;
void func() { x = 42; }
};
struct S3 {
virtual void func();
};
void f(struct S1 *p1, struct S2 *p2, struct S3 *p3) {
p1->func(); // Literally calls p1->func(); does not pass p1.
p2->func(); // Just sets p2->x.
p3->func(); // Calls a "virtual" function, which may vary if p3
// is actually part of a derived type. Passes p3.
}
In f(), the first call works just as in C, calling through p1->func.
The second call sets p2->x to 42, and the third call indirects
through p3's "vtable", which is much like the indirect call for
p1, but using compiler magic rather than an explicit indirect.
You must look up each of the three "struct"s to see which of the
three possible behaviors is being invoked. (This is, of course,
no worse than deciding which overloaded function or operator is
called, in the general case. The concept itself is sensible enough
too: the idea is that you are not supposed to care -- nor have to
care -- which of the various mechanisms is involved. My own
experience has been that you *do* have to care, though, and that
C++ code often[%] lacks sufficient "local" clues to tell you which
one you are getting. Perhaps better compile-time tools would help:
one could click on a "+" operator, for instance, and immedately be
told "this invokes the `add single-precision complex number to
matrix holding string data' function", or whatever. Of course this
does not work for runtime bindings, unless the compiler can somehow
prove that a given variable has some fixed datatype.)
[% "Often" here really means "somewhat rarely, but too often to
just ignore"
]