2

The behavior of my code is different in c and c++.

void *(*funcPtr)() = dlsym(some symbol..) ; // (1) works in c but not c++

int (*funcPtr)();
*(void**)(&funcPtr) = dlsym(some symbol..) ; // (2) works in c++

I dont understand how the 2nd casting works in c++ while the 1st casting doesnt work in c++. The error message for (1) show is invalid conversion from void* to void*() in c++.

White Philosophy
  • 173
  • 2
  • 11
  • 3
    Because `C` and `C++` are different languages, maybe? – Sourav Ghosh Apr 05 '19 at 06:40
  • sorry for the second one, i forget to include the initialisation of funcPtr - int (*funcPtr)(); – White Philosophy Apr 05 '19 at 06:45
  • 1
    C is not C++. There are many things you can do in `C` that are compiler errors in `C++`. – PaulMcKenzie Apr 05 '19 at 06:50
  • @Lundin There is a cast of the variable to "pointer to pointer to void" in the C++ snippet. – Martin Bonner supports Monica Apr 05 '19 at 07:05
  • Sorry about that @LorinczyZsigmond – White Philosophy Apr 05 '19 at 07:08
  • Do you need it to work in c++? If so surely it should be contained in an `extern c` scope? – Fantastic Mr Fox Apr 05 '19 at 07:10
  • In C `void*` type is like "variant" and is silently converted to any target pointer type. In c++ implicit casting from `void*` is disabled and casting have to be done explicitly. – Marek R Apr 05 '19 at 07:15
  • Well, the first one still gives warning in C -- in C++ that warning became error. The type-cast between code-pointer and data-pointer might be problematic as ISO-standard says it might be unsafe (aka Undefined Behaviour). But, on platforms that have `dlsym` (or `GetProcAddress`) it _is_ safe, because they mandate that code-pointers and data-pointers have the same size (wich is equal to the size of `intptr_t`) – Lorinczy Zsigmond Apr 05 '19 at 07:22
  • @Aconcagua No it isn't implementation-defined, the language simply doesn't say what will happen, so it is UB. The C standard non-normative Annex J.5.7 does list it as a common form of non-standard compiler extensions though. – Lundin Apr 05 '19 at 07:57

2 Answers2

4

The problem is that dlsym returns a void* pointer. While in C, any such pointer is implicitly convertible into any other (object!) pointer type (for comparison: casting the result of malloc), this is not the case in C++ (here you need to cast the result of malloc).

For function pointers, though, this cast is not implicit even in C. Apparently, as your code compiles, your compiler added this implicit cast for function pointers, too, as a compiler extension (in consistency with object pointers); however, for being fully standard compliant, it actually should issue a diagnostic, e. g. a compiler warning, mandated by C17 6.5.4.3:

Conversions that involve pointers, other than where permitted by the constraints of 6.5.16.1, shall be specified by means of an explicit cast.

But now instead of casting the target pointer, you rather should cast the result of dlsym into the appropriate function pointer:

int (*funcPtr)() = reinterpret_cast<int(*)()>(dlsym(some symbol..));

or simpler:

auto funcPtr = reinterpret_cast<int(*)()>(dlsym(some symbol..));

or even:

int (*funcPtr)() = reinterpret_cast<decltype(funcPtr)>(dlsym(some symbol..));

(Last one especially interesting if funcPtr has been declared previously.)

Additionally, you should prefer C++ over C style casts, see my variants above. You get more precise control over what type of cast actually occurs.

Side note: Have you noticed that the two function pointers declared in your question differ in return type (void* vs. int)? Additionally, a function not accepting any arguments in C needs to be declared as void f(void); accordingly, function pointers: void*(*f)(void). C++ allows usage of void for compatibility, while skipping void in C has totally different meaning: The function could accept anything, you need to know from elsewhere (documentation) how many arguments of which type actually can be passed to.

Aconcagua
  • 24,880
  • 4
  • 34
  • 59
  • I noticed that they are different. I didn't realise that C++ doesn't implicitly convert void* into any other pointer type. So i reckon i didnt handle that correctly in C. – White Philosophy Apr 05 '19 at 07:21
  • This is not correct. You cannot implicitly convert between a function pointer and `void*` in C. Implicit conversions only happens for pointers to object type. – Lundin Apr 05 '19 at 07:23
  • Well, you need to clarify that the first example does _not_ compile in C, contrary to what the OP claims. – Lundin Apr 05 '19 at 07:45
  • 1
    @Lundin GCC 8.1 (Linux) accepted nicely `void(*f)(void) = malloc(sizeof(void*)); f();` without even issuing a warning... – Aconcagua Apr 05 '19 at 07:51
  • 1
    @Aconcagua Yes, it is a non-comforming compiler when using default settings. See my answer. A conforming compiler must give a diagnostic message for this code. – Lundin Apr 05 '19 at 07:53
  • @Aconcagua Then if in the case of the funcPtr having arguments, would i need to typecast the types as well? i.e. int (funcPtr)(int,int,int,int) = reinterpret_cast(dlsym(some symbol..)); – White Philosophy Apr 05 '19 at 08:08
  • @MichaelGoh Yes, you would. Signature (including return type) is part of the type of a function pointer, so if parameters differ, you get different types. `void(*)()`, `int(*)()`, `void(*)(int)` are all off different type just like `int*` and `double*` are as well. – Aconcagua Apr 05 '19 at 08:49
  • @Lundin C4.6 *'[...] A conforming implementation may have extensions [...], provided they do not alter the behavior of any strictly conforming program.'* I do not see how the *extension* of accepting omitted cast would alter behaviour of a strictly conforming programme *having* the cast. So how is GCC not conforming in this respect? (Apart from that a diagnostics is *explicitly* mandated somewhere, but I didn't find – if so, could you give a reference?). – Aconcagua Apr 05 '19 at 09:30
  • @Aconcagua 5.1.1.3 "A conforming implementation shall produce at least one diagnostic message if a preprocessing translation unit or translation unit contains a violation of any syntax rule or constraint". And as demonstrated in my answer, an assignment from a function pointer to an object pointer is a constraint violation. The implementation is required to produce a diagnostic message or it is not conforming. – Lundin Apr 05 '19 at 12:57
  • I gave the tick to you because your explanation gave various variants of solutions – White Philosophy Apr 07 '19 at 23:57
  • @Aconcagua Obviously things that are extensions to the standard are not specified by the standard. In this case however, the code _violates_ the language standard, meaning that a diagnostic is required. – Lundin Apr 08 '19 at 08:27
  • @Lundin One step back (deleted previous comments) – found it myself, the implicit cast in question violates 6.5.4.3... Thanks for enlightenment. – Aconcagua Apr 08 '19 at 08:58
  • @Lundin OK, then if I see the matter right: VLA in C++ would require a diagnostic, too, as violating 11.3.4 `[constant-expression(opt)]`, GCC's designated initialisers for ranges (`[10 ... 12] = 7,`) or `##` for empty varargs would not. The latter two extend syntax, though. Not totally sure about extensions that would make UB well defined (e. g. accepting signed integer overflow in compile time expressions – hypothetically...). Would we need a diagnostics or not? – Aconcagua Apr 08 '19 at 09:03
  • @Aconcagua Casts are always explicit, conversions may be implicit, a cast enforces an explicit conversion. 6.5.4/3 points at 6.5.16.1 (simple assignment) which I already mentioned in my answer. You can of course make an explicit cast of either operand to make it conform to simple assignment - even though that doesn't guarantee that the conversion is valid or that the pointer types are compatible. – Lundin Apr 08 '19 at 10:55
  • @Lundin Well aware of all this, should have better payed attention on my inexact wording... Your answer is fine, but as far as I read the standard, from 6.5.16.1 alone a violation of a constraint wouldn't arise, that comes only together with 6.5.4.3 – unless we already violate a constraint by solely making some UB well defined (see my question above: diagnostic or not?). – Aconcagua Apr 08 '19 at 14:13
  • It is a constraint violation of 6.5.16.1 alone. Simple assignment allows various cases of pointer assignment: from pointers to compatible types where the left operand has all the qualifiers of the right one (not applicable here), from null pointer constants (not applicable here) and to/from pointer to **object** type to/from pointer to void, where they share qualifiers (applicable here). – Lundin Apr 08 '19 at 15:09
  • @Lundin It doesn't tell anything about non-object-pointers at all. OK, UB so far for these. Agree? But now, is not meeting *any* kind of UB a constraint per se (thus requiring a diagnostic)? If so, I can agree with you, if not, I still think we need 6.5.4.3 as well, which is then the constraint violated. Or I fundamentally misunderstand something... – Aconcagua Apr 08 '19 at 15:50
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/191466/discussion-between-aconcagua-and-lundin). – Aconcagua Apr 08 '19 at 17:07
3

Strictly speaking, none of this code is valid in either language.


void *(*funcPtr)() = dlsym(some symbol..) ;

dlsym returns type void* so this is not valid in either language. The C standard only allows for implicit conversions between void* and pointers to object type, see C17 6.3.2.3:

A pointer to void may be converted to or from a pointer to any object type. A pointer to any object type may be converted to a pointer to void and back again; the result shall compare equal to the original pointer.

Specifically, your code is a language violation of one of the rules of simple assignment, C17 6.5.16.1, emphasis mine:

  • the left operand has atomic, qualified, or unqualified pointer type, and (considering the type the left operand would have after lvalue conversion) one operand is a pointer to an object type, and the other is a pointer to a qualified or unqualified version of void, and the type pointed to by the left has all the qualifiers of the type pointed to by the right;

The reason why it might compile on certain compilers is overly lax compiler settings. If you are for example using gcc, you need to compile with gcc -std=c17 -pedantic-errors to get a language compliant compiler, otherwise it defaults to the non-standard "gnu11" language.


int (*funcPtr)();
*(void**)(&funcPtr) = dlsym(some symbol..) ; // (2) works in c++

Here you explicitly force the function pointer to become type void**. The cast is fine syntax-wise, but this may or may not be a valid pointer conversion on the specific compiler. Again, conversions between object pointers and function pointers are not supported by the C or C++ standard, but you are relying on non-standard extensions. Formally this code invokes undefined behavior in both C and C++.

In practice, lots of compilers have well-defined behavior here, because most systems have the same representation of object pointers and function pointers.

Given that the conversion is valid, you can of course de-reference a void** to get a void* and then assign another void* to it.

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • I'm pretty sure it's not **Undefined Behavior** in C++17. I recall the discussion in WG21 around 2002 or so, where `dlsym` was the explicit reason to add _conditionally-supported_. Importantly, implementations who do not support it must give a diagnostic. – MSalters Apr 05 '19 at 12:25
  • @MSalters Regardless, you cannot implicitly convert from a void* to a function pointer. It should have been written as `funcPtr = (int(*)()) dlsym(...`. Pretending that a function pointer in the application is a void pointer, then access it as such, is always wrong. – Lundin Apr 05 '19 at 13:02