The printf example you gave is an example of garbage in, garbage out. If NULL is a macro not defined as a pointer sized integer, then you're at fault here.
The printf example you gave is an example of garbage in, garbage out. If NULL is a macro not defined as a pointer sized integer, then you're at fault here.
Yes, it does.
> If you're programming in C, you usually define 0 as an invalid value, or a null value.
That was also the usual pattern in C++ when there was no alternative. Once nullptr was introduced in C++, NULL or 0 quickly became a code smell.
> C doesn't have the insane type system C++ has and doesn't have a very strong need to make a distinction between a pointer or an integer, since they're all in the end numbers.
C++'s type system is far from insane. It's actually one of it's killer features.
You're both entirely oblivious to the need to not conflate pointers with integers and failing to present any case in favour of the legacy and broken use of NULL, and in the process failing to address all family of known error patterns involving it.
> The printf example you gave is an example of garbage in, garbage out. If NULL is a macro not defined as a pointer sized integer, then you're at fault here.
Again, you seem to be completely oblivious to the problem domain. NULL is not a macro as far as C or C++ compilers are concerned. NULL is a magic constant that's resolved at preprocessing time. Replacing NULL with nullptr means a magic constant is replaced by a concrete type, and thus whole family of errors can be avoided with compile time checks. Claiming that the developers who wrote in bugs are at fault for inadvertently adding bugs makes no sense at all because it does not solve any problem at all, and instead is just cynical finger pointing. I take compile-time checks over unhelpful finger pointing all day every day.
The original mistake by the standards committee was allowing implicit conversions from integer to pointer. I.e. allowing NULL to be defined as simply 0.
If NULL had been defined always as ((void *) 0) then I don't see that we would have had a problem.
But that's all history now and in this situation I can see that adding nullptr becomes a reasonable way out.
It's ironic though that the fix for the different ways to write null is to add yet another way.
You're missing the whole point.
As per the C standard, NULL is an implementation-defined null Pointer constant.
Macros are resolved in the preprocessing step. The compiler does not know what a macro is. What the compiler knows is whatever the preprocessor passes off in place of the macro. This means the compiler only sees a constant, and has no way to tell what that constant means.
If instead of passing random pointer constants you pass an actual type, now the compiler can tell more things.
> If NULL had been (...)
Irrelevant. The whole point is that it wasn't the committee looked at the problem, and it determined that using a dedicated type is safer, more powerful, and more elegant than passing magic numbers around.
I'd categorize nullptr as 'mostly harmless'
If it was you who wrote stdlib.h, sure; otherwise, if you’re on a platform where NULL is traditionally defined as 0 and not (void *)0, you’re stuck. A conformant implementation is free to use either definition.
If you want to language-lawyer more heavily, C does not require there to be pointer-sized integers (uintptr_t is optional), does not require that all zero bytes represent a null pointer in memory (unlike for integers), does not require that the implementation choose to store an integer with value zero as all zero bytes (there may be other valid representations), and in any case does not require an implementation to do anything reasonable at all if the caller passes an integer but a vararg callee looks for a pointer (think separate integer and pointer registers).
[I’m not entirely sure if (void *)(void *)0 is a null pointer constant (though it’s certainly an expression that evaluates to a null pointer)—does it count as a zero-valued integer constant expression cast to a pointer to void? So you might not even be able to use (void *)NULL as a hedge against bad platform headers.]
#undef NULL
#define NULL ((void *) 0)
Just don't do it prior to the inclusion of any standard header (C or POSIX).> Each macro name in [the standard library] is reserved for use as specified if any of its associated headers is included; unless [you’re #undef’ing a function also provided as a macro].
The general idea seems to be that standard headers are allowed to use macros they define, even in other macros they define, and because macro names are late-bound (ugh), even if the user only redefines the name afterwards, every macro that uses it will then be affected.
As a silly example, a valid part of stdlib.h could be
#define NULL 0
#define EXIT_FAILURE (NULL)
#define EXIT_SUCCESS (EXIT_FAILURE+1)
and now after your redefinitions EXIT_SUCCESS becomes a constraint violation.(For an implementor to actually do this would of course be dumb, but you did say “allowed”, and that’s what the standard says here.)
Or did I misunderstand “use as specified”?
There is no difference between
char *p = nullptr;
and char *q = 0;
other than the variable name; the two have to compare equal: (p == q).What's wrong with 0 is that when it's not in an expression where it's being converted to integer type, it's just an integer.
volatile int* x=0; x=0x1234;
Did I just deference the null pointer or make a valid write to that memory location? There is no way to know for sure, you can only apply heuristics to make a guess.
Of course if the lines are that closely spaced you can guess, but in real code they can be in different translation units.
If you're using some environment in which x isn't the zero address, but you do want the actual zero address, you need some other way to obtain it, like converting a non-constant expression:
const int x = 0;
char *zeroaddr = (char *) x;
C implementations are not required to diagnose null dereferences. In many familiar environments, its arranged by the way the program is loaded. An unmapped page of virtual memory is put at that address. That scheme can be defeated if an offset is involved ptr[large_offset] or ptr->member where member is at a large offset into the struct type.It's possible to have run-time checks for a null pointer as a compiler option, with a run-time penalty. Other than that, you can use assertions to defend against them.
virtual void fun() = 0;
were blind to whatever mistake it embodies.When I gained the proper understanding that 0 is a null pointer constant, I immediatelly stopped using NULL. It's so much nicer.
- It works fine in a typeless language in which every value is a machine word, and integers and the null pointer share exactly the same representation.
- It works fine in a strongly typed language, where context indicates what zero means, even in the context of different types of different sizes and representations.
Unfortunately C (and also C++) contains some untyped areas, like variadic argument lists.
I would have designed it differently: only the token 0 would have the special overloaded meaning, and not all constant zero-valued expressions of any integer type. So (0 + 0) would not be a null pointer.
In unsafe contexts, the use of a 0 token would be diagnosed.
printf("...", ... 0, ...); // diagnosed
printf("...", ... 0L, ...); // not diagnosed: argument of long type
printf("...", ... 0 + 0, ...); // not diagnosed: argument of int type
printf("...", ... (void *) 0, ...); // not diagnosed: null pointer of (void *) type.
I think I would also not have the hexadecimal or octal zero tokens be the null pointer constant: char *p = 0; // null pointer, no diagnostic
char *q = 0x0; // error, integer to pointer with no cast
The downside is that the AST would have to retain that representation detail somewhere (possibly just in a single Boolean flag).There are legitimate cases where you need a function pointer assigned to address zero (reset vectors commonly). The correct behavior in C is ambiguous if the null address isn't also 0 since the standard doesn't call out special behavior for function pointers. That wouldn't be the case if nullptr had been standardized earlier and there was no need for the magic 0 as a null pointer constant.
int *p = 3;With gcc, you have to use `gcc -std=c99 -pedantic-errors`, otherwise it will accept programs which are invalid according to the standard.
int *p = 0;
intptr_t i = (intptr_t)p;
if(i == 0) ... // Isn't always trueOn the ARM Cortex I think address 0 is the initial stack pointer value.
My opinion is NULL being something special in the language is mathy CS academics trying to turn C into a mathy abstract language. Which it ain't.
> 2.1.2.3 Program execution
> The semantic descriptions in this Standard describe the behavior of an abstract machine [...]
Technically NULL == 0 always because the standard special-cases zero-valued integer constant expressions; (uintptr_t)NULL == 0 or NULL == *(void **)calloc(1, sizeof(void *)) is another matter :)
Language lawyering aside, a non-all-zeroes representation of NULL will probably blow up most C programs [e.g. static-storage-duration initialization is now not the same as calloc or memset(,0,) and is even type-specific]. Like CHAR_BIT, that’s a joint that technically exists but has been rusted for decades (pun not intended).
Those values just cannot participate in the "BSS" trick, whereby everything that is zero-initialized is put into a special section that doesn't actually exist in the program image, and is only provided on startup.
Those values would go into the initialized data section.
The problem with 0.0 or null pointers not being all zero bits is all the code that uses calloc or memset zero.
If this is on some specialized platform (e.g. DSP chip), it might not matter that vast quantities of C code are not portable.
In general, compiler (and to a great extent instruction set architecture!) designers are quite hamstrung by the expectations of C programmers and programs; that has been the situation for some thirty years now.
Today, you could not sucessfully introduce a system in which pointers to bytes (void , char ) have a different representation from other pointers (let alone different size, lord forbid).
What breaks are
- truly dubious programs which use pointers in ways they really shouldn't, and are almost certainly just bugs; like use-after-free. When a pointer with an out-of-date tag is passed to the library, that is pretty much a confirmed use-after-free, which is not a legitimate idiom of non-maximally-portable C, like assuming that all pointers are the same size.
- C programs which manipulate the pointer representation: e.g. run-times for dynamic languages that put their own tags in pointers. These can easily work around tagging. (I've dealt with this on Android recently, quite easily. For the affected objects, I strip the tag away, and work with the untagged pointers. When it's time to pass the pointer back to Android's library, I put the tag back. All other code works as before.
(C++’s std::aligned would be less awkward, but, well, memalignment() is what we get.)
You still can’t portably store pointers and integers (or doubles) in the same place and distinguish them, though.