22

It has just come to my attention that the C++ standard says that C and C++ functions have different and incompatible types, even if their type signatures are the same (for more info see this question). That means that you technically are not allowed to pass a C++ function to a C function like pthread_create().

I am curious if there are any platforms in use where the two ABIs are actually different (aside from the obvious name mangling differences). Specifically, does anyone know of any platforms where this C++ program will fail to compile and run?

#include <assert.h>

extern "C" int run(int (*f)(int), int x) { return f(x); }

int times2(int x) { return x * 2; }

int main(int argc, char *argv[]) {
  int a = times2(argc);
  // This is undefined behavior according to C++ because I am passing an
  // "extern C++" function pointer to an "extern C" function.
  int b = run(&times2, argc);
  assert(a == b);
  return a;
}
Community
  • 1
  • 1
Josh Haberman
  • 4,170
  • 1
  • 22
  • 43
  • `extern "C"` functions can take C++ types (like class instances) as arguments though right? I don't think `extern "C"` makes a function's parameters magically "non-C++". – Seth Carnegie May 17 '13 at 16:45
  • Perhaps stuff like endianness may differ for a given system between C and C++? That's the only thing I can think of right now. – Marc Claesen May 17 '13 at 16:51
  • @SethCarnegie: I think the problem is specifically that C++ says that C and C++ function pointers are different types. Since you cannot call a function through a pointer whose type is different than its declared type, I believe it follows that you cannot call an C++ function through an "extern C" pointer. I want to be wrong but there seems to be consensus about this (see the link I posted in the question). – Josh Haberman May 17 '13 at 16:56
  • 1
    @JoshHaberman well what I mean is, does the `extern "C"` on `f` apply to its arguments? the `int (*f)(int)` won't be `extern "C"` will it? In this example, it looks like you're passing a pointer-to-C++-function to a C function that takes a C++ parameter, which doesn't seem like it would present any problems. – Seth Carnegie May 17 '13 at 16:58
  • Shouldn't this have "language lawyer" tag? I believe the C++ standard ALLOW the pointer to be different, but it's by no means guaranteed that it is different. And as with all UB, it may behave exactly as you'd reasonably expect... – Mats Petersson May 17 '13 at 17:25
  • 1
    @SethCarnegie, yes, it applies to the arguments. The declaration of `run` is a function-with-C-language-linkage that takes an argument that is a pointer-to-function-with-C-language-linkage – Jonathan Wakely May 17 '13 at 18:07
  • I believe the practical issue is that, eg, `long` in C may not be the same length as `long` in C++. – Hot Licks May 17 '13 at 18:13
  • Can you make the parameter a pointer to a C++ function if you make the type a typedef outside of the `extern "C"` block? – Mark Ransom May 17 '13 at 18:41
  • 1
    @MarkRansom: yes, thats's what the standard says, though it is unclear why anyone would want that. – n. m. could be an AI May 17 '13 at 18:49
  • 1
    @HotLicks: that would be very unfortunate, as you would not be able to declare structs that are compatible with both C and C++. This would degrade usefulness of such C++ compiler to about zero. – n. m. could be an AI May 17 '13 at 18:54
  • @n.m. -- This is why there are all the `int32_t` style types defined. – Hot Licks May 17 '13 at 19:11
  • @HotLicks, no it isn't, the stdint types are more for interoperability with external programs, not for C code within the same program. How many system APIs are defined in terms of `int32_t`, rather than `int` `size_t` etc.? – Jonathan Wakely May 17 '13 at 19:28
  • 1
    @HotLicks: No they are not defined for that purpose. They are needed to make code and data portable between platforms, not between C and C++ compilers within the same platform (of course you can say your C++ compiler defines its own platform, but then again good luck finding another user for it). – n. m. could be an AI May 17 '13 at 19:37

1 Answers1

8

I don't know of any platforms where the ABI is different, but even if the C and C++ calling conventions are the same, the C++ standard requires the compiler to issue a diagnostic for the program. A pointer-to-function-with-C-language-linkage is a different type to a pointer-to-function-with-C++-language-linkage, so you should be able to overload run() like so:

extern "C" int run(int (*f)(int), int x) { return f(x); }
extern "C++" int run(int (*f)(int), int x) { return f(x); }

Now when you call run(times) it should call the second one, so it follows that the first one is not callable (there is no conversion from pointer-to-function-with-C-language-linkage to a pointer-to-function-with-C++-language-linkage) and so the original code should cause a compiler diagnostic. Most compilers get that wrong, though, e.g. http://gcc.gnu.org/bugzilla/show_bug.cgi?id=2316

N.B. The Solaris compiler does diagnose the incompatible language linkages, as a warning:

"t.c", line 11: Warning (Anachronism): Formal argument f of type extern "C" int(*)(int) in call to run(extern "C" int(*)(int), int) is being passed int(*)(int).

If you overload run with an extern "C++" function it correctly calls the extern "C++" one for run(times).

Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • Where does the Standard require that the compiler issue a diagnostic, let alone stop compilation? – Ben Voigt May 17 '13 at 18:54
  • 1
    @BenVoigt: This: "Two function types with different language linkages are distinct types even if they are otherwise identical" would imply that the call to `run` is in error. – n. m. could be an AI May 17 '13 at 19:03
  • @n.m.: The Standard explicitly avoids defining behavior for that, which means that implementations can document well-defined behavior (but are not required to do so). The types are different, yes. But the Standard does not forbid an implementation-specific implicit conversion, so long as the conversion is given worse rank than the identity conversion, so that overloading still works. – Ben Voigt May 17 '13 at 19:05
  • In summary, the code in the question is non-portable, because its behavior is not defined by the Standard. But a compiler can accept it and still be 100% conformant, AFAICT. (As [this other question](http://stackoverflow.com/questions/15536488/possible-ambiguity-with-extern-c-overloading-and-function-pointers) shows, most compilers aren't conformant, because they fail to make overloading work. But that's a different case than the acceptance of this code.) – Ben Voigt May 17 '13 at 19:07
  • @BenVoigt: Hm.. where implementation-specific implicit conversions are allowed? It would seem that the full set of implicit conversions is enumerated in clause 4 (the clause says so) and is not open to extensions. – n. m. could be an AI May 17 '13 at 19:17
  • @n.m. Clause 4 claims to enumerate the full set of "Standard conversions", obviously implementation-specific extensions are not among the "Standard" conversions. Implementations are free to give well-defined behavior to anything that the Standard leaves undefined. (The Standard does sometimes define things as ill-formed, where it wishes to forbid extensions) – Ben Voigt May 17 '13 at 19:19
  • I would argue that's not a strictly conforming extension, because the conversion is not one listed as allowed in the rules of casts/conversion, therefore the conversion should not be possible, even if it has a lower rank – Jonathan Wakely May 17 '13 at 19:21
  • @n.m.: Specifically, 1.4p8 says "A conforming implementation may have extensions (including additional library functions), provided they do not alter the behavior of any well-formed program. Implementations are required to diagnose programs that use such extensions that are ill-formed according to this International Standard. Having done so, however, they can compile and execute such programs." AFAICT according to the Standard, this is not ill-formed, it is undefined behavior. – Ben Voigt May 17 '13 at 19:21
  • [conv]/3 "An expression e can be implicitly converted to a type T **if and only** if the declaration T t=e; is well-formed". [dcl.init]/17 "**Standard conversions** (Clause 4) will be used, if necessary, to convert the initializer expression to the cv-unqualified version of the destination type; no user-defined conversions are considered. If the conversion cannot be done, the initialization is **ill-formed**." (emphasis mine). So a diagnostic (which could be just a warning, as Solaris CC does) is required – Jonathan Wakely May 17 '13 at 19:24
  • @Jonathan: Even if you're right that such a conversion is ill-formed, your first sentence is still wrong. In such a case the compiler would be required to issue a diagnostic... but it can then go ahead and compile successfully. – Ben Voigt May 17 '13 at 19:24
  • I like that much better. I'm still not convinced that when the Standard says "this specific group of conversions is considered and that specific group is not" that it precludes implementation-specific conversions which fall into neither group. Or that it prohibits an extension to the class of "function-to-pointer conversions" to handle mixed C++ and C language linkages. What no conforming extension can do, however, is treat the types as the same when the Standard clearly says they are different and usable for overloading. – Ben Voigt May 17 '13 at 20:41
  • Thanks for the info. I've also been pointed to this from a member of the committee -- this issue has been punted to the "evolution working group", who have not yet discussed it: http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_closed.html#1555 – Josh Haberman May 17 '13 at 21:26