Hello everybody.
The problem I want you to discuss may have been already reviewed here,
but I did not found anything similar in the mailing list archive. I was
trying to explain the idea as well as I could, but I'm not a native
speaker, so I might fail at some points. Excuse me for that.
Have an nice weekend,
——— Pavel Kretov.
=== The problem ===
This is a common practice in programming to check input parameters for
functions. As many parameters are pointers, smart or regular, checking
them against "nullptr" is a *very* frequent operation, but what should
you do if pointer is null? There are many options:
— silently return from the function?
— print warning message and return?
— throw an exception?
— call abort() from <cstdlib>?
— use some kind of ASSERT() and hope it will do the right thing?
— skip the check altogether and get a segfault in case?
The exact choice may be different for different projects but each has
its own drawbacks.
— If you chose to silently return, can you be sure that this is safe
for the caller to stay unaware? Missing required parameter is a
programmer's mistake and they should be told of it as soon as
possible.
— If you chose to throw an exception, which one would you throw? Unlike
languages like Java, C# or Python, C++ has a relatively poor set of
predefined exceptions. It is also considered to be a good practice
in C++ to trow and exception only in case of really exceptional
situations and a single null pointer does not seem to be that much
serious matter. Moreover, when choosing exceptions you will have to
either pollute your functions' throw lists with std::invalid_argument
or just omit them entirely. The latter means it is easy for your code
user to forget to catch exceptions at all.
— Calling abort(), directly or as result of ASSERT, is no better then
silently ignoring the error. I'm pretty sure nobody wants their
enterprise level system to shut down without even syncing disk cache
and database transactions due to forgotten null pointer passed to
logger function.
— Ditto for not checking arguments at all.
=== The proposed solution ===
The problem could be completely eliminated in many cases if pointers
were not allowed to carry invalid values at all. These days C++ has a
strong movement toward smart pointers, leaving plain C pointers as a
legacy option or for very low-level things. Classes std::shared_ptr and
std::weak_ptr are commonly seen as a replacement.
Shared pointers are designed to mimic the behavior of plain pointers,
they can wrap any pointer, even the one which is not safe to deallocate
using "delete" operator. It offers its user a great number of ways to
misuse it.
I propose to create a subclass of shared pointers, which is somewhat
guaranteed to hold a valid object which can be safely deallocated. This
class should:
— be unable to hold nullptr;
— have no conversion constructors from plain pointers;
— have object allocation to be the only way for pointer to be obtained.
We should also provide a way to create std::object_ptr from
std::shared_ptr or even plain pointers for legacy purposes, but there
should be the only one point there such a conversion is allowed. I'm not
quite sure about the exact name and signature, but it should be a single
function like that:
template <typename T>
std::object_ptr<T> object_ptr_cast(const std::shared_ptr<T>&)
throw(std::invalid_argument);
=== Conclusion ===
Eliminating null pointers validation may seem to be not that big deal to
create a special pointer type for that, but indeed it is a great
problem: thousands of people around the world, writing programs in
various languages, every day performs a lot of useless checks against
NULL, just to throw an exception which in most cases will never be
caught.
We must do something to left those legacy checks behind and thus
improve code readability and "self-documentability".
To improve the situation, Java has introduced @NonNull arguments
attribute years ago (which is anyway broken), Scala uses the Optional[T]
pattern (as many other functional languages). There are languages like
Kotlin, which sees "T" as non-nulable reference by default and require
user to explicitly declare nullable references like "T?". (Moreover,
Kotlin can check arguments during compilation, with respect to code path
branching.) I think this approach will be widely adopted in the future
programming languages and C++ must take some steps in this direction.
Thank you for your long, detailed response.
As a foreword to my answer I'd like to say that my proposal is not to
replace old good references with a custom template class, but to replace
std::shared_ptr in many cases with its non-nullable version. I feel
strong about making non-nullable pointer the default option with
nullable pointer as a special case. I don't consider regular pointers as
I believe that future programs will use them exceptionally rare.
> If a function takes a pointer type, and it cannot work if it is
> given a NULL pointer, then what has happened here is a violation of
> the implicit contract between the function and whomever called it.
> And a violation of this contract represents a program error, which
> means that the program is no longer in a reasonable state.
The point is to turn that implicit contract into an explicit one. Now it
can be frequently seen in functions' documentation comments:
/// @param ptr: Pointer to something, must not be null.
This is the implicit contract, it is nice and easy, until you want to
actually enforce it. Doing that leads to great deal of problems, or you
will end up with a halted program.
> Therefore, the program needs to halt execution.
In some cases you don't really want the entire program to halt, but, for
example, to unload the plugin, cleanup after it and reload, while
sending the programmer a happy letter about an accident.
Never letting
such a situation happen due to just a missed null pointer seems to be a
better approach to me, as these days programmers have a lot of much more
serious problems then dealing with null pointers.
> If the user doesn't catch an exception, the application halts. If the
> application can't halt, then that's the problem of the user. That
> being said, I wouldn't throw an exception just for a NULL pointer. A
> pointer being not-NULL is a precondition, and a violation of a
> precondition should not result in an exception.
>
> It should result (in debug) in program termination.
And what would you do in release? "Live fast die young" is suitable not
for each program. Nobody is guaranteed against accidental invalid
pointer, the proposal is to eliminate the problem entirely.
> Also, "functions' throw lists" are no longer part of C++.
Okay, my fault. Thank to you for teaching me the difference between
"throw()" and "noexcept" — the former is deprecated :). But you you
still unable to mark function with "noexcept" keyword if you throw an
exception when checking nullable parameters.
> If you're talking about an application that isn't allowed to
> terminate, then that means that your contract with the outside world
> needs to be permissive. Which means you have to accept NULL and
> define what should happen when you're given it. So you need to
> figure out how to do something innocuous if you get a bad parameter.
That permissiveness makes code paths more complex and can lead to
undesired consequences. For example, you have got invalid pointer as a
constructor parameters, what would you do? You cannot cancel object
construction,
but you don't want to construct invalid object either. So
you have to add more logic to you class to distinguish between properly
constructed objects and failback-constructed objects. Or throw an
exception which you generally dislike to do.
> And what if I already have a pointer, perhaps obtained via transfer
> of ownership from the outside world? Or if I need to use a special
> deleter on it?
>
> Why not just have the constructor that takes a pointer check for
> NULL and fail if it sees it? It can be explicit (just like
> shared_ptr's pointer constructor), so it can't accidentally acquire
> ownership.
We should definitely have the way to archive this, but I personally
would prefer to separate it from object_ptr class, just to make it
*extremely* clear for user that this is a point there ownership is
acquired or re-acquired.
Given that, the object_ptr itself can stand free from checks and exception-raising.
> I disagree with the idea that this is necessarily common (or at
> least, that it *has* to be). It is far more common to pass an object
> to a function without transferring ownership to it. In such cases, a
> reference is entirely appropriate.
I'm not about not using references there they should be.
> Given the above, if you're passing a pointer, then it's usually
> something that is one of the following: 1) can't be a reference (for
> some reason, like a naked C array or whatever), 2) is conceptually
> nullable (and therefore, the function is expecting to take a
> possibly null pointer), or 3) is a transfer of ownership. The cases
> you're worried about are #1 and #3.
>
> I don't think they happen often enough to require the creation of a
> whole new object type.
But they are often, at least the third one. If it wasn't, no smart
pointers would be introduced to the language at all.
If the user doesn't catch an exception, the application halts. If the application can't halt, then that's the problem of the user. That being said, I wouldn't throw an exception just for a NULL pointer. A pointer being not-NULL is a precondition, and a violation of a precondition should not result in an exception.
It should result (in debug) in program termination.
I don't consider regular pointers as
I believe that future programs will use them exceptionally rare.
This is the implicit contract, it is nice and easy, until you want to
actually enforce it. Doing that leads to great deal of problems, or you
will end up with a halted program.
> Therefore, the program needs to halt execution.
In some cases you don't really want the entire program to halt, but, for
example, to unload the plugin, cleanup after it and reload, while
sending the programmer a happy letter about an accident. Never letting
such a situation happen due to just a missed null pointer seems to be a
better approach to me, as these days programmers have a lot of much more
serious problems then dealing with null pointers.
That permissiveness makes code paths more complex and can lead to
undesired consequences. For example, you have got invalid pointer as a
constructor parameters, what would you do? You cannot cancel object
construction, but you don't want to construct invalid object either. So
you have to add more logic to you class to distinguish between properly
constructed objects and failback-constructed objects. Or throw an
exception which you generally dislike to do.
So I think what you're proposing would be better called "shared_ref". You'll also need "unique_ref", possibly "weak_ref", and certainly "observer_ref" as a typedef of "reference_wrapper" to complete the set.
On Sunday 08 March 2015 08:24:40 Pavel Kretov wrote:
> This is a common practice in programming to check input parameters for
> functions. As many parameters are pointers, smart or regular, checking
> them against "nullptr" is a very frequent operation, but what should
> you do if pointer is null? There are many options:
I was very much in favour of your idea until I read further. I thought you
were proposing a nonnull_ptr<T> which just wraps a T*, in the spriti of
Bjarne's Type-Rich Interface principle. But when I read "inheriting from
shared_ptr", I was disappointed.
What's wrong with
template <typename T>
class nonnull_ptr {
T *ptr;
public:
/* implicit */ nonnull_ptr(T *t) : ptr(t) { if (!t) std::terminate(); }
// or throw
// all you can do with a T*, except maybe pointer arithmetic
};
?
Thanks,
Marc
On Sunday, March 8, 2015 at 5:32:10 PM UTC+1, Nicol Bolas wrote:If the user doesn't catch an exception, the application halts. If the application can't halt, then that's the problem of the user. That being said, I wouldn't throw an exception just for a NULL pointer. A pointer being not-NULL is a precondition, and a violation of a precondition should not result in an exception.
It should result (in debug) in program termination.I agree, but what's the proper way to do that?assert doesn't generally work in release / NDEBUG mode and we don't have a variant of assert that does.
I think that normal references are the answer to most of Pavel's problems with pointers.
> I think "object_ptr" is very much the wrong name. Why? A big part of the
> problem is that the name obliterates the most important word in its
> predecessor: *shared*. What ownership semantics does "object_ptr" have:
> shared, unique, none, something else? What would you name the "unique_ptr"
> analog to "object_ptr", or the "weak_ptr" equivalent? You'll still need
> those to complete your interface (well, maybe not "weak_ptr"; that one you
> can probably reuse).
Maybe you're right. I was thinking a lot about choosing appropriate name
for this entity. I started from "notnull_ptr", then thought of
"shared_ref", but later decided it would be nice to employ term "object"
in the meaning which I remember from reading C standard text:
«Object — a region of data storage in the execution environment,
the contents of which can represent values. Except for bit-fields,
objects are composed of contiguous sequences of one or more bytes,
the number, order, and encoding of which are either explicitly
specified or implementation-defined.»
But to my surprise, when I've checked C++ 2014 draft I did not find this
term. Name "shared_ref" seems to be appropriate for me, but we will be
unable to make it behave like real reference as we cannot overload
"operator.", it still will act like a pointer.
> Pointers are how you refer to objects that might not be there. References
> are how you refer to objects that *must* exist. Even though language
> references are not rebindable after creation, std::reference_wrapper is. So
> it's very much the same concept as what you want, only without the
> ownership semantics.
But without ownership semantics the whole proposal looses its meaning.
> The only way this class solves the problem is if the user is *always* using
> it. If they used it from the moment they got that pointer, and passed it
> along to you that way.
> But shared_ptr beat you to it. It's already there, with lots of people
> using it for memory management. Not to mention the innumerable books and
> teaching materials beating "shared_ptr" into people's heads. Without
> widespread adoption of the type, the problem doesn't really get fixed.
You're freaking perfectly right. That is the sad part of story and the
reason why I wrote my proposal into the mailing list associated with C++
standard committee instead of my Facebook page. :)
> Well, you shouldn't be marking functions "noexcept" frequently to begin
> with. But even if you want it to be noexcept, let it. noexcept doesn't mean
> "nothing gets thrown beyond this point". It means "if an exception gets
> past me, kill the application." Therefore, the onus would be on the
> implementer of the function to catch any exceptions that he doesn't want to
> terminate the app.
Thank you once again for your explanation. I liked the idea of throw
lists a lot, given that that lists are to be checked by the compiler
whether possible. "Noexcept" seems to be quite different. It seems I
must read my C++ book once again.
> The vast majority of parameters are (or should be) taken by value or by
> reference. By smart pointer is, compared to all parameters used in an API,
> a relatively rare occurrence. Yes, it does happen. But it doesn't happen so
> frequently that you need a whole new type to cover "by smart pointer,
> previously assured to not be NULL".
Maybe. (This was on "need a whole new type", not on "should be
[preferably] taken by value or reference".) But, as you see from our
discussion, how do you, personally, think, should I prepare the official
proposal or the whole idea is doomed?
int foo(const object_ptr<int>& i) {
return (int)i * 2; // nice boy, does not need to check !
}object_ptr<int> caller(int *p, std::shared<int> sp) {
int *a = new int(1);
int b = 2;
foo(a); // should be ok ( even when a is a local )
foo(&b); // should be ok ( even when b is a local )
// doubts:
foo(p); // doubt 1 : should check for p==nullptr, if true, then what ? throw an exception ? build a default int ?
foo(b); // doubt 2 : this is possible ? since ( b is not a pointer )
foo(3); // doubt 3 : this is possible ? since 3 is an int const and foo parm also a const;
foo(sp); // doubt 4 : should check for sp==nullptr, if true, then what ? throw an exception ? build a default int ?
return &b; // doubt 5 : what happen here ? since b is local.
}
On Monday 09 March 2015 14:49:28 Nicol Bolas wrote:
> That already exists; it's called a *reference*.
Yes, yes, the expected knee-jerk reaction... :)
"We don't need a Speed class", said the NASA engineer, "because that already
exists: it's called a *double*."
So tell me: since when can references be re-seated?
A reference is *not* a non-nullable pointer.
ensured_shared_ptr<T,deleter,dummy_nullptr_instance_allocator>For a concrete example: how would std:.reference_wrapper help with expressing
the requirement that std::basic_string(const CharT*) requires a non-null
argument?
Requires: s points to an array of at least traits::length(s) + 1 elements of charT.