would C be easier to read if...

H

Hallvard B Furuseth

Ian said:
No, all the typedef dose is introduce an alias, it doesn't remove the
need to cast an inappropriate type.

Indeed. But I was talking about making such code readable, which
is what this thread is about in the first place. Like you said
in your next message in this thread:)
If you do need to clutter the code with unreadable casts, you are
doing something smelly and should reconsider what you are doing.

Likely yes. Not always.
 
O

Owen Jacobson

That means it's a pointer to a function.  The full type is
"pointer to function taking a void * argument and returning void
*".

OK thanks.
That's not a type or a declaration.  It's a cast.  In a cast,
there is no variable to name, so "a" is omitted.  If there was a
variable there, it would be in the same position.

I agree that the type-declaration scheme /is/ logical when you analyse it,
it's just difficult to read! Being adept at it does not change that.

While we're in this fantasy thread, I knocked up this new left-to-right
easy-to-read syntax for C type declarations. Type-decls are constructed with
the following symbols (enums and some other stuff left out):

*               Pointer to
type            Type name (built-in or typedef-ed)
function        Optional function indicator
(...)T          Function Takes these params, returns type T
[]              Array of
[N]             Array of N of
struct{...}     Struct of
struct S{...}   Struct S of

These are written left to right, example:

*int a,b;                 a,b are both pointers to int
function(int,int)float    Function taking two int params and returning float
result
(int,int)float            Same without the 'function'
[10]char s,t              s,t are both 10-char arrays
[10]*char list            Array of pointers to char (array of strings)
*function(*void)*void     Above example
*(*void)*void             Above example without 'function'

Pros:
* Easy to read, translates naturally to English
* Variables sharing same array/pointer properties share the one type-decl(as
in above examples)
* The typedef T U statement, in practice may have T and U hopelessly
intermingled for complex type-decls. With this scheme T and U stay separate.
* Also type-decls appear in typedefs, var-decls, casts, and function params
all have the same form

Cons:
* Can't mix declaration of variables that have different array/pointer
attributes (but I think that's a bad idea anyway).

Here's the thing: that's not any more or less readable than C's
syntax, just different. In order to really change the readability,
you'd have to go back to the type system itself and look at it.

C's type system admits the following[1] as types:

void (the type that has no values.)
char, short, int, long, long long, and unsigned/signed variants
(ordinal types.)
float and double
struct {T1, T2, ...} (structure/named tuple types.)
pointers to any function signature.
('a' is a type -> 'a (*) (T1, T2, ...)' is a type)
pointers to any value type
('a' is a type -> 'a *' is a type)
constants of any type
('a' is a type -> 'a const' is a type)

It also halfways admits to the existence of arrays; each pair
(unsigned number, type) is a distinct array type, but you can't pass
values of array types around directly.

Functions themselves, however, are not directly represented in C's
type system. This makes sense; C is not a functional language and you
cannot create functions in it at runtime.

The treatment of void is somewhat inconsistent: it's a type with
exactly zero values. All other types have at least one value.
Consequently, you can declare pointers to void, but not have
expressions or variables whose type is void.

I posit that any syntax that expresses the C type system will be
"messy" the same way C's syntax is: the mess is in the (pragmatic and
very useful) type system itself. If you want a language with a
cleaner syntax for expressing types, choose a language with a cleaner
type system.

-o-

[1] Not intended to be exhaustive.
 
V

vippstar

That is not true, and it's not about void *'s "magic". You can also
assign a const char * to a char * and still have a conforming
program.

Only because "conforming program" is a rather loose term. A
conforming program can use non-portable features.


What do you mean by "perfectly conforming"? If you mean the same as
"strictly conforming", I disagree. Here is the code again:

#include <stdio.h>
int foo(void *);
int main(void) {
const char * const p = "hello world";
foo(p);
return 0;}

int foo(void *p) { return puts(p); }
I'm not up to digging through the standard at the moment, but if you
compile it with "gcc -std=c99 -pedantic-errors" and bugreport the
resulting error message, maybe they'll tell you where to find it:)
gcc has idiotic and inaccurate warning messages [1] (which btw you
turn into diagnostic errors with -pedantic-errors)
My program is 100% conforming to C99 (and perhaps C89/POSIX too).

Again, what is 100% conforming? It violates a constraint and thus
requires a diagnostic.

6.5.2.2 Function calls
Constraints
...
2 If the expression that denotes the called function has a type that
includes a prototype, the number of arguments shall agree with the
number of parameters. Each argument shall have a type such that its
value may be assigned to an object with the unqualified version of
the type of its corresponding parameter.

I.e. the argument, p, must have a type such that its value may be
assigned to an object of type void *. The correctness of the call is
defined in terms of the correctness of:

const char * const arg = "hello world";
void *param = arg;

So we got to:

6.5.16.1 Simple assignment
Constraints
- both operands are pointers to qualified or unqualified versions of
compatible types, and the type pointed to by the left has all the
qualifiers of the type pointed to by the right;
Oh, I'm such a fool.
My apologies to Hallvard B Furuseth..

(just when you think you finally know C... :)
<snip>
 
J

John Bode

I've got more serious problems with understanding C declarations, for
example:

What on earth does (void *(*)(void*)) mean? It's some sort of cast, so the
type is:

void *(*)(void*)

My guess is it's a function returning type void*, and maybe taking a single
parameter of void*, but what about the (*) in the middle?!

That indicates that it's a pointer to a function.

A cast expression is basically a declaration without the identifier;
for example, when you see

(void *(*)(void*))ThreadProc

substitute

void *(*ThreadProc)(void*);

which reads as

ThreadProc -- ThreadProc
*ThreadProc -- is a pointer
(*ThreadProc)() -- to a function
(*ThreadProc)(void *) -- taking a void* parameter
*(*ThreadProc)(void *) -- and returning a pointer
void *(*ThreadProc)(void *) -- to void
And * does apparently seem to change position. Unless I've got these wrong:

int *a a is pointer to int (* on left)
(int *) pointer to int (* on right)
*a dereference pointer to int (* on left again).

In my case a type declaration that reads linearly from left to right would
help tremendously, because that's what I'm familiar with. Just having the
word 'function' in a function declaration would make things so much clearer!

Maybe C's syntax will get easier with use, I don't know, but since I stay
well clear of anything complicated, probably not.

C's declaration syntax follows a "declaration mimics use" paradigm;
IOW, an object or function's declaration should look as much like how
it's referenced in the executable code as possible. This is probably
best explained with some examples.

Let's say we have a regular int identifier named "foo". Here's how it
would be referenced in the code:

foo = blah();
bar = foo;
printf("%d\n", foo);

So, the declaration of "foo" is pretty simple:

int foo;

Now let's pretend "foo" is a pointer to int. Using the same examples
as above:

*foo = blah();
bar = *foo;
printf("%d\n", *foo);

In each case above, we use the dereference operator "*" to get to the
integer value pointed to by foo. So, our declaration of foo is:

int *foo;

Now let's pretend "foo" is an array of pointers to int:

*foo = blah();
bar = *foo;
printf("%d\n", *foo);

And the declaration:

int *foo[N];

In the declarations above, "foo", "*foo", and "*foo[N]" are all called
declarators; the declarator introduces the name of the thing being
declared, and provides additional type information. In the
declaration

int *foo[N];

the int-ness of foo is provided by the type specifier "int", while the
pointer-ness and array-ness are provided by the "*" and "[]" in the
declarator. Note that the "*" and "[]" operators are bound to the
identifier, not the type specifier. Cast expressions seem to violate
this rule, but they really don't; again, think of the cast as a
declaration without an identifier, and mentally substitute the thing
being cast for where the identifier would go.

A few more examples:

int (*foo)[N]; // foo is a pointer to an array of int
...
(*foo) = blah();
bar = (*foo);
printf("%d\n", (*foo));

int (*foo)(void); // foo is a pointer to a function returning int
...
blah = (*foo)(); // can also be written simply as foo();
printf("%d\n", (*foo)());

int (*foo[N])(void); // foo is an N-element array of pointers to
// functions returning int
blah = (*foo)();
printf("%d\n", (*foo)());

When you see a particularly hairy declaration, like

char *(*(*(*foo)[N])(double bar))[M];

start by finding the leftmost identifier, and then work your way out,
remembering that [] and () bind before * (i.e., *x[] is an array of
pointer, (*x)[] is a pointer to an array, *x() is a function returning
a pointer, and (*x)() is a pointer to a function):

foo -- foo
*foo -- is a pointer
(*foo)[N] -- to an N-element array
*(*foo)[N] -- of pointers
(*(*foo)[N])() -- to functions
(*(*foo)[N])(double bar) -- taking a double
parameter
*(*(*foo)[N])(double bar) -- returning a pointer
(*(*(*foo)[N])(double bar))[M]; -- to an M-element array
*(*(*(*foo)[N])(double bar))[M]; -- of pointers
char *(*(*(*foo)[N])(double bar))[M]; -- to char

Alternately, you could go the other way, and work from the outside
in. Start by substituting everything between the outermost () with X:

char *X[M];

X is an M-element array of pointer to char.

Now we can start decomposing X:

X = (*Y)
char *(*Y)[M];

Y is a pointer to an M-element array of pointer to char

Y = Z(double bar)
char *(*Z(double bar))[M];

Z is a function returning a pointer to an M-element array of pointer
to char

Z = (*G)
char *(*(*G)(double bar))[M]

G is a pointer to a function returning a pointer to an M-element array
of pointer to char

G = H[N]
char *(*(*H[N])(double bar))[M]

H is an array of pointers to functions returning a pointer to an M-
element array of pointer to char

H = (*foo)
char *(*(*(*(*foo)[N])(double bar))[M]

foo is a pointer to an N-element array of pointers to functions
returning a pointer to an M-element array of pointer to char.
 
B

Ben Bacarisse

Owen Jacobson said:
Functions themselves, however, are not directly represented in C's
type system.

This is not the case.
This makes sense; C is not a functional language and you
cannot create functions in it at runtime.

This is true, and it accounts for some of the limitations of function
types, but function types themselves *are* permitted in the type
system:

typedef int operation(int, int);

makes 'operation' a synonym for a function type. You can't use it
define any functions of this type (mainly, I assume, because you would
loose the parameter names), but you can declare functions using
it:

operation add, subtract, multiply, divide;

and you can define derived types from it:

typedef operation *bin_op_ptr;
operation *semantics[N_OPS];

It is about as good a type as C permits.
I posit that any syntax that expresses the C type system will be
"messy" the same way C's syntax is: the mess is in the (pragmatic and
very useful) type system itself. If you want a language with a
cleaner syntax for expressing types, choose a language with a cleaner
type system.

The "mess" comes from using operator syntax that requires parentheses
for two different purposes to represent some types. E.g.

int *f(int); /* compare */ int (*f)(int);

Add to that the fact that ()s enclose a type when it is used as a cast
(or in a sizeof expression) and that the name can be omitted when it
is no needed and you get thing like (int (*)(int)) even for simple
types.

I've grown oddly fond of the syntax. I did not like it at first but
it has grown on me.
 
B

Bartc

Owen said:
Ben Pfaff said:
What on earth does (void *(*)(void*)) mean? It's some sort of
That means it's a pointer to a function. The full type is
"pointer to function taking a void * argument and returning void
*".
I agree that the type-declaration scheme /is/ logical when you
analyse it, it's just difficult to read! Being adept at it does not
change that.

While we're in this fantasy thread, I knocked up this new
left-to-right easy-to-read syntax for C type declarations.
Type-decls are constructed with the following symbols...

* Pointer to
type Type name (built-in or typedef-ed)
function Optional function indicator
(...)T Function Takes these params, returns type T
[] Array of
[N] Array of N of
struct{...} Struct of
struct S{...} Struct S of

These are written left to right, examples ...
*function(*void)*void Above example
*(*void)*void Above example without 'function'
Here's the thing: that's not any more or less readable than C's
syntax, just different.

Maybe, but it's certainly simpler. Expressed in English, type-declarations
tend to written linearly, from left to right, the same as my syntax.

C's syntax tends to start in the middle and work outwords, jumping from side
to side. And sometimes there's a name embedded in there, sometimes not. And
sometimes extra attributes like array of/pointer to are just added to
variable names. It's rather messy.
In order to really change the readability,
you'd have to go back to the type system itself and look at it. .....
I posit that any syntax that expresses the C type system will be
"messy" the same way C's syntax is: the mess is in the (pragmatic and
very useful) type system itself. If you want a language with a
cleaner syntax for expressing types, choose a language with a cleaner
type system.

There's nothing much wrong with the idea of the C type system; for this
level of language, it's fine.

My variation chose to use the same symbols but to rearrange them. They could
be modified further. Taking the original example, changing void* to int*, it
would be:

int *(*)(int*) Original C, but this changes in typedefs and named
declarations

*(*int)*int My variation without 'function'...
*function(*int)*int ... and with

"Pointer to Function taking Pointer to Int parameter and returning Pointer
to Int"
In English

ref function (ref int) => ref int
Algol-68-like with functional "=>" symbol for good
measure

Given that such a type is desired, there are only so many ways to express
it. And out of these, C chose the most convoluted and unintuitive. That is
unfortunate.
 
H

Hallvard B Furuseth

Bartc said:
C's syntax tends to start in the middle and work outwords, jumping
from side to side. And sometimes there's a name embedded in there,
sometimes not. And sometimes extra attributes like array of/pointer
to are just added to variable names. It's rather messy.

That's why I'm saying "typedef".
Given that such a type is desired, there are only so many ways to
express it. And out of these, C chose the most convoluted and
unintuitive. That is unfortunate.

Maybe. But if you change the declaration syntax that doesn't change the
expression syntax it reflects, so you still have to learn to dig through
that complex syntax. With a complicated but different declaration
syntax there would be one more syntax to learn.

The trick is to learn to read a declaration as in an expression: "If
this was an expression, how would it be evaluated?". That mostly
answers what the declaration is saying.

Similarly, a trick for reading casts like (int (*)(void *)): If the
contents of the outer () were a declaration, where is the syntax error?
I.e. where can you insert a variable name to make it a valid
declaration? Imagine a variable name there, then read it as a
declaration, and you know the type. (In this case, after the first *.)
 
K

Keith Thompson

Kenneth Brody said:
Aside from what the others said, consider this:

char *p;

The use of "*" is consistent, IMO, because:

"p" is of type "char *", just like the definition says, and
"*p" is of type "char", just like the definition says.

Be careful. Your second statement, that *p is of type char, is
correct and consistent with the syntax of the declaration. The first,
that p is of type char*, is also correct, but it's not really what the
declaration *means*, at least not directly.

Consider:

char *p, q;

This says that *p is of type char (implying that p is of type char*),
but q is of type char, so p and q are of different types.

There's a school of thought that says that, in a pointer declaration,
the "*" should be adjacent to the type:

char* p;

and the declaration should be read as "p is of type char*". The above
problem is then avoided by declaring only one object per line:

char* p;
char q;

Personally, I prefer to have the code layout reflect the syntax. As a
programmer, you have to understand the syntax anyway, so I see no
point in concealing it with odd spacing.

Kenneth, I don't mean to imply that you don't understand this; my
remarks are mostly just a matter of emphasis.
 

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
473,769
Messages
2,569,578
Members
45,052
Latest member
LucyCarper

Latest Threads

Top