5

I'm trying to investigate the difference in generated code when I switch Visual Studio 2019 from /EHsc (Structured and C++ exceptions only) to /EHs (also do not assume that extern "C" functions won't throw – ref), but I can't seem to coax VS into providing a useful testcase when I've minimised it.

I'm surprised the following doesn't do it (the assembly in both cases is identical), since the contents of the function referred to by fptr (a possible definition being included in comments for exposition) are unknown to the optimiser.

void foo();

/*void foo()
{
    throw 0;
}*/

extern "C"
void bar(void (*fptr)())
{
    fptr();
}

int main()
{
    try
    {
        bar(&foo);
    }
    catch (...) {}
}

Granted, it knows that any hypothetical exception will be immediately caught, and since with /EHsc the result of this exception propagation is "undefined" per Microsoft, it "appearing to work" is of course a valid outcome. But that's not much help to me here!

So how can I perform this experiment, without introducing different translation units? Ideally I want to be able to come up with a Compiler Explorer snippet for this.

My goal is to prove that permitting extern "C" to throw (or, rather, propagate) C++ exceptions in a well-defined manner does not have a higher runtime cost than I'm willing to accept in trade.

Yes, I am aware of general advice not to let exceptions cross module boundaries or flow through third-party C code. Yes, I am doing that anyway. Yes, that's fine in our project!

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
  • All of the above is C++ code. `extern "C"` does not turn something into C code. It's still C++ code, but with C linkage, and can do anything that C++ code normally does (including proper exception handling). Need to declare something as `extern "C"`, then compile it as a different `.C` module. – Sam Varshavchik Nov 25 '19 at 12:02
  • @SamVarshavchik I know it's C++ code. But the documentation states that this flag turns on and off the ability for `extern "C"` functions to deal with exceptions. It specifically states that this is not the case (read: not guaranteed, "undefined") with `/EHsc` (contrary to your claim). In my real project the `extern "C"` function is indeed in a different project in a file whose extension is `.c`, but I don't think that should matter? Am I missing something? – Lightness Races in Orbit Nov 25 '19 at 12:03
  • I would of course accept an answer that says VS 2019 happens to generate identical code regardless unless the containing file is built as C (even though this is not required by the contract laid out in the documentation) – Lightness Races in Orbit Nov 25 '19 at 12:05
  • Will calling a `WinAPI` function do the trick? (These are all `extern "C"` IIRC.) – Adrian Mole Nov 25 '19 at 12:06
  • @Adrian-ReinstateMonica Interesting idea - unless it's inlined though there'll be no way to examine the definition of said function with the two different switches :( – Lightness Races in Orbit Nov 25 '19 at 12:07
  • I was thinking rather of putting the `WinAPI` call inside a `try...catch` block in a test function that you **do not** declare as `extern "C"`. – Adrian Mole Nov 25 '19 at 12:11
  • @Adrian-ReinstateMonica Hmm still probably doesn't tell me whether any extra machinery is built in to the `extern "C"` function with the switch changed. Though if that's done at the callsite instead somehow then I'd find that out. Alas, [Compiler Explorer suddenly not playing ball!](https://godbolt.org/z/PmfYts) – Lightness Races in Orbit Nov 25 '19 at 12:12
  • @LightnessRaceswithMonica, try putting a call to your extern "C" function in a nothrow function and see if the codegen change. – AProgrammer Nov 25 '19 at 12:24
  • Try constructing an auto object in the "extern C" function. A class that logs the invocation of its constructor and destructor, and does nothing more. I suspect that if exception handling is turned off, the destructor won't get called. – Sam Varshavchik Nov 25 '19 at 12:49

2 Answers2

0

The following MCVE may give you some ideas for a test framework. You could easily add more 'thorough' test code, but this will compile and run. Note that, with the /EHsc option set, this warning is generated:

warning C5039: 'FuncTst': pointer or reference to potentially throwing function passed to extern C function under -EHc. Undefined behavior may occur if this function throws an exception.

However, when using /EHs the warning goes away - which suggests at least the possibility of different code generation.

Here's the suggested code:

#pragma warning(disable:4514) // These two lines de-fluff the hundreds of other
#pragma warning(disable:4710) // warning generated when /Wall is used.

#include <iostream>

using pFunc = int(__stdcall*)(int);

extern int __stdcall FuncOne(int p) {
    int answer = 0;
    for (int i = 0; i < p; ++i) answer += i / p; // Possible divide-by-zero
    return answer;
}

extern "C" int __stdcall FuncTst(int i, pFunc fnc) noexcept // Expects noexcept but "FuncOne" can throw!
{
    return fnc(i);
}

int main()
{
    int q;
    std::cout << "Enter test number: ";
    std::cin >> q;
    int z = FuncTst(q, FuncOne);
    std::cout << "Result = " << z << std::endl;
    return 0;
}

Hope it helps! Feel free to offer critique and/or ask for any 'improvements' or explanation.

Note: Although you can't specifically enable the C5039 warning when you compile with (say) \W4, you can force it to flag as an error (though this may be a bit harsh), with:

#pragma warning(error:5039)
Adrian Mole
  • 49,934
  • 160
  • 51
  • 83
  • Man, C5039 would have been great to get. I don't know why our codebase wasn't triggering it; I'm 98% certain the bug I've been chasing is due to this whole thing. – Lightness Races in Orbit Nov 25 '19 at 13:09
  • 1
    Divide by zero won't throw a C++ exception though? – Lightness Races in Orbit Nov 25 '19 at 13:09
  • Once godbolt's Windows support comes back online I'll give this a go :) – Lightness Races in Orbit Nov 25 '19 at 13:10
  • Ah! You're right on the exception, but I'm sure you can come up with a code body that will - depending on the value of the given argument! (Feel free to edit it into the given answer.) – Adrian Mole Nov 25 '19 at 13:11
  • @Lightness PS: As you probably know, you need `/Wall` to get any level "5" warnings. – Adrian Mole Nov 25 '19 at 13:12
  • Ah so it's a level 5. We don't use `/Wall` because [it's crap on VS](https://stackoverflow.com/a/45586238/560648) – Lightness Races in Orbit Nov 25 '19 at 13:13
  • @Lightness - See the two #pragma lines I've added! I have a "pch.h" type of file I use for all projects that disables all the fluff before including the system headers, then re-enables (most of) them afterwards - the "push-pop" doesn't seem to work for level 5. – Adrian Mole Nov 25 '19 at 13:16
  • Too bad there's no "opt-in" equivalent to `/wd` :( – Lightness Races in Orbit Nov 25 '19 at 13:19
  • My goal was to demonstrate the cost, i.e. by looking at the assembly (or some version of it) and this doesn't quite let me do that (if nothing else, bringing IOStreams into it makes the result mahoooosive) but it's probably a good way of observing the difference at runtime! – Lightness Races in Orbit Nov 27 '19 at 15:04
  • I had to work around [a Compiler Explorer thing](https://stackoverflow.com/q/59050839/560648), by providing `/EHc-` in my build flags (to "undo" the `/EHsc` that CE sets), but [I'm still not having any luck seeing the `catch` elided](https://godbolt.org/z/G5-xQZ), which is just weird. Currently I reckon maybe whole-program-optimisation (`/GL`) has to be turned on to trigger it, but then Compiler Explorer doesn't give any results, so I might just give up. – Lightness Races in Orbit Nov 27 '19 at 15:06
  • If I could make VS give me nice output like CE does, I'd just do it from `cl`. As it is, I was able to prove what I wanted to prove, using VS's "native" assembly output. But a shareable, readable CE link would have been nicer! Alas. – Lightness Races in Orbit Nov 27 '19 at 15:07
  • @LightnessRaceswithMonica I can produce no difference in ASM output between `/EHsc` and `/EHs`, in either the `throw` or `catch` statements. Interestingly, MSVC translates `throw 0` into a call to `_CxxThrowException` - which is itself defined as `extern "C"...` I'm confused. – Adrian Mole Nov 27 '19 at 15:51
0

This seems a sort of bug on Compiler Explorer, that forces /EHc even if /EHs is explicitly set.

Try to compile this code on some MSVC version with /EHs:

extern "C" void foo() {
    throw 1;
}

static_assert(noexcept(foo()), "");

Surprisingly, the function foo is assumed noexcept, and the compiler output states the reason:

example.cpp (2): warning C4297: 'foo': function assumed not to throw an exception but does (2): note: The function is extern "C" and /EHc was specified

Here you can find this test. You may also see this by adding something like throw 1; in your example.

Nevertheless, you can fix it compiling with /EHc- /EHs (also replace /O3 with /O2, as it is not supported on MSVC), but the output is still the same.

You can find your example, fixed, here. In the /EHc- /EHs case, the output states

cl : Command line warning D9025 : overriding '/EHc' with '/EHc-'

and a static assertion will state that bar is actually not assumed noexcept.

Giovanni Cerretani
  • 1,693
  • 1
  • 16
  • 30