Hacker News new | ask | show | jobs
by rom1v 614 days ago
> This construct works perfectly fine in C

Intuitively, I would say that this is actually undefined behavior (it would probably be difficult to expose a wrong behavior in practice though).

In C specs, I found 6.5.2.2, paragraph 9:

> If the function is defined with a type that is not compatible with the type (of the expression) pointed to by the expression that denotes the called function, the behavior is undefined.

We might discuss whether

    void (*)(char *)
is "compatible" with

    void (*)(void *)
but I think it isn't, since:

    void target(void *ptr) {}
    void (*name)(char *ptr) = target;
fails to compile with the error message:

    initialization of ‘void (*)(void *)’ from incompatible pointer type ‘void (*)(char *)’
The compiler explicitly says "incompatible pointer type".

Same for:

    void target(char *ptr) {}
    void (*name)(void *ptr) = target;
4 comments

> it would probably be difficult to expose a wrong behavior in practice though

Emscripten breaks if you cast function pointers: https://emscripten.org/docs/porting/guidelines/function_poin...

Good point, interesting that I never ran into this issue in real world code bases so far (and I build a lot of code with Emscripten) :)
It's worse than that. This guy takes a void* function and casts it to a char* function, then passes it a char**.

    void (*name)(char *ptr);
    typedef void (*name_func)(char *ptr);

    void target(void *ptr)
    {
       printf("Input %p\n", ptr);
    }


    char *data = "string";
    name = (name_func)target; // Illegal: casting fn that takes void* to a fn that takes char*
    name(&data); // Illegal: passing a char** into a function that takes char*
Before someone mentions qsort(): the comparator function really is supposed to take a void*, and inside the function, you re-cast the void* argument to a pointer type of your desire. If you don't do it in that order, you're using it wrong.
Ironically, in K&R, they did exactly this for casting comparator functions for their own version of qsort.

  /* declarations */
  void qsort(void *lineptr[], int left, int right,
             int (*comp)(void *, void *));
  int numcmp(char *, char *);

  /* the offending line */
  qsort((void **) lineptr, 0, nlines-1, 
    (int (*)(void*,void*)(numeric ? numcmp : strcmp));
The C standard mandates that a “pointer to void shall have the same representation and alignment requirements as a pointer to a character type”. In consequence, the same holds for an array of void pointers vs. an array of char pointers. The code therefore seems valid to me, and furthermore at no point is a function called with an argument type different from its formal parameters.

In the GP example, a char** is passed where a char* is expected. That is clearly invalid.

I was more referring to how, in the K&R example, a function of type "int (*)(char *, char *)" is cast to "int (*)(void *, void *)", in contradiction to what they said here:

> Before someone mentions qsort(): the comparator function really is supposed to take a void*, and inside the function, you re-cast the void* argument to a pointer type of your desire. If you don't do it in that order, you're using it wrong.

In contrast, the K&R example is an example of explicitly undefined behavior in the C standard:

The behavior is undefined in the following circumstances:

- A pointer is used to call a function whose type is not compatible with the pointed-to type (6.3.2.3).

> the K&R example is an example of explicitly undefined behavior in the C standard

You can’t blame K&R C for that. It predates “the C standard” by over a decade (1978 vs 1989)

When it was written, what K&R said was C.

The second edition was released in 1988, and it was based on a draft of the first ANSI C standard, and even then, the line stating that this was undefined behavior was already present.

http://jfxpt.com/library/c89-draft.html#A.6.2

> A pointer to a function is converted to point to a function of a different type and used to call a function of a type not compatible with the original type (3.3.4).

The example is still the same in the second edition of K&R, which is based on C89.
Ah, I missed that. However, footnote 41 states “The same representation and alignment requirements are meant to imply interchangeability as arguments to functions […]”, which could be taken as implying compatibility of function types, though the normative wording doesn’t directly support that.
Good point. It would be nice if the C standard were slightly clearer here.
> name(&data); // Illegal: passing a char* into a function that takes char*

I assume this is just a typo in the article. He probably meant `name(data)`.

In C, all pointers are compatible to a void pointer without casting though (e.g. assigning a char pointer to a void pointer or the other way around is completely legal - I think that was the main reason why void pointers were added in the first place - it's basically an 'any pointer'). It's only C++ where this is an error (which is weird on its own, why even allow void pointers in C++ when the only reason they (presumably) exist doesn't work anymore).

The above code still doesn't compile in C because C complains about the function signatures being incompatible, even though the only difference is the argument type which itself is 'equivalent' in C - which I guess could be interepreted as an edge case in the C spec? (IMHO the function prototypes should actually be compatible).

"Compatibility" has a specific meaning in C which isn't the same as what you're describing here. What you're talking about is implicit conversions without narrowing.

Compatibility is essentially about ABI: https://en.cppreference.com/w/c/language/type#Compatible_typ...

The C standard is quite loose about pointer requirements. Conversions between data pointers or between function pointers are allowed, but conversions between each other are not guaranteed. You can read the nitty gritty here: https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf

POSIX defines stricter requirements for pointers than the C standard. All pointers are compatible, so they have the same size and representation, and conversions between function pointers and void* are explicitly allowed.

Not all pointers. Function pointers, though often used that way, should not be cast to void. ISO C forbids it. (Implementations may allow it.)
ISO C forbids it, but, FWIW, POSIX requires it. So pick your standard I guess.
you can convert a short to an int without casting and it will roundtrip without loss of data; similarly you can roundtrip any data pointer through a void pointer without loss of data. It doesn't mean that an int has the same representation of a short or a void* has (necessarily) the same representation as any other pointer.

In C++ any pointer is also implicitly convertible to a void *, it is the reverse implicit conversion that is prohibited as it is not safe in the general case.

For consistency C++ should also prohibit implicit narrowing conversions (e.g. int to short ); I guess this was thought to break too much existing code and it is generally safer than a pointer conversion (although potentially lossy the behavior is fully defined). Many compilers will warn about it, and narrowing conversions are now prohibited in braced initialization.

Re implicit narrowing conversions, I think most compilers have warnings for this by now both in C and C++, at least in the higher warning levels.
Yes. Simply put, void pointers and struct pointers (the actual breaking example in Curl) are not guaranteed to have the same byte-level representation, by the C standard. (For example, a struct pointer may always be aligned and therefore use fewer bits than a void pointer.) Passing one to a function that expects the other may result in arbitrary breakage.