c++visual-studioc++11macrosc++03

Emulating lambdas in C++03 for flow-control purposes in macros


I have some existing code in a header file which needs to be useable in the context of C++03 & C++11

It defines a macro TABORT that takes in a printf-style format string & arguments to be formatted using that string, prints the result to stdout, and then calls std::abort

Essentially, something similar to

#define TABORT(...) do { fprintf(stderr, "Aborting at %s:%d for reason: ", __FILE__, __LINE__); fprintf(stderr, __VA_ARGS__); std::abort(); } while(0)

I wanted to add some logic to catch the case where evaluating __VA_ARGS__ would throw an exception, preventing the call to std::abort. For example, in the following:

if (SomethingReallyBadHappened())
    TABORT("Aborting because %s", GetReallyBadDetails().c_str());

if GetReallyBadDetails throws an exception, I want to ensure that abort gets called (here, not after some exception unwinding).

So, I did something like:

#define TABORT(...) do { fprintf(stderr, "Aborting at %s:%d for reason: ", __FILE__, __LINE__); try { fprintf(stderr, __VA_ARGS__); } catch (...) { fprintf(stderr, "<Failed to evaluate abort message>\n"); } std::abort(); } while(0)

But this is causing the C4714 warning in visual studio when the macro is used in functions marked __forceinline, probably due to

In some cases, the compiler will not inline a particular function for mechanical reasons. For example, the compiler will not inline:

  • A function with a try (C++ exception handling) statement.

So, to avoid that warning (& to keep inlining functions which were previously determined to need inlining inlined [I hope this was done alongside profiling...]), I was thinking to do something like

// In header

// Assume this is like std::function except construction from a lambda can't throw
template <typename Sig>
class function_ref;
using PrintAbortMsg = function_ref<void(const char*)>;
void [[noreturn]] do_tabort(const char* file, int line, function_ref<void(PrintAbortMsg &)> msgGenerator) noexcept;
#define TABORT(...) do_tabort(__FILE__, __LINE__, [&](PrintAbortMsg& print) { print(SprintfToStdString(__VA_ARGS__).c_str()); })

// In cpp

void do_tabort(const char* file, int line, function_ref<void(PrintAbortMsg&)> msgGenerator) noexcept
{
    fprintf(stderr, "Aborting at %s:%d for reason: ", __FILE__, __LINE__);
    try
    {
        msgGenerator([](const char* msg) { fprintf(stderr, "%s\n", msg); });
    }
    catch (...)
    {
        fprintf(stderr, "<Failed to evaluate abort message>\n");
    }
    std::abort();
}

Which I think would work in the context of C++11, but I'm not sure how to do something which will work for for C++03. There are existing usages of the macro which I don't want to touch.


Solution

  • You are working too hard. Just write a class that calls abort() in its destructor. Create an instance of that class right after the opening brace in the macro. It will be destroyed and call abort at the closing brace, whether the block is exited normally or via exception. As in

    struct AbortWhenDestroyed {
      ~AbortWhenDestroyed() { abort(); }
    };
    
    #define TABORT(...) do { \
      AbortWhenDestroyed abort_me; \
      fprintf(stderr, "Aborting at %s:%d for reason: ", __FILE__, __LINE__); \
      fprintf(stderr, __VA_ARGS__); \
    } while(0)
    

    The search term for further research is "scope guard". See also: std::experimental::scope_exit


    You can also make this class print <Failed to evaluate abort message>, as in your try/catch example. Along these lines:

    struct AbortWhenDestroyed {
      ~AbortWhenDestroyed() {
          if (!normal_exit) {
            fprintf(stderr, "<Failed to evaluate abort message>\n");
          }
          abort();
      }
      bool normal_exit = false;
    };
    
    #define TABORT(...) do { \
      AbortWhenDestroyed abort_me; \
      fprintf(stderr, "Aborting at %s:%d for reason: ", __FILE__, __LINE__); \
      fprintf(stderr, __VA_ARGS__); \
      abort_me.normal_exit = true;
    } while(0)
    

    If evaluating __VA_ARGS__ throws an exception, then the line abort_me.normal_exit = true; won't be executed, and the destructor would emit the warning before aborting.