3

We are writing safety-critical code and I'd like a stronger way than [[nodiscard]] to ensure that checking of function return values is caught by the compiler.

[Update]

Thanks for all the discussion in the comments. Let me clarify that this question may seem contrived, or not "typical use case", or not how someone else would do it. Please take this as an academic exercise if that makes it easier to ignore "well why don't you just do it this way?". The question is exactly whether it's possible to create a type(s) that fails compiling if it is not assigned to an l-value as the return result of a function call .
I know about [[nodiscard]], warnings-as-errors, and exceptions, and this question asks if it's possible to achieve something similar, that is a compile time error, not something caught at run-time. I'm beginning to suspect it's not possible, and so any explanation why is very much appreciated.

Constraints:

  • MSVC++ 2019
  • Something that doesn't rely on warnings
  • Warnings-as-Errors also doesn't work
  • It's not feasible to constantly run static analysis
  • Macros are OK
  • Not a runtime check, but caught by the compiler
  • Not exception-based

I've been trying to think how to create a type(s) that, if it's not assigned to a variable from a function return, the compiler flags an error.

Example:

struct MustCheck
{
  bool success;
  ...???... 
};

MustCheck DoSomething( args )
{
  ...
  return MustCheck{true};
}

int main(void) {
  MustCheck res = DoSomething(blah);
  if( !res.success ) { exit(-1); }

  DoSomething( bloop ); // <------- compiler error
}
  

If such a thing is provably impossible through the type system, I'll also accept that answer ;)

daemacles
  • 302
  • 1
  • 6
  • 1
    If checking the return value is so important, why don't you throw an exception? – Peter Jul 21 '21 at 01:10
  • 8
    Although compilers are "encouraged" to issue a warning with `[[nodiscard]]` if the return value is discarded, most compilers can be configured to treat warnings as errors, and cease compilation. Microsoft compilers and IDEs support such an option. – Peter Jul 21 '21 at 01:12
  • C++ just doesn't work this way. The native way to do this in C++ is to throw an exception upon failure, so a successful return from a function indicates that it succeeded. – Sam Varshavchik Jul 21 '21 at 01:14
  • @Peter I understand about warnings-as-errors, but that is still based on warnings – daemacles Jul 21 '21 at 01:20
  • 2
    Also, bear in mind, that any approach that (attempts at) forcing the caller to check a return value can be explicitly circumvented by the caller. For example, if the compiler issues warnings/errors because of `[[nodiscard]]`, the caller can still beat the compiler into submission with a simple cast i.e. `(void)DoSomething(bloop)`. There is a point where you have to rely on other programmers reading documentation and doing what is needed. – Peter Jul 21 '21 at 01:20
  • @SamVarshavchik Thanks for the comment - I updated the question to clarify that we can't use an exception here. I understand this is not the "native" way. The question is whether it's possible. – daemacles Jul 21 '21 at 01:23
  • 8
    If a warning is turned into an error, it's a true error that will fail the build. Why is this a problem? Isn't it what you want? – Some programmer dude Jul 21 '21 at 01:23
  • And what is the *actual* problem this is supposed to solve? Right now this question is more of an [XY problem](https://xyproblem.info/). – Some programmer dude Jul 21 '21 at 01:25
  • 1
    Your MustCheck could check in its destructor (one that is marked `noexcept(false)`) if the object was checked, and if not then throw. With the caveat that if it throws in the context of a throw-in-flight, it will terminate the application. – Eljay Jul 21 '21 at 01:27
  • @Peter that's completely true - and (void) casting requires the programmer to explicitly acknowledge they are intentionally circumventing the compiler. If context helps, this is for an API with a large surface area and a lot of junior programmers where we want to add explicit DANGER WILL ROBINSON alerts in the manner I described – daemacles Jul 21 '21 at 01:27
  • @Eljay yes, I had a similar idea that would be caught at runtime, and that might be what we need to go with, however I was wondering if there is a way to encode this behavior in the type system itself such that "not checking" is equivalent to a language/type error. – daemacles Jul 21 '21 at 01:31
  • 1
    Nope, C++'s type system does not work this way. – Sam Varshavchik Jul 21 '21 at 01:36
  • 2
    DoSomething could take two parameters: lambda to call on success, and lambda to call on failure. That way, the caller is responsible for handling the success and failure cases explicitly (even if the handler is a no-op). – Eljay Jul 21 '21 at 01:39
  • From what you describe, I suspect the `[[nodiscard]]` attribute, coupled with a build process that is configured (in a way that can't be changed by junior programmers) to treat warnings as errors will suffice. Also, this seems a case where active mentoring of junior members of the team by senior members is warranted. Essentially, getting junior programmers to do something necessary for the project is a non-technical problem, and a purely technical solution (forcing compilation to fail is a technical solution) to a non-technical problem is rarely a good idea. – Peter Jul 21 '21 at 01:40
  • @Eljay The callback approach is interesting. I hadn't considered it, but it conceptually could work. – daemacles Jul 21 '21 at 01:41
  • @Peter All good points. It's about layering safety. I personally like to automate checking as much as possible, because humans err, and so this is an attempt to supplement social/process checks. It might not be possible as I've described, but still an interesting question. And hopefully someday `[[cantdiscard]]` is added :) – daemacles Jul 21 '21 at 01:44
  • 1
    `nodiscard` (configured to produce an error) does exactly what you are asking for -- there's no stronger error condition than producing an error . But you reject it out of hand with no real explanation. The code in your example will produce an error where requested, if function is marked [[nodiscard]] and you configure the compiler to produce an error for this. – M.M Jul 21 '21 at 01:48
  • "a type(s) that, if it's not assigned to a variable from a function return, the compiler flags an error." is possible but no help, as the coder could just make this assignment and then not use the resulting variable – M.M Jul 21 '21 at 01:48
  • @M.M I appreciate that, and if you read the rest of the comments, your points have already been addressed. Asking if something is possible doesn't need "real explanation". Either it's possible or it's not. And if it's not, an explanation of *why* is helpful. – daemacles Jul 21 '21 at 01:52
  • If the question is clarified via comments, you should edit the question to include such clarifications . This question is like "how do I hammer in nails, but I don't want to use a hammer just because." The use case you describe is exactly why nodiscard was created . – M.M Jul 21 '21 at 01:53
  • If you're not tied to **C++**, you might find that **Rust** provides *type state* feature that may provide what you want. (I know that *type state* was removed, but there are remnants that remain and are still part of the language. Mostly used for strict ownership checked at compile time.) – Eljay Jul 21 '21 at 01:57
  • @M.M good point, I'll update it. – daemacles Jul 21 '21 at 01:57
  • You claim that "Warnings-as-Errors also doesn't work" but don't say why. What *is* the problem you have with "Warnings-as-Errors"? Is it a requirement from higher up? That it can be circumvented? But if its circumvented (like e.g. a C-style cast to `void`) isn't it then "handled"? Perhaps it actually is time for automatic static analysis on commit, together with mandatory code review? – Some programmer dude Jul 21 '21 at 05:32
  • Your use-case is an exact match for exceptions. You don't say why this is not a solution for you. Given that the Standard Library can throw exceptions I don't think a blanket ban is appropriate without the reason. – Richard Critten Jul 21 '21 at 07:37
  • "provably impossible" was changed to "probably impossible" in an edit. This sound wrong to me but somehow was accepted. Please change it back to "provably impossible" if this is indeed what you wanted to say. – Dada Jul 21 '21 at 11:53
  • @Dada I didn't accept that edit on purpose, not sure how it got in there. I did mean "if there is something known about the type system that makes this impossible please explain that aspect to me" – daemacles Jul 21 '21 at 18:41
  • 1
    @RichardCritten I understand there are "canonical" and "alternate" ways to accomplish similar results. That's not the point of my question. In a sense it doesn't matter "why" I'm asking if something is possible; I'm not looking for a solution to some general requirement, I'm specifically asking whether the C++ type system can support this very specific use-case. Either it can or it can't. And either way I completely appreciate discussion on how/why. I'm not interested in "alternative methods". I know they exist. – daemacles Jul 21 '21 at 18:44
  • 1
    "Assigned to a variable" is awfully strong. There are a lot of other meaningful ways to "use" a result. `OtherFunction(DoSomething(blah));` for instance. – Nate Eldredge Jul 21 '21 at 19:20
  • It turns out (surprisingly) that what you're asking is possible... see my updated answer – Mark VY Jul 23 '21 at 05:02

3 Answers3

4

(EDIT) Note 1: I have been thinking about your problem and reached the conclusion that the question is ill posed. It is not clear what you are looking for because of a small detail: what counts as checking? How the checkings compose and how far from the point of calling?

For example, does this count as checking? note that composition of boolean values (results) and/or other runtime variable matters.

bool b = true; // for example
auto res1 = DoSomething1(blah);
auto res2 = DoSomething2(blah);

if((res1 and res2) or b){...handle error...};

The composition with other runtime variables makes it impossible to make any guarantee at compile-time and for composition with other "results" you will have to exclude certain logical operators, like OR or XOR.


(EDIT) Note 2: I should have asked before but 1) if the handling is supposed to always abort: why not abort from the DoSomething function directly? 2) if handling does a specific action on failure, then pass it as a lambda to DoSomething (after all you are controlling what it returns, and what it takese). 3) composition of failures or propagation is the only not trivial case, and it is not well defined in your question.

Below is the original answer.


This doesn't fulfill all the (edited) requirements you have (I think they are excessive) but I think this is the only path forward really. Below my comments.

As you hinted, for doing this at runtime there are recipes online about "exploding" types (they assert/abort on destruction if they where not checked, tracked by an internal flag). Note that this doesn't use exceptions (but it is runtime and it is not that bad if you test the code often, it is after all a logical error).

For compile-time, it is more tricky, returning (for example a bool) with [[nodiscard]] is not enough because there are ways of no discarding without checking for example assigning to a (bool) variable.

I think the next layer is to active -Wunused-variable -Wunused-expression -Wunused-parameter (and treat it like an error -Werror=...). Then it is much harder to not check the bool because comparison is pretty much to only operation you can really do with a bool. (You can assign to another bool but then you will have to use that variable).

I guess that's quite enough.

There are still Machiavelian ways to mark a variable as used. For that you can invent a bool-like type (class) that is 1) [[nodiscard]] itself (classes can be marked nodiscard), 2) the only supported operation is ==(bool) or !=(bool) (maybe not even copyable) and return that from your function. (as a bonus you don't need to mark your function as [[nodiscard]] because it is automatic.)

I guess it is impossible to avoid something like (void)b; but that in itself becomes a flag. Even if you cannot avoid the absence of checking, you can force patterns that will immediately raise eyebrows at least.

You can even combine the runtime and compile time strategy. (Make CheckedBool exploding.) This will cover so many cases that you have to be happy at this point. If compiler flags don’t protect you, you will have still a backup that can be detected in unit tests (regardless of taking the error path!). (And don’t tell me now that you don’t unit test critical code.)

alfC
  • 14,261
  • 4
  • 67
  • 118
  • 2
    C++ is meant to protect against Murphy, not Machiavelli. – Eljay Jul 21 '21 at 01:29
  • 1
    @Eljay, I totally agree, that is why I pretty much would stop at "I guess that is quite enough". What comes later is just a little game (and it has a little bonus). I think there is space for a `[[nodiscard]] class CheckedBool{...};` in a generic library. – alfC Jul 21 '21 at 01:30
1

What you want is a special case of substructural types. Rust is famous for implementing a special case called "affine" types, where you can "use" something "at most once". Here, you instead want "relevant" types, where you have to use something at least once.

C++ has no official built-in support for such things. Maybe we can fake it? I thought not. In the "appendix" to this answer I include my original logic for why I thought so. Meanwhile, here's how to do it.

(Note: I have not tested any of this; I have not written any C++ in years; use at your own risk.)

First, we create a protected destructor in MustCheck. Thus, if we simply ignore the return value, we will get an error. But how do we avoid getting an error if we don't ignore the return value? Something like this. (This looks scary: don't worry, we wrap most of it in a macro.)

int main(){
    struct Temp123 : MustCheck {
        void f() {
            MustCheck* mc = this;
            *mc = DoSomething();
        }
    } res;
    res.f();
    if(!res.success) print "oops";
}

Okay, that looks horrible, but after defining a suitable macro, we get:

int main(){
    CAPTURE_RESULT(res, DoSomething());
    if(!res.success) print "oops";
}

I leave the macro as an exercise to the reader, but it should be doable. You should probably use __LINE__ or something to generate the name Temp123, but it shouldn't be too hard.

Disclaimer

Note that this is all sorts of hacky and terrible, and you likely don't want to actually use this. Using [[nodiscard]] has the advantage of allowing you to use natural return types, instead of this MustCheck thing. That means that you can create a function, and then one year later add nodiscard, and you only have to fix the callers that did the wrong thing. If you migrate to MustCheck, you have to migrate all the callers, even those that did the right thing.

Another problem with this approach is that it is unreadable without macros, but IDEs can't follow macros very well. If you really care about avoiding bugs then it really helps if your IDE and other static analyzers understand your code as well as possible.

Mark VY
  • 1,489
  • 16
  • 31
  • In general, proving anything "impossible" in C++ is hard. For instance, you might think that if you evaluate a `constexpr` function, at compile time, twice, with the exact same arguments, should alway give the same answer. Obviously, right? And then you see things like this: https://stackoverflow.com/questions/60082260/c-compile-time-counters-revisited – Mark VY Jul 23 '21 at 15:16
0

As mentioned in the comments you can use [[nodiscard]] as per:

https://learn.microsoft.com/en-us/cpp/cpp/attributes?view=msvc-160

And modify to use this warning as compile error:

https://learn.microsoft.com/en-us/cpp/preprocessor/warning?view=msvc-160

That should cover your use case.

jman
  • 685
  • 5
  • 15
  • Thanks - I updated the question to clarify that it's not enough to enable warnings-as-errors. – daemacles Jul 21 '21 at 01:25
  • 1
    link-only answers will become invalid when the link rots, and MS has changed their MSDN links so many times. See [Are answers that just contain links elsewhere really “good answers”?](https://meta.stackexchange.com/q/8231/230282) – phuclv Jul 21 '21 at 06:19