7

For instance, would this c++ function be a good idea?

void doSomething(not_nullptr<MyType> arg)
{
    // stuff
}

With not_tullptr being a template wrapper for pointers, that will throw an exception if a null value is ever assigned to it. It has the advantage that it finds errors fast, and clearly documents assumptions made directly in the function prototype.

The traditional way to do something like this would be:

void doSomething(MyType* arg)
{
    assert(arg != nullptr);
    // stuff
}

This method accomplishes the goal of finding null errors fast. But it does not itself document that assumption in the prototype. So my question is, is the idea above a good one? It is not the standard way of doing things in the c++ and could of course be expanded to other assumptions.

  • I reject the premise of your question slightly as you're doing quite a bit more here than just documenting assumptions. – Lightness Races in Orbit Feb 25 '16 at 22:15
  • 3
    This is a VERY good idea. It BOTH documents the assumption AND it hardwires the null guard into the code. – John R. Strohm Feb 25 '16 at 22:18
  • 15
    The traditional way is to use a reference: void doSomething(MyType& arg). This clearly documents assumptions made. – Sjoerd Feb 26 '16 at 2:37
  • @Sjoerd - as long as you're aware of the convention. Probably it should be made explicit in a style guide for the project. – Jules Feb 26 '16 at 10:38
  • 1
    the notnull_ptr template does not go against the standard, as template types that throw exceptions are perfectly valid C++ expressions. The assert version is generally a hangover from C code. Not that that is bad, as the assert is very simple and easy to read and maintain. I think you mean "common conventions" of language code, and in that respect I think breaking commonality is something to be considered only with extreme good reason. – gbjbaanb Feb 26 '16 at 13:59
8

Everything has a cost, even if it isn't measured in runtime performance.

Encoding such assumptions into the type system sounds like a good idea. But it is not without its flaws. In particular, it requires you to have and use a bunch of increasingly specific types for increasingly specific assumptions.

Let's say that you have a function that takes an array from the user and modifies the first three elements in it. Now, this function makes two assumptions: that there's actually an array and that the array is at least 3 elements long.

There are types which can encode both of these assumptions. The guideline support library type span can cover both of these. But just look at the code for that type. If it weren't available, you probably wouldn't write it yourself.

The more such assumptions you have, and the more special-case they get, the harder it is to write a type just for them. After all, span only solves this particular problem as a partial by-product of solving its real problem: having a way to represent an array of some size.

So it's a balancing act. You don't want to spend more time writing special-case types, but you do need some to cover a lot of bases. Where exactly you draw the line depends on your needs, but I don't feel that trying to encode everything into the type system is worthwhile.

Also, having contracts as part of C++, which people are working on (PDF), would be able to bridge the gap here in many of the special cases.

There is also the issue of dealing with combinations of such contracts. The not_null contract is generally a good idea, but by its very nature it cannot work with move-only types that leave the moved-from object null. Thus, not_null<unique_ptr<T>> is not a functional type.

Again, that's not to say that you shouldn't have these. But you really need to think about when it is truly appropriate to have a type encapsulate a contract and when it is not.

  • 3
    Another problem along similar lines is combinatorial explosion. So you have a non-null annotation. You'll also want to track object lifetime (either using shared/unique pointers or through some other annotation). Perhaps also whether the object is required to be thread safe. Now you either need a standard way of wrapping in order, which will be a maintenance headache, or combined types for each combination, which could easily add up to a very large number of wrapper types. – Jules Feb 26 '16 at 10:45
  • @Jules Rust's type system encapsulates exactly those parameters though the (mostly) the standard library and a little language support – Daenyth Feb 26 '16 at 13:45
  • 1
    @Daenyth Yes. And with language support it works, the problem is that to do it in C++ will be a nightmare because you'll end up having to write wrapping and unwrapping code every time you need to pass an object to a recipient that has a slightly different set of requirements. Or flatten it so that you can implement implicit conversions, but have a very large number of types instead. – Jules Feb 26 '16 at 20:48
  • I don't disagree with that. I was just pointing out that it's an issue with C++, rather than being impossible in general. – Daenyth Feb 27 '16 at 1:06
3

For instance, would this c++ function be a good idea?

Yes. This is such a good idea, that the Guidelines Support Library contains an implementation of it.

(The template name is gsl::not_null<T*>).

2

I don't know if this particular example it is standard in C++, but yes it is a good idea.

Various languages have various ways of handling null checking, some of them allow some of the null checking to be done at compile time.

Whenever you can check or enforce something at compile time it is a big a plus. The type system is a great way to do that, and, it is live (stays up-to-date, or doesn't go out of date like documentation can) and virtually self documenting (it is clear that or where some specific type is needed)!

[One way to think about the type system and compile time checking is that they work to prevent illegal (and unexpected) program states.]

Some languages have an optional or a "maybe" capability. Swift, a modern language from Apple has such a construct and encourages compile time null analysis. These capabilities require that you check for null before using. If I recall correctly, the object referencing types cannot be null in Swift; you have to use the optional feature to get even have a null reference. I think other languages would have done this if they had the benefit of foresight (Haskell does this also, with its "maybe").

What you're proposing goes the other way (when setting), which is great, when appropriate!

0

No, it is not good.

Much better way is to test your code at various levels, and to use references, instead of raw pointers.

  • Testing is not mutually exclusive with modeling an interface in a way to establish preconditions (e.g. the "not null" in the question). Personally, in that specific example, I would use a reference and have unit tests in place. – user22815 Mar 3 '16 at 16:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.