Macro NULL or 0

I

Ioannis Vranos

Hi, in a discussion I am having with a programmer, we have a dispute if it is a good idea to use the macro
NULL instead of 0 for denoting a null pointer value.


I think 0 should be preferred and NULL should be avoided, but the other programmer says that using the macro
NULL in C++ is better, because it is easier to distinguish it.


Some of my arguments:

1. C++ standard mentions:

"A null pointer constant is an integral constant expression (5.19) rvalue of integer type that evaluates to
zero. A null pointer constant can be converted to a pointer type; the result is the null pointer value of that
type and is distinguishable from every other value of pointer to object or pointer to function type. Two null
pointer values of the same type shall compare equal. The conversion of a null pointer constant to a pointer to
cv-qualified type is a single conversion, and not the sequence of a pointer conversion followed by a
qualification conversion (4.4)".


C standard mentions (from K&R2, page 198, ?6.6):

"An integral constant expression with value 0, or such an expression cast to type void *, may be converted, by
a cast, by assignment, or by comparison, to a pointer of any type. This produces a null pointer that is equal
to another null pointer of the same type, but unequal to any pointer to a function or object".



Also TC++PL3 mentions:

"5.1.1 Zero

Zero (0) is an int. Because of standard conversions (C.6.2.3), 0 can be used as a constant of any integral
(4.1.1), floating-point, pointer, or pointer-to-member type. The type of zero will be determined by context.
Zero will typically (but not necessarily) be represented by the bit pattern all-zeros of the appropriate size.

No object is allocated with the address 0. Consequently, 0 acts as a pointer literal, indicating that a
pointer doesn't refer to an object.

In C, it has been popular to define a macro NULL to represent the zero pointer. Because of C++'s tighter type
checking, the use of plain 0, rather than any suggested NULL macro, leads to fewer problems. If you feel you
must define NULL, use

const int NULL= 0;

The const qualifier (5.4) prevents accidental redefinition of NULL and ensures that NULL can be used where a
constant is required".


So in C++, the "natural" value denoting a null pointer is the integral value 0, while in C, the "natural"
value denoting a null pointer is the value that *results* from the conversion of the integral value 0 to the
type of the pointer.




2. The C++ standard says about the macro NULL:

" The macro NULL is an implementation-defined C++ null pointer constant in this International Standard (4.10)
*180.


*180 Possible definitions include 0 and 0L, but not (void*)0".


I think this implies that an implementation may define macro NULL with an implemetation-defined way, for
example a keyword:

#define NULL _somekeyword



3. Since in C NULL is *usually* defined as a pointer type (e.g. #define NULL ((void *)0) ), and in C++ NULL is
*usually* defined with the integral value 0, a programmer that knows C may easily think that NULL is of
pointer type, and be easily confused by the code:


#include <iostream>
#include <cstddef>


void f(void *) { std::cout<< "f(void *) was called.\n"; }

void f(int) { std::cout<< "f(int) was called.\n"; }

void f(long) { std::cout<< "f(long) was called.\n"; }



int main()
{
f(NULL);
}


which in my system produces:



john@john-laptop:~/Projects/anjuta/cpp/src$ g++ -ansi -pedantic-errors -Wall main.cc -o foobar
john@john-laptop:~/Projects/anjuta/cpp/src$ ./foobar
==> f(long) was called.
john@john-laptop:~/Projects/anjuta/cpp/src$


So I think using NULL is a bad practice n C++.



The other programmer considers using NULL in C++ a good practice, because:


1. Using NULL denotes a pointer, while 0 has more uses, and when you see NULL you easily know that you are
dealing with a pointer.

2. NULL is easier to search and find in the source code.

3. NULL is the definition of a constant, and it can easily be modified, so in upcoming C++0x can be easily
defined as #define NULL nullptr.


Also the mentality of C++0x, is more towards the way of the old NULL, rather than 0.

So, using NULL is NOT a bad style.



So, who is right?



Thanks,

--
Ioannis A. Vranos

C95 / C++03 Developer

http://www.cpp-software.net
 
P

Pascal J. Bourguignon

Ioannis Vranos said:
No object is allocated with the address 0. Consequently, 0 acts as a
pointer literal, indicating that a pointer doesn't refer to an object.

AFAIK, this is not exactly exact. "address" is not a C notion. C has
a notion of pointer. The pointer 0 cannot refer any object. But it
doesn't mean that the microprocessor address 0 may not be used to
store a object. For example, AFAIK, a C compiler could map pointers
to addresses by subtracting a small constant, eg. sizeof(int). Then
if sizeof(int)==4, (*((char*)4))=42; would store 42 at the byte at
address 0.

So I think using NULL is a bad practice n C++.

I agree. Nowadays, I tend to use 0 both in C and C++.
 
J

James Kanze

This post is guaranteed to start a holy war.

That's highly likely:). (For the record, most programmers I
know, including myself, prefer NULL. But there are reasonable
arguments for 0 as well---in particular, people doing a lot of
template programming or abusing overloading tend to prefer 0.)
For the record, I don't care for 0 or NULL, nor do I see any
need for the new nullptr keyword. I prefer to use a
default-constructed rvalue of the specific type I want.

I went that way for a short period of time. Not a
default-constructed rvalue---that didn't exist back then. But a
macro which took the type as an argument, and returned a
correctly converted 0. In theory, it should be better---more
type safety and all that. In practice, one quickly tires of
writing "null( MyOuterNamespace::MyInnerNamespace::MyClass* )",
when simply NULL will do the trick. Null (or nullptr) seems to
be a compromise which gives enough of the type checking
advantages, without being too verbose. (This seems to be the
experience of other languages, with stricter type checking, as
well.)
Aside from their other problems, "every-null" values cannot
possibly work properly with function overload resolution,
since they do not encode enough static type information.
For example:
template<typename T>
bool is_pointer(T) {
return false;
}
template<typename T>
bool is_pointer(T*) {
return true;
}
struct value_t { };
/* Introduce a compile-time abstraction, rather than hard-coding use
* of raw pointers in subsequent code. I have found this to be
* worth doing.
*/
typedef value_t* value_pointer;

Most people I know (myself included) consider this more
obfuscating than otherwise. (But I don't think we've ever tried
it where pointer was spelled out in clear, rather than using
some obfuscating abbreviation.)

In a few cases, I have found it useful---in such cases, I'll
usually use a typedef in the class, so the name of the pointer
type is Value::ptr. (The Ptr is historically conditioned; were
I inventing the idiom today, I think I'd write Pointer.) Most
of the time, however, this is because the class is designed to
be used with smart pointers, and the typedef is a smart pointer.
#include <iostream>
int main() {
/* False. Integer literals smell bad. */
std::cout << is_pointer(0) << '\n';
/* False. Macros smell bad. */
std::cout << is_pointer(NULL) << '\n';
/* True. Type-safe. Flexible. */
std::cout << is_pointer(value_pointer( )) << '\n';

And what does
std::cout << std::nullptr << '\n' ;
give?

To tell the truth, I'm not sure, but I have a sneaky feeling
that it is false. The standard overloads template functions for
nullptr_t in a lot of places where T* used; this could be used
here, by adding:

template<>
bool is_pointer( std::nullptr_t )
{
return true ;
}

On a related note, I'm not in love with any of the common
expressions returned from ::main, so I generally take
advantage of the fact that we're allowed to omit any such
expression. Here are the alternatives I've considered:
int main() {
/* There should be no need for the macro. */
// return EXIT_SUCCESS;
/* There should be no need for the integer literal. */
// return 0;
/* "Return the ordinary, default value of my return type." Makes
* sense, though sufficiently unfamiliar to most developers that
* it may initially hurt readability.
*/
// return int( );
/* Arguably ideal, though verbose. Self-documenting. */
typedef int result_type;
result_type const exit_success = result_type( );
return exit_success;
}

And how do you return failure?

What I'll usually do is:
return ProgramStatus::instance().returnCode() ;
The actual value returned will depend on which calls to
ProgramStatus::instance().setError( gravity ) ...
have been made. Somewhere deep down in there, there's an enum,
which is mapped in a platform dependent manner to the actual
return code, with the default mapping using EXIT_SUCCESS and
EXIT_FAILURE (but neither Unix nor Windows use the default
mapping---no point in throwing information away). Anyway, the
fact remains that you need more than one value.
 
S

SG

And what does
    std::cout << std::nullptr << '\n' ;
give?

To tell the truth, I'm not sure, but I have a sneaky feeling
that it is false.

It won't compile. nullptr is a keyword and not a name that's declared
in std:: namespace. Apart from that there will probably an ambiguity.
There are many operator<< overloads that take a pointer as parameter
and none of them is nullptr_t as far as I can tell.


Cheers!
SG
 
J

James Kanze

Ok, *now* you're dating yourself. :)

I learned C++ from the first edition of Stroustrup. Back when
there weren't really nested classes, much less templates and
exceptions.
One quickly tires of having to look up function signatures to
see what NULL is meant to represent in a given function call.
What aren't you passing, and why aren't you passing it?
/* I detest this. */
std::string const s = read_string(NULL);
/* I prefer: */
/* Somewhere near the top of the file, among configuration data. */
typedef char* buffer_pointer;
buffer_pointer const using_default_buffer = buffer_pointer( );
/* In some function body. */
std::string const s = read_string(using_default_buffer);
Anyway, I begin with the premise that pointer types are given
local typedefs, so the case of
some::long::name::that::defies::the::law::eek:f::demeter::anyway*
does not arise. What appear often, and I admit is a pain in
the neck, is the set of typedefs at the top of some function
or class definition:
typedef std::iterator_traits<Forward_iterator> traits;
typedef traits::pointer pointer;
typedef traits::reference reference;
// various types and constants...
I find this the lesser evil. YMMV.

As I said, I like the idea in theory. (Although I find the
typedef's that I've seen for this sort of thing more confusing
than anything else... why write somthing_or_other_pointer, when
somthing_or_other* says exactly what you mean. Not to mention
the confusion which can occur when you're dealing with pointers
to both const and non-const.) In practice, my experience seems
to coincide with that of the authors of languages with stricter
type checking (Pascal and its descendants, including Ada): you
do want to distinguish "null pointers" from integers, etc., but
it doesn't seem really necessary to distinguish the actual type
of a null pointer; there's a small advantage in doing so, but
not enough to justify the extra effort. But you're at the
limits, and I can easily understand your position.

(Technically, of course, one could argue that a null pointer
should be a different type: a T* points to a T, and a null
pointer doesn't point to anything, so it can't have type T*.
Except, of course, when you assign it to a T*, it will have type
T*, so the argument is IMHO fatally flawed.)
I use pointer for actual types, and ptr for templates. That
seems to be in keeping with the STL and Boost conventions,
e.g. iterator_traits<whatever>::pointer, but shared_ptr.

Historically: it was auto_ptr in the standard, and ...::pointer
in the STL. The real difference here, however, is that in
....::pointer, pointer is the complete name of the type; in
auto_ptr, it's not.
There's precious little benefit to hard-coding raw pointers,
but a strong up-side to using the typedefs consistently. I
don't always know ahead of time whether a smart pointer type
will be needed in a particular context. I'm also just more
comfortable working at a higher level of abstraction; I don't
always want to know the details. It's enough to know that I
can look under the covers when the need arises.

The fact that something is a pointer isn't really a detail. Of
course, you indicate this clearly in the name, so there's no
problem, but I still don't see where it buys that much.
result_type const exit_failure = !exit_success;

Except that that's not failure on some systems. For historical
reasons, you're dealing with an int, not a bool. As far as the
standard goes, there are three values you can use for that int
that have a meaning defined by the standard: 0, EXIT_SUCCESS and
EXIT_FAILURE (EXIT_SUCCESS may or may not be equal to 0).
Anything else is implementation defined.
I have no problem with that. You haven't hard-coded any
literals, or used any macros.

At the lowest levels, I have.
By the way, do you actually make use of the various return
values after the program returns? In the common case of using
non-zero exit values to indicate errors, my exit values are
effectively boolean, with details printed to stderr in natural
language.

I do and I don't. Internally, I distinguish between comment,
warning, error, fatal and internal error. (When you output an
error with fatal status, ProgramStatus will exit immediately,
and when you output with internal error, ProgramStatus will
terminate using abort().) Under Unix (and Windows), I'll map
them to different values, although I'm not really happy about
the mapping---warning returns 1, where as it probably should
return 0, with some other means used to return a 1 (without a
message), so you can do something along the lines of grep (which
returns 1 if it doesn't find the pattern, and 2 for any real
errors).

Which is, of course, the crux of the issue. At the kernel
level, Unix doesn't have the notion of success/failure (unless
you consider a core dump failure). Generally, the convention is
anything other than 0 is failure (but this is a pure
Unix'ism---under VMS, odd is success, and even is failure, and
when you return 0, the runtime maps it to some odd value). But
I've seen more than a few shell scripts which expand $? and test
for more different possibilities.
THe platform-dependent values presumably come from
platform-specific headers, so still no hard-coded literals.

Sooner or later, there is a "1", "2", etc. But yes---a lot
later, in a platform-specific header. From
syst-posix/system.hh:

static char const preferredOptId = '-' ;
static char const allowedOptId[] = "-" ;
static char const altOptId = '+' ;
static char const asciiEsc = '\\' ;
static char const preferredDirSep = '/' ;
static char const allowedDirSep[] = "/" ;
static char const pathSep = ':' ;
static bool const ignoreCase = false ;
static char const stdinName[] = "-" ;
static char const defaultTmpDir[] = "/tmp" ;

static int const exitSuccess = 0 ;
static int const exitWarning = 1 ;
static int const exitError = 2 ;
static int const exitFatal = 3 ;
static int const exitInternal = 4 ;

syst-windows/system.hh obviously looks a little different, but
the exit codes are the same. For VMS, if I supported it, the
exit codes would also be different. And for systems I'm not
sure about, I'd map them to EXIT_SUCCESS and EXIT_FAILURE.

Note that the names are fairly abbreviated. This is very old
code, and while the names don't meet my current standards, I'm
not about to try and find all of the places they're used, to
replace them.

Also, these were all macros for a long time (and the header was
shared with C).
If you do actually have to write out the literals, I would
expect them to be assigned to constants with meaningful names,
or at least to be accompanied by comments. The platform
headers may provide the expected values only as MACROS, but
c'est la vie, we live in an imperfect world.

The reason why EXIT_FAILURE and EXIT_SUCCESS are macros is
simple: the header that defines them is shared with C, and
macros are the only way to get a named constant in C.
Right, but those other values have meanings of their own, that
should be spelled out explicitly in the code. Your use of
enums suggests that you're already doing that.

Anytime I'm not returning success, I'm outputting an error
message to std::cerr as well. ProgramStatus has a member
function setError, which takes a "ProgramStatus::Gravity", and
returns an OutputStreamWrapper (so you can write:
ProgramStatus::instance().setError( ProgramStatus::error )
<< "error message with a value" << value ;
). If I do want to set a return status without outputting an
error message, there's also a setReturnCode function, but it's
little used. (If I had it to redo, I'd have warning map to
success as well, and invent some other code to return 1 in cases
like grep.)
 
R

Rolf Magnus

Pascal said:
AFAIK, this is not exactly exact. "address" is not a C notion.

Not right. Although the C standard doesn't define the meaning of the word
"address", that word is used a lot in the standard. Basically an address is
a pointer's value.
C has a notion of pointer. The pointer 0 cannot refer any object. But it
doesn't mean that the microprocessor address 0 may not be used to
store a object.

An address doesn't even need to be a simple linear number. It can be
anything.
For example, AFAIK, a C compiler could map pointers to addresses by
subtracting a small constant, eg. sizeof(int).

The only mapping that you get here is the mapping between pointer and
integer if you convert from one to the other, and that mapping is
implementation-defined. However, that's not really relevant, because 0 is a
special value in a pointer context. The literal 0 is a pointer value, while
all other integer values are not.

Basically, in the following code, p1 and p2 may end up having different
values:

int i = 0;
void* p1 = (void*)0;
void* p2 = (void*)i;
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,582
Members
45,065
Latest member
OrderGreenAcreCBD

Latest Threads

Top