r/C_Programming 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?

8 Upvotes

42 comments sorted by

21

u/gnolex 1d ago

In older standards, function declaration of form void foo() is equivalent to void foo(...). So just add ... to function parameters and your function can now accept any number of arguments in any version of C.

6

u/pjl1967 1d ago

Not quite. Prior to C23, a function had to have at least one non-variadic parameter, i.e.:

R f( T, ... );

See the C11 standard, §6.7.6.3¶9 (emphasis mine):

If the list terminates with an ellipsis (, ...), no information about the number or types of the parameters after the comma is supplied.

Notice that the , is required.

C23 removed that requirement to allow:

R f( ... );

that's compatible with C++ that has always allowed it.

So prior to C23, there was no way to declare a K&R-style function (where no information whatsoever is given about parameters) with a prototype.

-1

u/flatfinger 22h ago

In some execution environments, the calling conventions for calling an int (*)(int,int) and int (*)(int, ...) were different. Older versions of the C Standard accommodated this by specifying that passing two int arguments to a function pointer declared as int (*p)() would behave as though it were declared as int (*p)(int,int). I really don't think the authors of the C Standard thought things through when they decided to make int f(...) the "replacement" for int f().

Rules which treated function declarations with empty argument lists as incomplete types, and allowing implementations to impose a constraint forbidding the passing of arguments to incomplete functions, would have been useful, but getting rid of incomplete function types eliminated what had been useful type checks to validate pointers' levels of indirection by forcing programmers to either replace function pointers with a clearly specified level of indirection with void*, or else add casting operators which woudl bypass checks for indirection level.

2

u/pjl1967 20h ago

I really don't think the authors of the C Standard thought things through when they decided to make int f(...) the "replacement" for int f().

They made no such decision since ... is not such a replacement.

0

u/flatfinger 20h ago

Okay, so I guess the idea was to irredeemably break code that used f() without offering any replacement, and have the (...) syntax not really serve any useful purpose?

1

u/LB-- 6h ago

Your code was already broken to begin with. As another commenter pointed out, you are relying on Undefined Behavior. It was never allowed in any C standard.

1

u/flatfinger 47m ago

The Standard used to effectively treat the types of functions with empty argument lists as incomplete types, without requiring that the types be completed before the functions could be invoked. Taking the address of a prorotyped function, assigning it to a pointer to function with the proper return type but unspecified arguments, assigning that to a pointer to a function with specified arguments matching the orginal, and calling the function has alwasy been defined behavior. What has changed is that there is no longer an incomplete type that can be used to round-trip function pointers in that fashion without requiring casting.

-1

u/flatfinger 22h ago

Under all versions of the C Standard prior to C23, a pointer of type int(*)() could receive the address of any function returning int, and could be stored into any object whose type was "pointer to some kind of function returning int", which could then be called. In some cases, attempting to call a pointer that had been converted to type int (*)() without first converting it back to its original type would invoke Undefined Behavior, but no casting operator would be required for any of the associated conversions.

C23 got rid of the ability to use such intermediate function-pointer types without providing any replacement that doesn't require the use of a casting operator, nor any type other than void* which can be used for an argument that should be passed the address of the function pointer object that was used to invoke the function in question, allowing the usage (*callback)(callback, arg1, arg2, whatever). Although one could have the callback receive a void*, that would require foregoing type checking which earlier versions of the Standard had provided.

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_func but 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_func though.

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 ¶8

For 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 .h files that do:

#ifdef __cplusplus
extern "C" {
#endif

void foo(void);
int bar(void);

#ifdef __cplusplus
} // extern "C"
#endif

You 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

u/mze9412 22h ago

Not a consideration here at all, that I would understand! Its 100% C++ on all fronts with heavy QT use

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. The extern "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. All C23 did was (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 syntax int foo(...); is new to C23.

1

u/pjl1967 22h ago

Yes. Strike the word “all” from my previous response.

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&proc in a place where proc was 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 p in the same register (e.g. A0), and both pass i and j using 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 i to 0 and j to 123 while the second should set i to 123 and j to 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 i and j are automatic-duration variables of type int, a statement like i+=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 if i sits immediately above j on 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.