The lack of default function parameter in C99 makes compatibility difficult.

S

Stephen Sprunk

Eric said:
Stephen said:
[...]
We could simply steal the rules from C++, where all this has already
been sorted out. [...]

While we're at it, let's steal some rules from Snobol
and Perl and RPG. Oh, and PL/I -- perhaps the outstanding
example of an "everything anybody ever thought somebody might
ever want someday" language.

In other words, if you want C++ you know where to find it.

There are plenty of things I don't like about C++, but that's no excuse
not to import the _useful_ improvements and reduce the conflict between
real C and the C-like subset of C++.

It drives me nuts that in C a "const int" cannot be a constant integer
expression, e.g. for switch case statements. It also bothers me that a
string literal is not const. Namespaces, default arguments, and even
function overloading are useful--and can be made backwards compatible.

Classes, templates, exceptions, operator overloading, etc. fundamentally
change the language and how one uses it; add those and, yes, you might
as well have C++. The stuff above, though? It'd still be C.

For that matter, I love the closures that Apple added to Objective C; I
don't like their syntax for it, but it'd be extremely useful at times.

S
 
E

Eric Sosman

Stephen said:
There are plenty of things I don't like about C++, but that's no excuse
not to import the _useful_ improvements and reduce the conflict between
real C and the C-like subset of C++.

The debate, of course, is about "useful," and different
people will have different notions of utility. My own bias
is that a Really Strong case justifies a change, but an It
Would Be Nice doesn't. I'm in sympathy with Kernighan and
Plauger's remark: "When in doubt, treat `feature' as a
pejorative."
It drives me nuts that in C a "const int" cannot be a constant integer
expression, e.g. for switch case statements.

extern const int foo;
switch (getchar()) { case foo: ... }

const int bar = rand();
switch (getchar()) { case bar: ... }

const int baz = rand() & 1;
switch (rand() & 1) {
case 0: ...
case baz: ... /* duplicate case label? or not? */
}

.... and so on; all the usual gotchas. Yes, all could be made
well-defined by adopting suitable definitions, but has anything
useful been gained?
It also bothers me that a
string literal is not const.

The practical reason for literals being unalterable but
non-const is that millyuns and millyuns of lines of existing
code would break, requiring legions and legions of programmers
to spend time on what amounts to busy-work, "fixing" already-
correct code, inevitably committing silly misteaks along the
way and thereby adding bugs. It's regrettable, yes -- but it's
a sobering example of what happens when a new feature (`const')
is added to a language that didn't always have it. See also
the unholy mess that "generics" made of Java.
Namespaces, default arguments, and even
function overloading are useful--and can be made backwards compatible.

Perhaps. "Probably," even. But there's a cost, too: Today
if I write fputc('\n') the compiler bleats, I give myself a
dope slap, and the silly mistake is fixed right away. But
when some bright lad decides that the second argument to fputc()
should default to stdout, the compiler holds its peace[*] and I
don't learn about my error until the program misbehaves. That's
not an undiluted improvement ...

[*] Yes, the compiler can always issue a warning. But if the
nice new feature provokes a warning every time it's used, it will
not be pleasant to use. Especially in shops that adopt a "No
warnings allowed" policy.[**]

[**] Yes, I know such a policy is silly, even hopeless, when
carried to the extreme. But even without going to extremes, it
would be no fun at all if half the calls to a function elicited
warnings.

Finally, let's take a look at the O.P.'s case from a software
engineering viewpoint. He suggests a function that originally
took just one argument, an int, but was later enhanced to take
a second char* argument. For the benefit of existing one-arg
callers, he decides the second argument should default to NULL
if not provided explicitly. Fine, but then what?

So programmers write calls to the new, improved foo(), and
rely on the fact that the second argument defaults to NULL if
not specified. They happily write foo(42) instead of writing
foo(42, NULL), secure in the knowledge that the defaulter will
supply the missing NULL for them ...

... and then somebody decides the second argument should
default to "" instead ...

"Nobody would ever do *that*" I hear you cry, "That's
changing the published API!" Well, the API has *already*
changed once, hasn't it? Isn't that prima facie evidence that
it's unstable, subject to further change? And doesn't that
mean that the prudent programmer would write foo(42, NULL)
anyhow, Just In Case? I betcha "Don't use default arguments"
would make it into everybody's coding guidelines right rapidly --
leaving a feature that's actively shunned by careful coders ...
 
J

jacob navia

Stephen Sprunk a écrit :
Eric said:
Stephen said:
[...]
We could simply steal the rules from C++, where all this has already
been sorted out. [...]
While we're at it, let's steal some rules from Snobol
and Perl and RPG. Oh, and PL/I -- perhaps the outstanding
example of an "everything anybody ever thought somebody might
ever want someday" language.

In other words, if you want C++ you know where to find it.

There are plenty of things I don't like about C++, but that's no excuse
not to import the _useful_ improvements and reduce the conflict between
real C and the C-like subset of C++.

Exactly

The lcc-win compiler has introduced
o operator overloading
o default arguments
o references

This extensions make C a much more usable language, and they are compatible with the
core language. I have been attacked by many people in this group because of this
extensions.
It drives me nuts that in C a "const int" cannot be a constant integer
expression,

lcc-win will do this if the const int is static
e.g. for switch case statements. It also bothers me that a
string literal is not const. Namespaces, default arguments, and even
function overloading are useful--and can be made backwards compatible.

Yes, they can be added to C.
Classes, templates, exceptions, operator overloading, etc. fundamentally
change the language and how one uses it; add those and, yes, you might
as well have C++. The stuff above, though? It'd still be C.

Agreed.

For that matter, I love the closures that Apple added to Objective C; I
don't like their syntax for it, but it'd be extremely useful at times.

S

The last point:

Can you show me an application?

I have some difficulties with them.

jacob
 
E

Eric Sosman

bartc said:
Eric Sosman said:
bartc wrote:

["The lack of default function parameter in C99 makes compatibility
difficult."]
Ahh, the blind worship of The Little Tin God persists even
to this day.

I'm sorry, what do you mean by the Little Tin God? You mean, the pursuit of
performance (which is half the reason why C is used at all)?

I in turn am sorry; I'd imagined a quick Google would turn
up the reference I intended, but instead it gets 24,400 hits,
with the screed I meant not even on the first page of results.
Please search for "The Ten Commandments for C Programmers,"
and accept my apologies for ambiguity.
 
S

Stephen Sprunk

jacob said:
Stephen Sprunk a écrit :

lcc-win will do this if the const int is static

That's reasonable, provided the value can be determined at compile-time
(e.g. to disallow Eric's case of the initializer being rand()).
Can you show me an application?

I have some difficulties with them.

The first example I saw goes something like this:

T array[N];
for(int i=0;i<N;i++) {
dosomething(&array);
dosomethingelse(&array);
}

Say you want to parallelize it with task queues, so you do this:

void loop_body(T *arg) {
dosomething(arg);
dosomethingelse(arg);
}
....
task_queue q;
T array[N];
for(int i=0;i<N;i++)
task_add(q, loop_body, &T);
task_wait(q);

That's a lot of rewriting, and it turns out that programmers simply
didn't do the latter very often because it made their functions more
difficult to read and maintain, so it wasn't worth using the feature
unless it was _known_ that N would usually be very large and would
_always_ benefit from task queues' parallelization.

but with closures:

task_queue q;
T array[N];
for(int i=0;i<N;i++) task_add(q, ^{
dosomething(&array);
dosomethingelse(&array);
});
task_wait(q);

It's a minimal and fairly unobtrusive change, yet you get all the same
benefits with none of the visual costs. Incidentally, closures making
task queues easier to use _alone_ is supposedly responsible for the vast
majority of performance improvements in OSX Snow Leopard, which my Mac
friends report is so much faster "it's like I got a brand-new machine".
How often can a little syntactic sugar have such a dramatic effect?

An example in the realm of callbacks, for those that don't admit to C
having threads in the real world:

void doButtonClicked(void *arg) {
*(int *)arg=42;
}
....
int x=0;
guiSetHandler(button, doButtonClicked, (void *)&x);

is much shorter, simpler, and less likely to have bugs with a closure:

int x=0;
guiSetHandler(button, ^{ x=42; });

In both closure examples, notice how the closure inherits the scope of
where it's defined, which eliminates the need to pass arguments. What
happens if the objects go out of scope before the closure executes,
though, is a mystery to me; I haven't gotten that far into it yet.

S
 
B

bartc

Eric Sosman said:
Stephen Sprunk wrote:

extern const int foo;
switch (getchar()) { case foo: ... }

const int bar = rand();
switch (getchar()) { case bar: ... }

const int baz = rand() & 1;
switch (rand() & 1) {
case 0: ...
case baz: ... /* duplicate case label? or not? */
}

... and so on; all the usual gotchas. Yes, all could be made
well-defined by adopting suitable definitions, but has anything
useful been gained?

What people really want is to be able to write something like:

constant int size = 42;

rather than: #define size 42

or: enum {size=42};

(or some tacky-looking macro to achieve a similar result).

Writing 'const int size=42' looks like what is needed but means something
rather different (some sort of read-only, or write-once/read-many,
variable), which would have the problems you describe.

You can't have external 'constant int's; if shared across modules, the
definition complete with it's value is repeated, or is in a shared header.

And the value associated with it must be determinable at compile-time.

Finally, let's take a look at the O.P.'s case from a software
engineering viewpoint. He suggests a function that originally
took just one argument, an int, but was later enhanced to take
a second char* argument. For the benefit of existing one-arg
callers, he decides the second argument should default to NULL
if not provided explicitly. Fine, but then what?

So programmers write calls to the new, improved foo(), and
rely on the fact that the second argument defaults to NULL if
not specified. They happily write foo(42) instead of writing
foo(42, NULL), secure in the knowledge that the defaulter will
supply the missing NULL for them ...

... and then somebody decides the second argument should
default to "" instead ...

"Nobody would ever do *that*" I hear you cry, "That's
changing the published API!" Well, the API has *already*
changed once, hasn't it? Isn't that prima facie evidence that
it's unstable, subject to further change? And doesn't that
mean that the prudent programmer would write foo(42, NULL)
anyhow, Just In Case? I betcha "Don't use default arguments"
would make it into everybody's coding guidelines right rapidly --
leaving a feature that's actively shunned by careful coders ...

No, there are two types of foo(42) calls: those written by the original
programmers who know nothing about the second parameter. In this case the
API has been *extended* in an upwards compatible manner.

The second type of foo(42) call has been coded by someone aware of the
default second parameter. Because of the existence of this new class of
code, it would be inadvisable to *modify* the API in a way that could break
existing code, unless it becomes a requirement to review all existing code.

(Otherwise there is nothing to stop the API being changed from the original
spec to foo(char*) for example which would also cause some problems.)
 
K

Keith Thompson

Eric Sosman said:
The debate, of course, is about "useful," and different
people will have different notions of utility. My own bias
is that a Really Strong case justifies a change, but an It
Would Be Nice doesn't. I'm in sympathy with Kernighan and
Plauger's remark: "When in doubt, treat `feature' as a
pejorative."

Agreed, mostly, I think.
extern const int foo;
switch (getchar()) { case foo: ... }

const int bar = rand();
switch (getchar()) { case bar: ... }

const int baz = rand() & 1;
switch (rand() & 1) {
case 0: ...
case baz: ... /* duplicate case label? or not? */
}

... and so on; all the usual gotchas. Yes, all could be made
well-defined by adopting suitable definitions, but has anything
useful been gained?

Again, I think we can just steal the C++ rules for this. I don't
have a C++ reference handy, but I think that an object declared
"const" is constant if its initializer is a constant expression
(there's probably more to it than that). All three of your examples
are constraint violations.

What we've gained is the ability to declare symbolic constants
without using the preprocessor or abusing "enum" (the latter only
supports int) -- without breaking existing code.
The practical reason for literals being unalterable but
non-const is that millyuns and millyuns of lines of existing
code would break, requiring legions and legions of programmers
to spend time on what amounts to busy-work, "fixing" already-
correct code, inevitably committing silly misteaks along the
way and thereby adding bugs. It's regrettable, yes -- but it's
a sobering example of what happens when a new feature (`const')
is added to a language that didn't always have it. See also
the unholy mess that "generics" made of Java.

If the next C standard made string literals const, compilers
would surely provide an option to use the old behavior. Existing
code could continue to be compiled with the option, or with old
compilers, and code could be fixed incrementally. Any code for
which a diagnostic is produced under the new standard needs to be
carefully examined anyway; it *might* attempt to modify a string
literal somewhere (which is why gcc's "-Wwrite-strings" option
is useful). And any new code could benefit from the extra checking.

[...]

Incidentally, exceptions (though probably not as elaborate as C++
exceptions) are on my Would Be Nice list.
 
K

Keith Thompson

jacob navia said:
Exactly

The lcc-win compiler has introduced
o operator overloading
o default arguments
o references

This extensions make C a much more usable language, and they are compatible with the
core language. I have been attacked by many people in this group because of this
extensions.
[...]

No, you have not. You have been criticized for discussing them here.
I don't recall anyone criticizing you, much less attacking you,
for implementing them. (A few might have suggested that you should
have placed a higher priority on C99 conformance than on extensions,
but that's between you and your users.)

And can you please keep your lines below 80 columns, preferably below
72? It would make your articles easier to read.
 
K

Keith Thompson

Stephen Sprunk said:
That's reasonable, provided the value can be determined at compile-time
(e.g. to disallow Eric's case of the initializer being rand()).
[...]

That's probably what jacob meant by "static". I missed that
myself on the first reading; for example, Ada uses the term "static
expression" for what C calls a "constant expression".

jacob: Is that what you meant? Or is it really tied to the storage
duration? Does lcc-win allow this?

int main(void)
{
const int c = 42;
switch (42) {
case c:
break;
}
return 0;
}
 
J

Joachim Schmitz

jacob said:
Seebs a écrit :

They can't be used together. the ellipsis and default arguments are
incompatible.

Rationale:

The default arguments must be at the end of all other arguùents.
After the first default argument all others MUST be default
arguments. Ellipsis means an underterminate number
of arguments, without any defaults, so it can't happen after
any default argument.

Since ellipsis is ALWAYS the last argument, it can't be used with
default arguments.

A compiler I'm working with seems to do it differently, the default args can
be anywhere.
You just call such a function by not providing the parameters you want the
defauls for:

foo(a, b, , , c); /* calling it wit the 1st, 2nd and 5th param, defaults for
3rd, 4th and everything after the 5th */

Of course this is an extension that has to get turned on explicitly. Don't
ask me how it's implemented...

Bye, Jojo
 
D

David Thompson

A compiler I'm working with seems to do it differently, the default args can
be anywhere.
You just call such a function by not providing the parameters you want the
defauls for:

foo(a, b, , , c); /* calling it wit the 1st, 2nd and 5th param, defaults for
3rd, 4th and everything after the 5th */

Of course this is an extension that has to get turned on explicitly. Don't
ask me how it's implemented...
Since you've mentioned elsewhere and I recognize it anyway, and I
think it's a fairly interesting example of what can be done:

The ex-Tandem exish-NonStop (classic) systems defined two similar but
nonidentical mechanisms for this, starting well before C.

The original TNS architecture, later retronymed TNS1, had a
(interlanguage) convention VARIABLE which consists of the compiler
automatically pushing a bitmask of arguments provided/omitted; the
callee can (and practically must) check for omitted arguments, and do
whatever it wants. Almost always what it does is fail or just supply a
default; that's easiest to document and explain, and there is positive
benefit in having semantics for 'omitted' the same as for some value.
(Or rather, having some value that is the same as omitted.)

When TNS1 was succeeded by the enhanced TNS2, they added another form
called EXTENSIBLE. This consists of automatically pushing a slightly
different bitmask (arg *words* rather than arg items) PLUS A COUNT.
This addresses the related issue brought up elsethread about upgrading
the callee (typically in a library) without recompiling callers/calls.
The callee prologue uses a special instruction (ESE) which checks that
the count pushed by the caller matches that expected; if less, it
automatically inserts trailing omitted args as needed. The callee then
normally defaults (all) those omitted args, just as it would for a new
caller that knew about them but chose to omit them. (If an older
callee mistakenly gets used by an upgraded caller which passes *more*,
I believe it just faults, although if *all* the surplus args are
omitted it reasonably could proceed by deleting them.)

When they then proceeded to implement C (some years after TNS2 IIRC)
of course they added _variable and _extensible to access these
features and allow full interoperability with their other languages
which supported them, particularly their proprietary SIL TAL.

More recently they dropped actual TNS* CPUs in favor of others (first
MIPS and now Itanium) and I believe they continued to emulate this
functionality in the new 'native' C's, and other languages, but can't
speak to it firsthand since I wasn't using that.
 
D

David Thompson

Again, I think we can just steal the C++ rules for this. I don't
have a C++ reference handy, but I think that an object declared
"const" is constant if its initializer is a constant expression
(there's probably more to it than that). All three of your examples
are constraint violations.
It also must be of integral (aka integer) or enumeration type. Which
is enough at least in C++ because all the places that require a CE
require an ICE. In C also the 'special' cases like case label and
(nonVLA) array bound are integer; fltpt and pointer constants/CEs are
used to initialize static-duration objects (and subobjects), but C++
doesn't require those to be CE.

Enums are explicit because in C enum types are just aliases for (some)
integers, but in C++ they are distinguishable (= overloadable) types,
which silently convert _to_ integers, but not _from_, or between.

(Also, C++ doesn't *call* it CV. In C a 'shall' requirement by itself
doesn't require diagnosis, only if it is flagged as a constraint. In
C++ any 'shall' requires diagnosis unless explicitly waived. The
substantive effect is the same: some require diagnosis, some don't.)
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
474,262
Messages
2,571,056
Members
48,769
Latest member
Clifft

Latest Threads

Top