Sign in to follow this  

NULL vs nullptr

This topic is 1106 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

I've been trying to embrace C++-11's features this past year, and one thing I keep wondering about is when to use nullptr. I'm currently using it for all of my pointers everywhere. The problem is, if I'm using a pointer to a class/struct that's defined in a library, use functions/methods also defined in that library to create and destroy it, and it assigns NULL instead of nullptr, then should I be using NULL to initialize my pointers, and making the appropriate NULL-checks in general use instead of nullptr? I use a lot of libraries written in C, and built with a C compiler. These libraries won't even be able to use nullptr within the scope of their code-base let alone be built to use nullptr instead of NULL.

Share this post


Link to post
Share on other sites

There is a type safety aspect to nullptr.  See the example below

// Declarations
int func(int Input);
int func(int* Input);
 
func(0);
func(NULL);
func(nullptr);

Which version of the function gets called for each line?

Edited by Rattrap

Share this post


Link to post
Share on other sites

I believe SiCrane used to cite as an example that member function pointers on a popular compiler can be bigger than regular pointers, and have a non-zero bit pattern for the "null" value.

Yes, that's a good example for another pointer/size pitfall. But note that it does not apply to the null pointer itself. A value of type nullptr_t is explicitly defined [3.9.1/10] to have a size of sizeof(void*). It can however be converted to a pointer to member (presumably changing its size!).

Share this post


Link to post
Share on other sites

I have two very large codebases that disagree with you very hard on this one. You're forgetting generic code, methinks.

You cut off 'The other use is inside templates'.
IMHO, the only place I'd use it is deep inside some template metamagic. For day to day code, like initializing local pointer variables, there's no difference between it and 0 (AKA NULL), so the best thing to do is just stick with existing conventions of the rest of the codebase (which in my case is literal 0).

Also, many kinds of serialization and file streaming APIs. I've run into overload problems with things like `Write(int)` vs `Write(char const*)` and needs to write out explicit NUL values often enough (there's still issues with all the implicit conversions between integer types, but that's solved already by type suffices; pointers were not, until now).

Personally that's a non-problem to me. Passing literals into serialization functions is simply bad code(tm).
What if there's overloads for Write(void*), Write(Object*), etc... Your code for serializing a "null string" by hijacking nullptr is hard to read for someone new to the code (having to read through multiple files to answer "what does Write(nullptr) mean? Oh it's a missing char array!"), but more importantly it's tying the your client-code to an implementation detail of the serialization library -- each of those calls is assuming that Write(char*) is the only pointer overload. If another pointer overload is added later, all your "null string" code breaks!

Hiding explicit programmer intentions behind implicit conversions should get you reprimanded in code review.

Even with the integer overloads, I'd consider passing literals into a heavily overloaded function like this 'Write' to be bad code(tm).
In any case, you are explicitly specifying one overload to call, but the code makes it non obvious.
It's much more readable to assign to a variable first, use a cast, or a template arg.
e.g.
Write(0); // ?-- no!
u32 i = 0; Write(i);
Write((u32)0);
Write<u32>(0);

Share this post


Link to post
Share on other sites
so the best thing to do is just stick with existing conventions of the rest of the codebase (which in my case is literal 0).

 

The best case for your code base may indeed be to use old conventions, but that does not mean that it is a good idea to recommend everyone working on new projects follows the conventions from back when your project started.

 

nullptr is superior to 0 because it prevents silent errors.

 

IMO standard library writers should have used something like nullptr as the expansion of NULL before C++11. It'd only require a little compiler help to make code like pow(NULL) a warning.

Share this post


Link to post
Share on other sites

There is a type safety aspect to nullptr.  See the example below

// Declarations
int func(int Input);
int func(int* Input);
 
func(0);
func(NULL);
func(nullptr);

Which version of the function gets called for each line?

 

This won't compile in GCC without throwing an error.

You have to cast 0 to the correct type so GCC knows which function to call.

Share this post


Link to post
Share on other sites

Is it just me, or are ambiguous overloads like Write(int) and Write(int*) a giant code smell in the first place? Seems to me like using nullptr to select the correct overload is only bandaging what was a poor interface to begin with. What does Write(int*) even do? Write the pointed to value? Write the pointer value itself? What if that were Write(char*) instead? Now you introduce the possibility that it could be writing a constant C-string, and the original author just forgot/omitted the const for whatever reason. To me this isn't convenience, it's a maintenance nightmare.

Share this post


Link to post
Share on other sites

I use nullptr everywhere, but I admit NULL or 0 would be just as good except in a few corner cases, as long as you are consistent.

 

Also, with '0', I can't tell if function parameters are taking a pointer or an integer, which is occasionally annoying, so I prefer NULL over 0, if I can't use nullptr.

 

One annoyance in C++11, is you can't use nullptr for pure virtual functions. sad.png

virtual void MyMemberFunc() = nullptr; //Nope!

(yea, I know the virtual func isn't a pointer; still, I'd like it, or a keyword, anyway)

Edited by Servant of the Lord

Share this post


Link to post
Share on other sites


Is it just me, or are ambiguous overloads like Write(int) and Write(int*) a giant code smell in the first place?

 

Arbitrarily encoding parameter information into the function name is a bad idea -- one that has outlived its usefulness stemming from languages and times before such overloading was possible. For one, what do you do when parameter types change? If you're being consistent, you then have to change the function name and update it at every callsite as well -- even if otherwise the change in parameter type was transparent to the callsite (e.g. whatever conversions were introduced due to the change were correct and desirable). Plus, every distinct name is a new verb that you have to remember in the language of your program's source code.

 

A function's name should say only what it does. The fact that "Write" is ambiguous is not the fault of its parameters -- You might want to Write an integer just as you might want to write a float, or you might want to WriteWithTerminator an integer just as you want to WriteWithTerminator a float. Conceptually, these latter functions perform the same action regardless of their parameters, even if the terminator for an integer is different from the terminator for a float. Indeed, this ability to change behavior solely on the type of a parameter is very desirable -- its an analog to polymorphism but for single functions, where the name of the function itself is effectively acting as an abstract base, and the various overloads acting as specializations of that base.

Share this post


Link to post
Share on other sites
I never advocated encoding parameter information into function names.

A function's name should say only what it does. The fact that "Write" is ambiguous is not the fault of its parameters -- You might want to Write an integer just as you might want to write a float, or you might want to WriteWithTerminator an integer just as you want to WriteWithTerminator a float. Conceptually, these latter functions perform the same action regardless of their parameters, even if the terminator for an integer is different from the terminator for a float. Indeed, this ability to change behavior solely on the type of a parameter is very desirable -- its an analog to polymorphism but for single functions, where the name of the function itself is effectively acting as an abstract base, and the various overloads acting as specializations of that base.


Except that in the case of [font='courier new']Write(char*)[/font], the function name doesn't say what it does. It could do one of several things as I pointed out above. It's not the parameters' fault that it's ambiguous, I agree. But by your own admission, it's the fault of the function name... so you change the function name. You don't include it as one of the possible overloads of [font='courier new']Write[/font]. You make [font='courier new']WritePointerValue[/font] (and have a set of overloads to handle various pointer types), [font='courier new']WritePointedToValue[/font], [font='courier new']WriteString[/font], for C-strings, etc.

Share this post


Link to post
Share on other sites

I never advocated encoding parameter information into function names.




A function's name should say only what it does. The fact that "Write" is ambiguous is not the fault of its parameters -- You might want to Write an integer just as you might want to write a float, or you might want to WriteWithTerminator an integer just as you want to WriteWithTerminator a float. Conceptually, these latter functions perform the same action regardless of their parameters, even if the terminator for an integer is different from the terminator for a float. Indeed, this ability to change behavior solely on the type of a parameter is very desirable -- its an analog to polymorphism but for single functions, where the name of the function itself is effectively acting as an abstract base, and the various overloads acting as specializations of that base.


Except that in the case of Write(char*), the function name doesn't say what it does. It could do one of several things as I pointed out above. It's not the parameters' fault that it's ambiguous, I agree. But by your own admission, it's the fault of the function name... so you change the function name. You don't include it as one of the possible overloads of Write. You make WritePointerValue (and have a set of overloads to handle various pointer types), WritePointedToValue, WriteString, for C-strings, etc.


Seems like you're getting tripped up on semantics because you're using the wrong parameter type.

Write tells you exactly what it does.

If you want to write a string... then you make "Write(std::string)". If you want to write an array, then you "Write(std::array<int>)" - or more likely "template<typename ConstIterator> Write(ConstIterator<int>, ConstIterator<int>)".

Assigning arbitrary meaning to some random type that the type never had is the problem, not the naming of the function.

"char*" means "pointer to character". Not "pointer to a zero-terminated array of characters".

Well, unless you're using C, in which case you can't overload anyway. Edited by SmkViper

Share this post


Link to post
Share on other sites


Seems like you're getting tripped up on semantics because you're using the wrong parameter type.

Write tells you exactly what it does.

If you want to write a string... then you make "Write(std::string)". If you want to write an array, then you "Write(std::array)" - or more likely "template Write(ConstIterator, ConstIterator)".

Assigning arbitrary meaning to some random type that the type never had is the problem, not the naming of the function.

"char*" means "pointer to character". Not "pointer to a zero-terminated array of characters".

Well, unless you're using C, in which case you can't overload anyway.

I'm not using the wrong parameter type. The person who wrote the interface I'm coding against used the wrong parameter type. Yet I wouldn't know that until I attempted to use it and observed incorrect behavior, because the function name wasn't descriptive enough for the char* overload and the original author was relying too heavily on parameter information to imply behavior. Although you're right that this is C++, so I don't want to lean too heavily on my contrived C-string example. It was only one hypothetical case.

 

However this brings me back to my original point. The canonical use case of nullptr is dealing with ambiguous overloads, yet that fact that one would even have multiple ambiguous overloads where a parameter could either be a integral type or a a pointer type already seems highly suspect to me. I've noticed that serialization libraries tend to be the worst offenders in this regard since they have highly generic, nondescript function names (Read/Write) that support dozens of different overloads. It's highly unlikely that the verbs 'Read' and 'Write' can accurately and concisely convey their behavior for all the possible type overloads they support.

 

Function overloading is a great feature, but this is one example of how it's often abused.

Share this post


Link to post
Share on other sites
Ambiguous overloads are harder to avoid with overloaded operators. Consider ostream <<. Has to have both int and pointer overloads.

I was under the impression from a Microsoft interview with one of the VC developers that nullptr was required to allow perfect forwarding to work with varadic templates.

Share this post


Link to post
Share on other sites


Except that in the case of Write(char*), the function name doesn't say what it does. It could do one of several things as I pointed out above. It's not the parameters' fault that it's ambiguous, I agree. But by your own admission, it's the fault of the function name... so you change the function name. You don't include it as one of the possible overloads of Write. You make WritePointerValue (and have a set of overloads to handle various pointer types), WritePointedToValue, WriteString, for C-strings, etc.

 

Maybe, but for myself I wouldn't introduce new verbs if it performs the same logical function (that is, its part of the same package of functionality, and is meant to be interchangeable with other "writes" in that context). Instead, by observing that writing/serializing a pointer itself is typically not useful because the address won't be preserved across sessions, I would simply avoid the confusion by explicitly converting it to a type that represents a handle that can be re-hydrated when the file is read back. With that in place, you then have write(int) which writes the integer value provided as a parameter, you have write(int*) which writes the integer value stored at the address provided by the parameter, and write(my_handle<int>) which writes some representation that makes it easier to correlate and re-hydrate its relationship to some integer. For the rare cases that you might actually want to write the actual address of some data (say, for debugging, or a hardware address), simply cast the pointer to an appropriate type like std::size_t and provide an overload for it (be aware the size_t doesn't apply to member pointers). You can also treat indexes/pointers into contiguous arrays specially since there's a common base address.

 

Also keep in mind that if you want your serialization to be cross platform, you need to be mindful of 32bit vs. 64bit systems, of alignment requirements on different systems, endianness and other things.

 

See also: std::addressof for completeness of implementation.

 

 

All of that said, if you're working with an existing system that you can't change or can only change in limited ways, simply providing a different name for the function might be the path of least resistance.

Share this post


Link to post
Share on other sites


Ambiguous overloads are harder to avoid with overloaded operators. Consider ostream <<. Has to have both int and pointer overloads.

Very true, you often don't have a choice with overloaded operators. And to be honest, I try to avoid using them if there could be any confusion over their behavior. A classic example is a vector class that overloads the multiplication operator for itself. Is that a dot product or component-wise multiplication?!

 

I wouldn't consider the const void* overload of ostream << ambiguous, either, since there's not much else you can do with a void pointer in that context other than treat it as an integral value.

Share this post


Link to post
Share on other sites

With that in place, you then have write(int) which writes the integer value provided as a parameter, you have write(int*) which writes the integer value stored at the address provided by the parameter, and write(my_handle) which writes some representation that makes it easier to correlate and re-hydrate its relationship to some integer.

 

To me that would make write(int*) unnecessary, since I could just deference my pointers and it would resolve to the write(int) overload. Same with the other pointer types. Unless you're using write(int*) because it handles null pointers somehow. However more often than not, the calling code is already handling null pointers and missing data, because that may need to be handled differently depending on what's being serialized (sometimes you should write nothing, sometimes you write zero or some other sentinel value, sometimes you throw an exception, etc.).

 

 

 

For the rare cases that you might actually want to write the actual address of some data (say, for debugging, or a hardware address), simply cast the pointer to an appropriate type like std::size_t and provide an overload for it (be aware the size_t doesn't apply to member pointers).

 

Exactly, which is why I don't see the utility in the pointer overloads. Maybe we'll just have to agree to disagree.

Share this post


Link to post
Share on other sites

This topic is 1106 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

Sign in to follow this