Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to force a compile error in C++(17) if a function return value isn't checked? Ideally through the type system

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 ;)

like image 847
daemacles Avatar asked Dec 06 '25 07:12

daemacles


2 Answers

(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.)

like image 141
alfC Avatar answered Dec 08 '25 19:12

alfC


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.

like image 31
Mark VY Avatar answered Dec 08 '25 21:12

Mark VY



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!