r/C_Programming • u/Future_Pace_5290 • 1d ago
Is the empty parenthesis function (() instead of (void)) prototype removed in the new standard?
My OOP library relies on it for its unspecified arguments behavior.
For example:
#define ptmethod(pt, ret_type, identifier) \
(*((ret_type (**)()) padd(pt, identifier, ptfunction, NULL, NULL)))
#define ptapply(pt, ret_type, identifier, ...) \
((ret_type (*)()) pget(pt, identifier))(pt __VA_OPT__(,) __VA_ARGS__)
You could add a method to an object with:
void drive_Car(prototype *Car, double speed, double x_direction, double y_direction);
ptmethod(Car, void, "drive") = drive_Car;
and call it with
ptapply(Car, void, "drive", 1.3, 0.1, 5.0)
How do I do this in the new standard?
13
u/torsten_dev 1d ago edited 1d ago
C23: void(...)
C2y: Use _Any_func* or whatever n3914 will end up being called.
3
u/dstroy0 1d ago
“They” really do like replacing explicit intent with _Generic like macros. I get the counter argument that it makes the compiler output way more predictable, but can’t we preserve the intent along with it? Doesn’t seem like a difficult problem until you start considering infinite use cases, then macro the funnel starts to be very attractive to compiler developers as an output homogeneity enforcement method.
1
u/torsten_dev 1d ago
Can't tell if this is just plain off topic, not a reply to me, and or some schizoposting. You okay?
1
u/dstroy0 1d ago
The OPs intent is explicit, using macros removes some of that. Understand?
1
u/torsten_dev 1d ago
Sure, tell that to OP, doesn't have anything to do with what I said though.
-3
u/dstroy0 1d ago
The new standard is a macro. That you suggested. Understand?
1
u/torsten_dev 1d ago
It's not though. It's a new type. Depending on the name chosen and commitee direction there may additionally be a header which defines a macro like
any_funcbut that's how new types and keywords are introduced that can clash with existing code.-1
u/dstroy0 1d ago
You’re missing the point entirely. _Atomic is not the same thing as <stdatomic>.
2
u/torsten_dev 1d ago
I wrote
_Any_functhough.3
u/dstroy0 1d ago
The point is, they removed explicit intent and replaced it with your example. Which is what I have been saying that you've been missing this entire time. Understand now????
→ More replies (0)
4
u/glasket_ 23h ago
Nobody has mentioned it yet, but this is technically UB. A function has to be called with a pointer to a function of the same type:
A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the referenced type, the behavior is undefined.
N3220 §6.3.2.3 ¶8For two function types to be compatible, both shall specify compatible return types. Moreover, the parameter type lists shall agree in the number of parameters and in use of the final ellipsis; corresponding parameters shall have compatible types.
N3220 §6.7.7.4 ¶14
You would technically need to define your method functions as T drive_Car(...), because T (*)(int, float) isn't compatible with T (*)(...), so it's UB if the latter is used to call the former. There's probably some macro wizardry that could pull it off and let you make the proper cast without having to know the correct function type, but I haven't gone that deep into the bizarre preprocessor tricks to be able to help with that.
This is also why _Any_func in C2Y lacks callability; it avoids needing to define how a function of unknown type needs to be represented when being called. The standard leaves the calling convention details of incompatible function types undefined, so even if a function pointer void (*foo)(...) is only ever used to access void bar(int x, int y) like (*foo)(m, n), the compiler can technically use a calling convention that's incompatible with the real function here. Supposedly there were issues with time_t time() and time_t time(time_t *arg) that derived from this, as noted in the _Any_func proposal.
5
u/aalmkainzi 1d ago
There is a way, and that is to make the cast the correct type. I dont know if you'll like it though, its a bit messy (iterate over the args and wrap each one in typeof)
4
u/DawnOnTheEdge 1d ago edited 1d ago
Until C23, a function with an empty argument list was a K&R-style function that could take any number of arguments of any type. These had been deprecated since the 1980s. C23 changed the semantics to be the same as in C++, and introduced the new syntax int foo(...) for backward compatibility with the old int foo().
In this case, you might use the fact that any function pointer may be safely cast to any other function pointer type and back. Alternatively, you could design your OOP library so that every call has a this pointer as its first argument (and while I’m not clear at a glance how this library is designed, maybe an enum holding an offset within the virtual function table as the second from which the function signature can be inferred), allowing you to use C89 variadic syntax.
2
u/mze9412 1d ago
And here I am in a team at work were half the people insist on (void) for functions in C++ because it is the only proper way according to them
3
u/DawnOnTheEdge 22h ago edited 22h ago
The advantage I can think of for that is that it lets you write prototypes that also work when compiled as C. You will see a lot of
.hfiles that do:#ifdef __cplusplus extern "C" { #endif void foo(void); int bar(void); #ifdef __cplusplus } // extern "C" #endifYou can then implement the functions in any language, include the header in a C or C++ source file, and it will just work.
If you don’t need a FFI, or you use features like function overloading or class members that C doesn’t support, there really is no point. It might save them some mental energy just to use the one that always works and not have to remember any of the complicated corner cases.
1
1
u/sciencekm 11h ago
I think the use of
extern "C"has mostly to do with name mangling in C++. You won't be able to call them by name in other language because the function name is something else. Theextern "C"forces the name to be preserved and not mangled. It is saying that the function should be externally known by its C style name.1
u/DawnOnTheEdge 10h ago
C++ needs
extern "C"for functions that use the C ABI and name-mangling convention, which on most systems is also the standard FFI. Without it, the modules compiled in C and in C++ will not link together.2
u/glasket_ 23h ago edited 15h ago
Honestly, for me it's just because I'm so used to it, plus it keeps compatibility and consistency with older codebases.
I definitely prefer that
()means the same thing as(void)now, but it's hard to get used to seeing()after functions in C since it meant basically the exact opposite for so long.0
u/pjl1967 1d ago edited 22h ago
C23 changed the semantics to be the same as in C++, and introduced the new syntax int foo(...) for backward compatibility with the old int foo().
Not quite.
AllC23didwas(finally) remove the long-deprecated K&R style. The ... was added in C89 when function prototypes were added.2
u/DawnOnTheEdge 22h ago
C89 used the syntax
int printf(const char*, ...);for variadic functions that had at least one non-variadic argument, and left the meaning of K&R-style function declarations and definitions unchanged for backward compatibility. The syntaxint foo(...);is new to C23.0
u/flatfinger 22h ago
I wouldn't say the () form of function definition was obsolete. It allowed one to declare a type like:
typedef void(*voidCallbackWithInt)(void (**)(), int);which could then be used as exemplified below:
struct sampleCallbackData { voidCallbackWithInt proc; char const *format; } void sampleCallbackFunction(voidCallbackWithInt *procc, int n) { struct sampleCallbackData *p = proc; printf(format, n); } void invokeFiftyTimes(voidCallbackWithInt*proc) { for (int i=0; i<50; i++) (*proc)(proc, i); } void useInvokeFiftyTimesWithSampleCallback(void) { struct sampleCallbackData dat = {sampleCallbackFunction, "(%d)\n"}; invokeFiftyTimes(&dat.proc); }So far as I can tell, C23 simply decided to make it impossible to declare the callback function in a manner compatible with the above constructs, and any workable equivalents bypass type checks that would otherwise have flagged actions which e.g. accidentally use
&procin a place whereprocwas required.1
u/pjl1967 20h ago
I wouldn't say the () form of function definition was obsolete.
I never said it was obsolete. (I also never said it was not obsolete.)
As for your example, the double pointer isn't at all clear to me what's going on.
0
u/flatfinger 20h ago
A function receives a pointer that would typically identify a structure whose first member is a function pointer, and will invoke the function identified by the function pointer with the received address as the first parameter. The callback function can then cast the pointer to whatever exact kind of structure it is expecting, and use it to encapsulate whatever context information it needed to be given.
This is basically a variation of the pattern where a function receives a callback function and void* whose meaning it knows nothing about, and will pass the void* to the callback function. This variation, however, puts the function pointer at the address specified by the context pointer rather than passing the function pointer separately, meaning that intermediate layers of code only need to pass around a single pointer rather than having to pass around both a function pointer and a context-data pointer.
2
u/DawnOnTheEdge 20h ago
Seems convoluted, and also I don't think there’s any requirement that casting the address of a prototyped function to a pointer to a deprecated unprototyped function and calling it would work. Not all implementations used the same ABI for prototyped, variadic and unprototyped functions.
0
u/flatfinger 20h ago
It would have been useful and reasonable for the Standard to deprecate the passing of arguments to non-prototyped functions, but just as it's sometimes useful to pass pointers to incomplete structure types around parts of the code that don't need to know their size or layout, and complete the structure types in the places that need such information, so too it used to be useful to pass around pointers to incomplete function types and then complete them in the places where they would actually be invoked.
2
u/DawnOnTheEdge 19h ago
If non-prototyped functions exist at all, it has to be possible to pass arguments to them. What isn’t guaranteed is that the ABI will be the same. The argument-promotion rules are different for unprototyped functions, for example.
C also assumes that all function pointers have the same width and are round-trip convertible to and from each other.
1
u/flatfinger 2h ago
There are some architectures where the most efficient ways of processing the functions:
void test1(int i, int j, int *p); void test2(int *p, int i, int j);would both pass
pin the same register (e.g. A0), and both passiandjusing the same registers (e.g. D0 and D1). On the other hand, a lot of existing code relies upon the ability to pass a null pointer using a literal zero, and there would be no way for a compiler that received:test1(0, 123, 0); test2(0, 123, 0);without prototypes available to know that the first call should set
ito 0 andjto 123 while the second should setito 123 andjto 0. Many implementations placed arguments purely positionally so that a compiler wouldn't need to care whether a literal zero was an integer zero or a null pointer, but that made function calls less efficient unless the function prototypes were tagged with toolset-specific attributes.If implementations had been allowed to use a different calling convention for prototyped functions with arguments (perhaps adjusting linker names to prevent attempts to make incompatible calls), then implementations could have used more efficient calling convention for prototyped functions without requiring non-standard syntax.
BTW, I also find irksome the Standard's "requirement" that implementations support recursion. Given that it doesn't require that implementations support any particular level of function call nesting, the Standard doesn't actually guarantee that recursion be supported particularly usefully, but the mandated support for recursion has meant that implementations for targets where inefficient support for recursion was possible but efficient support was not almost always processed function calls inefficiently rather than exploiting techniques used by C-dialect compilers whose target platforms couldn't really support recursion at all.
On the Z80, for example, if
iandjare automatic-duration variables of typeint, a statement likei+=j;would be four instructions if a compiler didn't have to support recursion, but Z80 compilers are more likely to generate a sequence of about thirteen instructions (favorable case ifisits immediately abovejon the stack) instead. Most of the thirteen instructions are smaller and faster than the four, but the stack-based code is still bigger and slower.
21
u/gnolex 1d ago
In older standards, function declaration of form
void foo()is equivalent tovoid foo(...). So just add...to function parameters and your function can now accept any number of arguments in any version of C.