Is it possible to make void * safer?

C

Clint Olsen

I was just thinking about the virtues of C vs. C++ wrt. ADT/generic
programming. The biggest complaint about writing container libraries for
ADTs is that void * offers no type safety. Does it really have to be this
way?

Couldn't you for instance track an object's accesses with void pointers and
ensure they are used consistently across calls?

---------

typedef struct {
size_t count;
/* ... */
void *data;
} array_t;

int array_push(array_t *array, void *data);
void *array_pop(array_t *array);

---------

Based on these two interface functions, we can on an individual object of
type array_t track that the parameter passed to push() has the same type as
the one to which you would assign from the pop() function.

Of course, for functions that take multiple void arguments or use void
arguments in inconsistent ways, we would need to track how those arguments
are used and are eventually assigned/read to/from the members of type
array_t.

This would appear to work in theory for code in which you have all the
source files available. It falls apart however when you only have a header
file as a specification for a pre-compiled library. I suppose since the
parameter identifiers are optional in function prototypes that you might be
able to divine something if the programmer elects to use some sort of
predictable naming for the void arguments.

At any rate, it was just a random thought. My idea is full of holes but it
was at least in the spirit of C :)

Thanks,

-Clint
 
S

SM Ryan

# I was just thinking about the virtues of C vs. C++ wrt. ADT/generic
# programming. The biggest complaint about writing container libraries for
# ADTs is that void * offers no type safety. Does it really have to be this
# way?

Then don't use C. There are other languages that are plenty
type safe. You can even write your front end to convert
a more sophisticated type checking language into C, or hire
people like me to write a front end for you.
 
W

websnarf

Clint said:
I was just thinking about the virtues of C vs. C++ wrt. ADT/generic
programming. The biggest complaint about writing container libraries for
ADTs is that void * offers no type safety. Does it really have to be this
way?

Its not just void *, but <anything> * that has little to no type
safety. That's C's *real* problem w.r.t. safety. If you want to avoid
void *, its not a problem, but avoiding pointers in general -- you
won't get too far in C trying that.
Couldn't you for instance track an object's accesses with void pointers and
ensure they are used consistently across calls?

---------

typedef struct {
size_t count;
/* ... */
void *data;
} array_t;

int array_push(array_t *array, void *data);
void *array_pop(array_t *array);

---------

Well actually the way to do this is as follows:

struct tagarray {
size_t count;
/* ... */
void * data;
};

extern int untyped_array_push (struct tagarray *array, void *data);
extern void *untyped_array_pop (struct tagarray *array);

Then you write the following wrappers:

struct <name>_tagarrwrap {
struct tagarray generic;
}

int <name>_array_push (struct <name>_tagarrwrap *array, <type> * data)
{
return untyped_array_push (&array->generic, (void *) data);
}

<type> * <name>_array_pop (struct <name>_tagarrwrap *array) {
return (<type> *) untyped_array_pop (&array->generic);
}

The point is that these wrappers are so syntactically trivial, that you
can invoke them as a result of some (name, type) indexed macro. Then
you just use these wrapper functions.

Now if C were any good of a language, this would be *perfect*. You
could even hide the struct tagarray definition inside a generic module
(its definition is unnecessary except for the generic void * function
implementations), this would be about as safe as you get -- *EXCEPT* C
is so worthless ...

No matter what <type> you pick, you won't get any type safety. For
example, if we've set up an int array:

genericArrayImpl (foo, int); /* Let's say this is our macro */
struct foo_tagarrwrap arrInt;
float y = 2.0; /* This is not an int */

foo_array_push (arrInt, &y);

The compiler won't even give you a warning here. You don't actually
*have* anything resembling typesafety in C. You need to use a C++
compiler before you are informed of any kind of an error here.

So we've avoided void*, but we haven't actually gained any typesafety,
unless we require the use of a C++ compiler. But in that case, why not
simply use the C++ language capabilities to do this right via
templates?
 
B

Ben Hinkle

genericArrayImpl (foo, int); /* Let's say this is our macro */
struct foo_tagarrwrap arrInt;
float y = 2.0; /* This is not an int */

foo_array_push (arrInt, &y);

The compiler won't even give you a warning here. You don't actually
*have* anything resembling typesafety in C. You need to use a C++
compiler before you are informed of any kind of an error here.

Huh? It is illegal to implicitly convert from float* to int*, if that's what
you are hinting at. See sections 6.5.16.1 of the C99 spec, for example. The
OP posted that void* is the gateway to implicitly throwing away type safety
and he's right.
 
C

Clint Olsen

So we've avoided void*, but we haven't actually gained any typesafety,
unless we require the use of a C++ compiler. But in that case, why not
simply use the C++ language capabilities to do this right via templates?

Thanks for your post.

The problem I have with C++ and other template-style programming is that
the functions are instantiated for every unique type they use. It sort of
reminds me of the old days of C++ that where it was primarily a language
front-end and just blurted out C. Yeah, it's type safe, but it seems like
we could make smarter compilers that used the _same_ exact code to do the
same work. I don't know enough about generic programming support in other
languages to compare C++ against, but it seems they are sort of doing an
end-around approach to get type safety. After all, it's just damn bytes
we're talking about in the end.

Thanks,

-Clint
 
M

Mark McIntyre

Its not just void *, but <anything> * that has little to no type
safety. That's C's *real* problem w.r.t. safety.

Well sure, C isn't particularly type safe. Since when is this news? If
you want C++ you know where to find it.
Now if C were any good of a language,

Here we go again.
No matter what <type> you pick, you won't get any type safety. For
example, if we've set up an int array:

genericArrayImpl (foo, int); /* Let's say this is our macro */
struct foo_tagarrwrap arrInt;
float y = 2.0; /* This is not an int */

foo_array_push (arrInt, &y);

The compiler won't even give you a warning here.

If you've avoided void*, then the compiler will warn you about the
above, since int* and float* are incompatible, as are float* and a
pointer-to-struct.

You don't actually know C that well do you?


Mark McIntyre
 
R

Rod Pemberton

Clint Olsen said:
I was just thinking about the virtues of C vs. C++ wrt. ADT/generic
programming. The biggest complaint about writing container libraries for
ADTs is that void * offers no type safety. Does it really have to be this
way?

Couldn't you for instance track an object's accesses with void pointers and
ensure they are used consistently across calls?

From the other posts, it sounds like you may need C to be completely safe.
There are a number of programs which will help you make your C programs much
safer, such as LINT, CCURED, or CIL. Or, you could switch to a different
language such as Lustre and then run SCADE which was used sucessfully with
EADS Airbus.

Rod Pemberton
 
I

Ian Collins

Clint said:
Thanks for your post.

The problem I have with C++ and other template-style programming is that
the functions are instantiated for every unique type they use. It sort of
reminds me of the old days of C++ that where it was primarily a language
front-end and just blurted out C. Yeah, it's type safe, but it seems like
we could make smarter compilers that used the _same_ exact code to do the
same work.

Well you can do this with a container like you describe by using void*
for the internals, with a template wrapper that imposes the type safety.
With care, the wrapper member functions are trivial and will be
inlined away.

You probably could simulate this is C with a struct comprising function
pointers defined within a macros, something like:

#define X( type ) \
typedef struct xX##type { \
void (*push)( array_t*, type ); \
} X##type; \
void push##type##( array_t* array, type data ) { \
array_push( array, (void*)data); }

#define GET_X( type, name ) \
X##type name; \
name.push = push##type;

X(int)

int main()
{
GET_X( int, x );
array_t array;
x.push( &array, 42 );
}
 
W

websnarf

Ian said:
Well you can do this with a container like you describe by using void*
for the internals, with a template wrapper that imposes the type safety.
With care, the wrapper member functions are trivial and will be
inlined away.

You probably could simulate this is C with a struct comprising function
pointers defined within a macros, something like:

#define X( type ) \
typedef struct xX##type { \
void (*push)( array_t*, type ); \
} X##type; \
void push##type##( array_t* array, type data ) { \
array_push( array, (void*)data); }

Well this means that the data is getting implicitely copied into from
the call site to the local parameter. The cast to (void *) looks
wrong, I think you mean: (void *)&data, but you're still getting 1
shallow copy (suppose said:
#define GET_X( type, name ) \
X##type name; \
name.push = push##type;

I'm not sure what you're gaining from this function pointer abstraction
since you are requiring, specifically "push##type" to exist in file
scope.

I think my way works best in practice, *except* for the fact that when
you build many of them for various types, the lack of type safety so
easily leads to utter chaos. Unless you run it through a C++ compiler,
but then its just ironic.
 
I

Ian Collins

Well this means that the data is getting implicitely copied into from
the call site to the local parameter. The cast to (void *) looks
wrong, I think you mean: (void *)&data, but you're still getting 1
shallow copy (suppose <type> is a struct.)
Actually, it should have been 'type* data )' in the function definition,
I was assuming type was a struct.
I'm not sure what you're gaining from this function pointer abstraction
since you are requiring, specifically "push##type" to exist in file
scope.
The ability to create an instance of the object, as illustrated by the
example push in my original post.

I was specifically demonstrating how to mimic template in C with macros.
I think my way works best in practice, *except* for the fact that when
you build many of them for various types, the lack of type safety so
easily leads to utter chaos. Unless you run it through a C++ compiler,
but then its just ironic.
That's why I added the function pointers, they are type safe.
 
W

websnarf

Ian said:
Actually, it should have been 'type* data )' in the function definition,
I was assuming type was a struct.


The ability to create an instance of the object, as illustrated by the
example push in my original post.

I was specifically demonstrating how to mimic template in C with macros.

Well, you are also using function pointers. This is a performance
concession, especially if you allow it to have an external interface --
then the compiler can't be "smart", and you eat the indirect function
call cost.
That's why I added the function pointers, they are type safe.

Uhhm ... not on the C compilers I use. In fact, I encourage a related
function pointer abuse in Bstrlib (though, I don't require it -- you
can be to the letter if you want; and they are cases where no real
system is actually going to go wrong) for some cases, and it doesn't
come up as an issue.
From what I can tell, pointers are basically never checked by the
compilers -- they take the attitude that all pointers are the same (or
that the programmer knows what s/he's doing), and this includes
function pointer parameters. You have to actually dereference before
you get an error. Obviously you can't get away with this in C++.

I'll check this all again when I get access to a compiler again (I am
not near that kind of a system right now) but I'm pretty sure
contemporary modern C compilers all just screw you on this issue. (The
question is -- is this a "NULLStone" compliance issue, or what? -- Why
do *ALL* the C compilers screw you on this?)
 
I

Ian Collins

Well, you are also using function pointers. This is a performance
concession, especially if you allow it to have an external interface --
then the compiler can't be "smart", and you eat the indirect function
call cost.
Agreed, that's one big hit when simulating templates over using C++
templates. C++ compiles will inline the trivial wrapper functions.
Uhhm ... not on the C compilers I use. In fact, I encourage a related
function pointer abuse in Bstrlib (though, I don't require it -- you
can be to the letter if you want; and they are cases where no real
system is actually going to go wrong) for some cases, and it doesn't
come up as an issue.
Yes it will complain, based on my earlier macros:

#define X( type ) \
typedef struct xX##type { \
void (*push)( array_t*, type ); \
} X##type; \
\
void push##type##( array_t* array, type data ) { \
array_push( array, data); }

#define GET_X( type, name ) \
X##type name; \
name.push = push##type;

typedef int* intp;

X(intp)

int main()
{
GET_X( intp, x );

array_t array;
int n = 0;
float f = 0.0;
x.push( &array, &n );
x.push( &array, &f );
}

Will generate warnings for x.push( &array, &f );
 
K

Keith Thompson

From what I can tell, pointers are basically never checked by the
compilers -- they take the attitude that all pointers are the same (or
that the programmer knows what s/he's doing), and this includes
function pointer parameters. You have to actually dereference before
you get an error. Obviously you can't get away with this in C++.

I'll check this all again when I get access to a compiler again (I am
not near that kind of a system right now) but I'm pretty sure
contemporary modern C compilers all just screw you on this issue. (The
question is -- is this a "NULLStone" compliance issue, or what? -- Why
do *ALL* the C compilers screw you on this?)

void foo(void)
{
int *ip;
float *fp;
ip = fp;
fp = ip;
}
% gcc -c tmp.c
tmp.c: In function `foo':
tmp.c:5: warning: assignment from incompatible pointer type
tmp.c:6: warning: assignment from incompatible pointer type

Show us an example that you think *ALL* C compilers screw you on.
 
W

websnarf

Ian said:
Yes it will complain, based on my earlier macros:

#define X( type ) \
typedef struct xX##type { \
void (*push)( array_t*, type ); \
} X##type; \
\
void push##type##( array_t* array, type data ) { \
array_push( array, data); }

#define GET_X( type, name ) \
X##type name; \
name.push = push##type;

typedef int* intp;

X(intp)

int main() {
GET_X( intp, x );

array_t array;
int n = 0;
float f = 0.0;
x.push( &array, &n );
x.push( &array, &f );
}

Will generate warnings for x.push( &array, &f );

Have you tried it? After adding in a return 0; MSVC 6.0 says there are
no problems at its highest warning level. I'll check it with Intel,
Watcom, Borland, and gcc in a couple days, but my recollection is that
only Intel at its very highest warning level says anything -- but
Intel's highest warning level is annoying (it tells you strlen(s) +
strlen(t) is suspect because strlen could be overridden and have a side
effect and the + operation does not guarantee order) and I think a lot
of people dial it down at least one level from the highest, at which
point we lose this warning.
 
K

Keith Thompson

Ian Collins wrote: [...]
Yes it will complain, based on my earlier macros:

#define X( type ) \
typedef struct xX##type { \
void (*push)( array_t*, type ); \
} X##type; \
\
void push##type##( array_t* array, type data ) { \
array_push( array, data); }

#define GET_X( type, name ) \
X##type name; \
name.push = push##type;

typedef int* intp;

X(intp)

int main() {
GET_X( intp, x );

array_t array;
int n = 0;
float f = 0.0;
x.push( &array, &n );
x.push( &array, &f );
}

Will generate warnings for x.push( &array, &f );

Have you tried it? After adding in a return 0; MSVC 6.0 says there are
no problems at its highest warning level. I'll check it with Intel,
Watcom, Borland, and gcc in a couple days, but my recollection is that
only Intel at its very highest warning level says anything -- but
Intel's highest warning level is annoying (it tells you strlen(s) +
strlen(t) is suspect because strlen could be overridden and have a side
effect and the + operation does not guarantee order) and I think a lot
of people dial it down at least one level from the highest, at which
point we lose this warning.

I'm surprised you got far enough to get any warnings. When I compiled
the above code with gcc, I got:

tmp.c:15: error: parse error before '*' token
tmp.c:15:1: pasting "pushintp" and "(" does not give a valid preprocessing token
tmp.c:15: error: parse error before '*' token

followed by a cascade of other errors.

Getting rid of the macros and stripping down the program, I tried
this:

void push_int(int *ptr)
{
/* ... */
}

int main(void)
{
struct {
void (*push)(int*);
} x;
x.push = push_int;
int n = 0;
float f = 0.0;
x.push( &n );
x.push( &f ); /* This is line 15 */
return 0;
}

"gcc -c tmp.c" gave me:

tmp.c: In function `main':
tmp.c:15: warning: passing arg 1 of pointer to function from incompatible pointer type

(This was without any arguments to increase the warning level.)

If you're using a compiler that doesn't at least warn you about this
kind of type mismatch, get a better compiler.
 
I

Ian Collins

Have you tried it? After adding in a return 0; MSVC 6.0 says there are
no problems at its highest warning level.

Well it should. That's a rather old and non-compliant compiler.

Sun cc (normal warnings) gives:

"/tmp/x.c", line 28: warning: argument #2 is incompatible with prototype:
prototype: pointer to int : "unknown", line 0
argument : pointer to float

gcc (normal warnings) gives:

/tmp/x.c: In function `main':
/tmp/x.c:28: warning: passing arg 2 of pointer to function from
incompatible pointer type
 
I

Ian Collins

Keith said:
I'm surprised you got far enough to get any warnings. When I compiled
the above code with gcc, I got:

tmp.c:15: error: parse error before '*' token
tmp.c:15:1: pasting "pushintp" and "(" does not give a valid preprocessing token
tmp.c:15: error: parse error before '*' token
Interesting, gcc4 gives the error, gcc3 does not.

Which is correct and if it's gcc4, how does one use the preprocessor to
build a function prototype?
 
K

Keith Thompson

Ian Collins said:
Interesting, gcc4 gives the error, gcc3 does not.

Which is correct and if it's gcc4, how does one use the preprocessor
to build a function prototype?

The first thing I'd do is drop the unnecessary ## operator that tried
to join an identifier to a '('. When I tried that with the posted
program, I got other errors that I didn't bother to track down.

To summarize: I don't know.
 
I

Ian Collins

Keith said:
The first thing I'd do is drop the unnecessary ## operator that tried
to join an identifier to a '('. When I tried that with the posted
program, I got other errors that I didn't bother to track down.
You're right, removing the unnecessary ## fixes the error. It was
redundant anyway.

Adding

typedef int array_t;
extern int array_push(array_t *array, void *data);

to the top of the file as a kludge will remove any remaining errors.
 
W

websnarf

Ian said:
You're right, removing the unnecessary ## fixes the error. It was
redundant anyway.

Adding

typedef int array_t;
extern int array_push(array_t *array, void *data);

to the top of the file as a kludge will remove any remaining errors.

I have implemented the whole shehbang here:

http://www.pobox.com/~qed/gstack.zip

The ## rules are annoying, and I find that in general, the way to deal
with them is to wrap that whole thing in a two-level macro itself (it
works, so long as we don't care about namespace invasion).
 

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

No members online now.

Forum statistics

Threads
473,755
Messages
2,569,536
Members
45,007
Latest member
obedient dusk

Latest Threads

Top