3

TL;DR version

How to achieve exception-free C++ assignment-expression in throwing statement without sacrificing code readability and maintainability?


Yadi Yada version

Goal and current implementation

Recently I'm trying to figure out a way to avoid throwing exception in the assignment-expression part of C++ throw statement. But I cannot find a way to accomplish my goal without sacrificing readability and ease of maintainability.

To be more precise, I'm trying to conform to MISRA-C++ 2008 Rule 15-1-1 (page 157).

Before the issue came to my notice from the static program analysis tool used in the company, my throwing statement would often looks something like this

int someValue;
if (/*something is wrong*/) {
    throw MyExceptionClass(string("Blah some message [").
                                  append(to_string(somevalue)).
                                  append("]"));
}

Basically I would dump the relevant context out into a standard string, pass the said string into constructor of the soon-to-be-thrown exception then throws it.

Problem with the original implementation

The problem is either my custom to-string function (e.g., wrapper for inet_ntop) or the std::to_string function may throw exceptions (in this case, std::bad_alloc). What happens next is the exception thrown in the assignment-expression would be propagated, instead of the original-to-be-thrown exception. Since the original exception was never constructed successfully, effectively shadowing the original exception.

Some alternatives I can think of and their problem

In order to abide by the rule, I think I would have to completely steer away from dynamic allocation. That leaves me with

  1. Purely static error output string
if (/*something is wrong*/) {
    throw MyExceptionClass("Something is wrong");
}
  1. Locally statically allocated buffer, keep track of proper buffer index myself, if the error message exceed the pre-defined buffer size, the message would have to be truncated.
int someValue;
if (/*something is wrong*/) {
    char buf[ERROR_MESSAGE_SIZE] = {0};
    size_t bufIndex = 0;
    int ret = snprintf(buf, sizeof(buf), "Something is wrong, somevalue: [%d]", someValue);
    if (ret < 0) {
        /* Deal with the sprintf error here, at this point probably would just abort */
        /* Note that throwing stuff here is also effectively equivalent of shadowing the original exception */
    } else if (ret >= sizeof(buf)) {
        /* The message is written, but truncated, not sure how to handle that (if meaningful context is dropped) */
    }
    throw MyExceptionClass(buf);
}
Problems with solution 1

The problem with solution 1 is there is not sufficient context given in the message. If someone is calling my function / library but insisting in sending illegal argument, and at the same time ignore returned error code / no proper handling of any exception (more often than not these things will happen in the same piece of garbage code). It would take some extra time in figuring out what the current state inside my own code.

As a side note here, I wrap all my throwing statement with a layer of extra macro I wrote (omitted in the given example) using the compiler provided __FILE__, __LINE__ and __func__ macro. So in this case I'm not actually worried about figuring out where the actual exception is thrown.

Problems with solution 2

Basically means in all my exception-handling part of the code I would have to use pure C language. The code would bloat really quickly, and not inline with the coding style of the rest of the code, making it hard to track and maintain. Plus after a while someone would probably try to come up with a common helper function and fuck up again.

Some feature I thought may comes in handy but doesn't seem to be the case

  1. std::string_view
  2. std::to_char

Originally I thought these two relatively new feature in C++ could help me a bit (haven't actually use them before to be honest). But after some thought and looking at the document, these two still requires a pre-allocated buffer to work on (an std::string on the frist one, and a char buffer on the second. Plus I still have to check for error code return in the to_char case.

Looking for alternatives

I wish know if there are other options that can abide by the rule without significantly sacrifice the readability and maintainability. Or maybe I should stand proud and strong behind my current implementation arguing it's currently at a acceptable balance between maintainability and robustness, then happily click the ignore button on the static code analysis tool?

A random question that popped up in my head while I was writting this question

Also, come to think of it, how does std::throw_with_nested deal with the problem? I would imagine having to construct a new exception would take up some more memory space, does that mean std::throw_with_nested may throw some other stuff in the process? (The cpp reference API does not have noexcept on the function itself)

Thanks for any suggestion / feedback in advance, cheers.

user2535650
  • 265
  • 1
  • 2
  • 8
  • 1
    Use a (constexpr) string type with fixed maxmimum memory size. The main problem you are facing is that your strings can use dynamic memory allocation during your exception construction and that should be avoided. – Pepijn Kramer Oct 09 '22 at 08:24
  • So basically the first alternative in the question I mentioned I guess. But not being able to dump relevant value in the current stack still frienghtens me to be honest. – user2535650 Oct 09 '22 at 08:25
  • 1
    You may be able to do the message construction in the catch. Sure, you may need another try/catch, but it would be an option. Alternatively you could pass a functor for determining the error message to the exception class and have the constructor deal with exceptions: `templateMyExceptionClass(F&& f, Args&&...args) noexcept { try{ m_message = std:::forward(f)(std::forward(args)...);} catch(...) { /* .... */ } }` `throw MyExceptionClass([someValue]() { return string("Blah some message [").append(to_string(somevalue)).append("]"); });` – fabian Oct 09 '22 at 08:46
  • Option 1, can do memory allocation again – Pepijn Kramer Oct 09 '22 at 08:47
  • @PepijnKramer if the system is already stringent on the memory, it would still pop a `bad_alloc` exception on this case right? Then I would still have to deal with / suppress the exception in the end – user2535650 Oct 09 '22 at 09:52
  • Throwing an exception when handling an exception will terminate your program. So that's bad when your software is controlling something. (I assume that is the case since you're using MISRA) – Pepijn Kramer Oct 09 '22 at 10:11
  • @PepijnKramer I'm afraid you have misunderstand the problem, in this case it will not cause termination, since the original exception is never successfully constructed. So to C++, the actual stack unwinding process haven't started yet. – user2535650 Oct 09 '22 at 10:22
  • @user2535650 Yes you are right :) – Pepijn Kramer Oct 09 '22 at 10:25
  • @fabian I have not think of the functor approach, this seems to be able to keep the extra try-catch clause away from the main execution flow. Combine this with the fallback static message below, I might be able to come up with a reasonable solution. Thank you for the suggestion. – user2535650 Oct 09 '22 at 11:36
  • You shouldn't be using exceptions in the kind of applications that require MISRA. Nor std::string or anything else with implicit heap allocation. It's incredibly hard to write safe C++, I wouldn't even recommend the attempt. – Lundin Oct 10 '22 at 06:42
  • @Lundin Well, since MISRA actually included an exception section and rule I would suppose they at least think it is feasible in some way? And I'm sort of in the mood of find that way right now. As for the question whether the code I'm writing need the level of safety from MISRA, well, some people just like to tick all the standard in the static code analysis tool (and decree that all issue needs to be fix, but funny enough ignoring them is fine), and another group of people give schedule that produce garbage code constantly. So I would say this is beyond my control. – user2535650 Oct 10 '22 at 17:42
  • What I'm trying to do is figuring out whether I can have the benefit of using exception and at the same time conform to the rule. Just trying get some self-improvement to savage the situation you can say. – user2535650 Oct 10 '22 at 17:44
  • I have no high opinions of MISRA C++ or C++ in general. Anyone who has worked with it extensively and got some common sense can easily tell that C++ is _way_ too complex to even consider for mission-critical systems. For example it literally contains _thousands_ of cases with explicitly pooly-defined behavior. It's hard enough to write good C++ code for a regular non-critcial microcontroller application. And then there's language recommendations from big picture standards like IEC 61508 and I don't think "C++ with safe subset" is listed even after the release of MISRA-C++:2008. – Lundin Oct 10 '22 at 19:15
  • While the arguments _for_ using C++ for such systems are usually among the lines of "hey why not" or "my pointy-haired boss insisted on it"... – Lundin Oct 10 '22 at 19:17
  • In my mind the system we're currently working on probably is closer to desktop environment. It is in no way a mission-critical system, the worst that would happen is user lost their photos or some other stuff stored in the disk. So what the pointy-haired boss did instead is apply MISRA-C++ to all the codebase, while in the meantime asking to fix the issue "in-place" (with no overhaul to the system). – user2535650 Oct 12 '22 at 03:18
  • I'm quite curious on the value of applying standards such as MISRA onto non mission critical system (not used as a guideline, as mandatory requirement). Does that just make developer's life harder since it requires them to handle a lot of issue that's not expected to be handled in this level, or does it force a good habit in some way? – user2535650 Oct 12 '22 at 03:21

1 Answers1

1
int someValue;
if (/*something is wrong*/) {
    auto ex = MyExceptionClass("Blah some message");
    try {
        ex = MyExceptionClass(string("Blah some message [").
                                      append(to_string(somevalue)).
                                      append("]"));
    }
    catch (...)
    {
    }
    throw ex;
}

Generally, you want your MyExceptionClass to contain a char const* to a static description string (literal), and an auxiliary data element (of type e.g. std::string) that supplements or replaces the static description string. You swallow exceptions populating the auxiliary data, so that if this process fails you still have the static description string to fall back on.

ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • Obviously `auto` will be banned from all safety-related software, since it needs explicit type safety and deterministic behavior. Similarly, exception handling will be banned too. And heap allocation. – Lundin Oct 10 '22 at 06:44
  • @Lundin you may not have noticed, but this question is about using exceptions and memory allocation in MISRA-compliant software. – ecatmur Oct 10 '22 at 08:43
  • I'm talking about safety-related software, which is not necessarily the same thing as MISRA-C++ compliance. C++11 and beyond _are_ however explicitly banned from MISRA-C++:2008. A new version "MISRA-C++:202X" is in the making and is going through reviews, but so far it is not released. – Lundin Oct 10 '22 at 09:02
  • @Lundin can you provide the reference on c++11 being explicitly banned from MISRA C++ 2008? I just skimmed through the PDF and try to Google with a few keyword, but can seems to find anything. Thank you. – user2535650 Oct 10 '22 at 17:55
  • @user2535650 Well perhaps not explicitly but banned since it isn't c++03. "Rule 1–0–1 (Required) All code shall conform to ISO/IEC 14882:2003 “The C++ Standard Incorporating Technical Corrigendum 1”". – Lundin Oct 10 '22 at 19:07
  • Ahh, I see. So basically applying MISRA C++ 2008 to anything beyond C++03 is stepping into an undefined territory. – user2535650 Oct 12 '22 at 02:43