A generic interface for numeric variables

P

pozz

In C I can have a small set of numeric type variables: char, short,
int and long (ignoring long long), and their unsigned counterpart.
In one of my program, I have a lot of (about 200) numeric variables of
different types, signed and unsigned.

Now I want to create a generic function that shows the value of a
variable on the display with a format string dependent on the
variable, increase/decrease it inside a range dependent on the
variable and set it the new value.

I create the following code... do you have any suggestions or
improvements? It seems to me too complex to read (a lot of if(p-
type)... )

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

signed char sc = -1;
unsigned char uc = UCHAR_MAX;
signed short ss = -1;
unsigned short us = SHRT_MAX;
signed int si = -1;
unsigned int ui = UINT_MAX;
signed long int sl = -1;
unsigned long int ul = ULONG_MAX;

typedef struct {
enum {
NUM_TYPE_SCHAR,
NUM_TYPE_UCHAR,
NUM_TYPE_SSHRT,
NUM_TYPE_USHRT,
NUM_TYPE_SINT,
NUM_TYPE_UINT,
NUM_TYPE_SLONG,
NUM_TYPE_ULONG,
} type;
void *ptr;
const char *fmt;
union {
signed char min_sc;
unsigned char min_uc;
signed short min_ss;
unsigned short min_us;
signed int min_si;
unsigned int min_ui;
signed long min_sl;
unsigned long min_ul;
} min;
union {
signed char max_sc;
unsigned char max_uc;
signed short max_ss;
unsigned short max_us;
signed int max_si;
unsigned int max_ui;
signed long max_sl;
unsigned long max_ul;
} max;
} num_t;

/* Just some example variables */
num_t num_sc = { NUM_TYPE_SCHAR, &sc, "%d degrees", { .min_sc = -10 },
{ .max_sc = 10 } };
num_t num_uc = { NUM_TYPE_UCHAR, &uc, "%u apples", { .min_sc = 0 },
{ .max_sc = UCHAR_MAX } };
num_t num_ss = { NUM_TYPE_SSHRT, &ss, "%d points", { .min_ss = -10 },
{ .max_ss = 10 } };
num_t num_us = { NUM_TYPE_USHRT, &us, "%u meters", { .min_ss = 0 },
{ .max_ss = USHRT_MAX } };
num_t num_si = { NUM_TYPE_SINT, &si, "%d dollars", { .min_si = -10 },
{ .max_si = 10 } };
num_t num_ui = { NUM_TYPE_UINT, &ui, "%u seconds", { .min_si = 0 },
{ .max_si = UINT_MAX } };
num_t num_sl = { NUM_TYPE_SLONG, &sl, "%d depth", { .min_sl = -10 },
{ .max_sl = 10 } };
num_t num_ul = { NUM_TYPE_ULONG, &ul, "%u km", { .min_sl = 0 },
{ .max_sl = ULONG_MAX } };

void
num_print(num_t *num)
{
if (num->type == NUM_TYPE_SCHAR) {
printf (num->fmt, *(signed char *)num->ptr);
} else if (num->type == NUM_TYPE_UCHAR) {
printf (num->fmt, *(unsigned char *)num->ptr);
} else if (num->type == NUM_TYPE_SSHRT) {
printf (num->fmt, *(signed short *)num->ptr);
} else if (num->type == NUM_TYPE_USHRT) {
printf (num->fmt, *(unsigned short *)num->ptr);
}
/* ... */
}
void
num_increase(num_t *num)
{
if (num->type == NUM_TYPE_SCHAR) {
signed char value = *(signed char *)num->ptr;
if (value < num->max.max_sc) {
*(signed char *)(num->ptr) = ++value;
}
} else if (num->type == NUM_TYPE_UCHAR) {
unsigned char value = *(unsigned char *)num->ptr;
if (value < num->max.max_uc) {
*(unsigned char *)(num->ptr) = ++value;
}
} else if (num->type == NUM_TYPE_SSHRT) {
signed short value = *(signed short *)num->ptr;
if (value < num->max.max_ss) {
*(signed short *)(num->ptr) = ++value;
}
} else if (num->type == NUM_TYPE_USHRT) {
unsigned short value = *(unsigned short *)num->ptr;
if (value < num->max.max_us) {
*(unsigned short *)(num->ptr) = ++value;
}
}
/* ... */
}

void
num_decrease(num_t *num)
{
if (num->type == NUM_TYPE_SCHAR) {
signed char value = *(signed char *)num->ptr;
if (value > num->min.min_sc) {
*(signed char *)(num->ptr) = --value;
}
} else if (num->type == NUM_TYPE_UCHAR) {
unsigned char value = *(unsigned char *)num->ptr;
if (value > num->min.min_uc) {
*(unsigned char *)(num->ptr) = --value;
}
} else if (num->type == NUM_TYPE_SSHRT) {
signed short value = *(signed short *)num->ptr;
if (value > num->min.min_ss) {
*(signed short *)(num->ptr) = --value;
}
} else if (num->type == NUM_TYPE_USHRT) {
unsigned short value = *(unsigned short *)num->ptr;
if (value > num->min.min_us) {
*(unsigned short *)(num->ptr) = --value;
}
}
/* ... */
}

void
num_cpyvalue(num_t *dst, const num_t *src)
{
if (dst->type == NUM_TYPE_SCHAR) {
*(signed char *)dst->ptr = *(signed char *)src->ptr;
} else if (dst->type == NUM_TYPE_UCHAR) {
*(unsigned char *)dst->ptr = *(unsigned char *)src->ptr;
} else if (dst->type == NUM_TYPE_SSHRT) {
*(signed short *)dst->ptr = *(signed short *)src->ptr;
} else if (dst->type == NUM_TYPE_USHRT) {
*(unsigned short *)dst->ptr = *(unsigned short *)src->ptr;
}
/* ... */
}

void
num_change(num_t *num)
{
char c;
num_t num_new = *num;
signed char new_sc;
unsigned char new_uc;
signed short new_ss;
unsigned short new_us;
/* ... */

if (num->type == NUM_TYPE_SCHAR) {
new_sc = *(signed char *)num->ptr;
num_new.ptr = &new_sc;
} else if (num->type == NUM_TYPE_UCHAR) {
new_uc = *(unsigned char *)num->ptr;
num_new.ptr = &new_uc;
} else if (num->type == NUM_TYPE_SSHRT) {
new_ss = *(signed short *)num->ptr;
num_new.ptr = &new_ss;
} else if (num->type == NUM_TYPE_USHRT) {
new_us = *(unsigned short *)num->ptr;
num_new.ptr = &new_us;
}

printf("The value is: ");
num_print(num);
putchar('\n');

printf ("Press U/D/O: ");
do {
c = getchar();
if (c == '\n') {
continue;
} else if ((c == 'U') || (c == 'u')) {
num_increase (&num_new);
} else if ((c == 'D') || (c == 'd')) {
num_decrease (&num_new);
} else if ((c == 'O') || (c == 'o')) {
break;
}
printf("The value is: ");
num_print(&num_new);
putchar('\n');
printf ("Press U/D/O: ");
} while ((c != 'O') && (c != 'o'));
num_cpyvalue (num, &num_new);
}

int
main (int argc, char *argv[])
{
num_change (&num_sc);
printf ("The new value is: ");
num_print(&num_sc);
putchar('\n');
return 0;
}
 
E

Eric Sosman

In C I can have a small set of numeric type variables: char, short,
int and long (ignoring long long), and their unsigned counterpart.
In one of my program, I have a lot of (about 200) numeric variables of
different types, signed and unsigned.

Now I want to create a generic function that shows the value of a
variable on the display with a format string dependent on the
variable, increase/decrease it inside a range dependent on the
variable and set it the new value.

I create the following code... do you have any suggestions or
improvements? It seems to me too complex to read (a lot of
if(p->type)... )
[...]

I agree: It's too complex. Let's try to simplify, shall we?

First, I see no need for all of `signed char', `signed short',
`signed int', and `signed long': Just let `signed long' stand in
for the whole batch. You're placing upper and lower bounds on
each value anyway, so if the value's bounds are within the range
of `signed short' they are also within the range of `signed long'.
Thanks to the bounds you will never over- or underflow, so you
needn't worry about `signed char' and `signed int' behaving
differently with out-of-range results; the out-of-range results
will never occur in the first place.

Similar remarks apply to the `unsigned' variants. Since the
wrap-around behavior of `unsigned' arithmetic is well-defined, there
are circumstances where `unsigned short' and `unsigned long' will in
fact behave differently but reliably. However, your own upper and
lower bounds still lurk in the background: If you never try to go
below the lower nor above the upper, you never encounter wrap-around
situations anyhow. So if wrap-around doesn't happen, you don't rely
on `unsigned char' and `unsigned int' having different ranges, and
you can use `unsigned long' as a proxy for the entire suite.

Okay, we've simplified by reducing eight possibilities to just
two. (Somebody's sure to say we're wasting space by using a `long'
where a `char' would suffice, but any such Somebody hasn't been
paying attention: Your present scheme uses a `union' of all the
candidate types, so you use a `long's worth of space even for a
`char' variable. Using `long' for everything takes no more space
than you're already using.)

You could go ahead with a three-quarters-reduced version of
your current scheme, but now that the possibilities are down to two
(signed vs. unsigned) I'd be strongly tempted to say "Two is a
manageable number; I'll handle the two types separately." This is
optional, of course, but it has the advantage of eliminating all
those `if's you find so objectionable. You'd get something like

typedef struct {
signed long val;
signed long min;
signed long max;
} Signed;

typedef struct {
unsigned long val;
unsigned long min;
unsigned long max;
} Unsigned;

void increaseSigned(Signed *thing);
...
void decreaseUnsigned(Unsigned *thing);
...

Signed celsius = { 0, -273, LONG_MAX };
Unsigned kelvin = { 273, 0, ULONG_MAX );
...

increaseSigned(&celsius);
decreaseUnsigned(&kelvin);

This has the disadvantage that you must remember whether to use
the signedXxx() or unsignedXxx() function -- but then, the compiler
will tell you if you get it wrong. With two possibilities instead of
eight this seems not too great a burden, but if you dislike even that
much you can always stick a type code into the `struct', as in your
original scheme.

Another thing to consider is that the range limits probably aren't
specific to each variable instance, but to whatever the variable's
value measures. It's so-and-so many splonders in a bank account, or
so-and-so many poods per dunam of fertilizer, or so-and-so many
centicenturies of post-secondary education. This suggests separating
the ranges from the values, leading to a rather different scheme:

typedef struct {
signed long min;
signed long max;
} SignedRange;

typedef struct {
unsigned long min;
unsigned long max;
} UnsignedRange;

signed long increaseSigned(signed long value,
const SignedRange *range);
...
unsigned long decreaseUnsigned(unsigned long value,
const UnsignedRange *range);
...

const SignedRange celsiusRange = { -273, LONG_MAX };
const UnsignedRange kelvinRange = { 0, ULONG_MAX };
...

signed long celsius = 0;
unsigned long kelvin = 273;
...

celsius = increaseSigned(celsius, &celsiusRange);
kelvin = decreaseSigned(kelvin, &kelvinRange);

Note that this works (with suitable ranges) even if `celsius'
is `signed short' and `kelvin' is `unsigned char': C's argument
promotions and assignment conversions allow you to use the `long'
functions with narrower quantities. Of course, you *can* stick
with per-instance ranges, if that makes more sense for your problem,
but per-"scale" ranges may turn out to be more convenient.

Summary: Three-quarters of your complexity can be done away with
at one stroke. You may choose to live with what remains, or to adopt
a slightly different API.
 
P

pozz

[...]
Okay, we've simplified by reducing eight possibilities to just
two. (Somebody's sure to say we're wasting space by using a `long'
where a `char' would suffice, but any such Somebody hasn't been
paying attention: Your present scheme uses a `union' of all the
candidate types, so you use a `long's worth of space even for a
`char' variable. Using `long' for everything takes no more space
than you're already using.)

The approach I presented is a small variation of the one I'm really
using.
I need to declare const structures to save precious RAM space on
my embedded platform, but I can't initialize whatever member of
unions/structs (a limit of the compiler). So my real scheme doesn't
use unions but a struct/substruct approach:

typedef struct {
signed char *ptr;
signed char min;
signed char max;
} scnum_t;

typedef struct {
unsigned char *ptr;
unsigned char min;
unsigned char max;
} ucnum_t;

/*...*/

typedef struct {
enum {
NUM_TYPE_SCHAR,
NUM_TYPE_UCHAR,
NUM_TYPE_SSHRT,
NUM_TYPE_USHRT,
NUM_TYPE_SINT,
NUM_TYPE_UINT,
NUM_TYPE_SLONG,
NUM_TYPE_ULONG,
} type;
void *subnum; /* Pointer to scnum_t, ucnum_t, ... */
} num_t;

In this case I can write:

signed char temp; /* The variable */
const scnum_t snTemperature = { &temp, -10, +10 };
const num_t nTemperature = { NUM_TYPE_SCHAR, &snTemperature };

With an additional pointer (subnum in num_t), I can declare min/max
values as the correct type, so without losing space declaring long or
unsigned long min/max.
Most of the variables have small values, so they are already declared
as signed/unsigned char/short. In that case, on a 16-bit platform
(like the one I'm using), I need one num_t struct (4 bytes) and one
"subnum" struct (4 bytes).
If subnum types are incorporated into num_t with min/max as long, I
need only one num_t struct (12 bytes) for all variables.
With my approach I can save 4 bytes for each "small values" variable.

Anyway this isn't a very big problem, because I have a lot of "const"
space in non-volatile memory.

You could go ahead with a three-quarters-reduced version of
your current scheme, but now that the possibilities are down to two
(signed vs. unsigned) I'd be strongly tempted to say "Two is a
manageable number; I'll handle the two types separately." This is
optional, of course, but it has the advantage of eliminating all
those `if's you find so objectionable. You'd get something like

typedef struct {
signed long val;
signed long min;
signed long max;
} Signed;

typedef struct {
unsigned long val;
unsigned long min;
unsigned long max;
} Unsigned;

I can't do in this way. The variables are also declared and used in
several parts of the code and in different modules I can't touch.
So the numeric value can't reside inside Signed/Unsigned struct.
I have to use a pointer to the variable.

typedef struct {
enum {
NUM_TYPE_SCHAR,
NUM_TYPE_SSHRT,
NUM_TYPE_SINT,
NUM_TYPE_SLONG
} type;
void *ptr; /* Pointer to signed char/short/int as in type
*/
signed long min;
signed long max;
} Signed;

typedef struct {
enum {
NUM_TYPE_UCHAR,
NUM_TYPE_USHRT,
NUM_TYPE_UINT,
NUM_TYPE_ULONG
} type;
void *ptr; /* Pointer to unsigned char/short/int as in
type */
unsigned long min;
unsigned long max;
} Unsigned;

But, in this case, the compiler can't give me warning if I wrongly
assign to ptr member of Signed/Unsigned struct a variable with a
different type as indicated in the type member.
Also this can be a minor problem (in my struct/substruct approach
above, I have the same problem with subnum that is void*).

[...]
Another thing to consider is that the range limits probably aren't
specific to each variable instance, but to whatever the variable's
value measures. It's so-and-so many splonders in a bank account, or
so-and-so many poods per dunam of fertilizer, or so-and-so many
centicenturies of post-secondary education. This suggests separating
the ranges from the values, leading to a rather different scheme:

typedef struct {
signed long min;
signed long max;
} SignedRange;

typedef struct {
unsigned long min;
unsigned long max;
} UnsignedRange;

signed long increaseSigned(signed long value,
const SignedRange *range);
...
unsigned long decreaseUnsigned(unsigned long value,
const UnsignedRange *range);
...

const SignedRange celsiusRange = { -273, LONG_MAX };
const UnsignedRange kelvinRange = { 0, ULONG_MAX };
...

signed long celsius = 0;
unsigned long kelvin = 273;
...

celsius = increaseSigned(celsius, &celsiusRange);
kelvin = decreaseSigned(kelvin, &kelvinRange);

Note that this works (with suitable ranges) even if `celsius'
is `signed short' and `kelvin' is `unsigned char': C's argument
promotions and assignment conversions allow you to use the `long'
functions with narrower quantities. Of course, you *can* stick
with per-instance ranges, if that makes more sense for your problem,
but per-"scale" ranges may turn out to be more convenient.

This sounds very good. I can continue using already defined variables
with their "natural" type and use the automatic promotion/assignment
conversion of C.

Summary: Three-quarters of your complexity can be done away with
at one stroke. You may choose to live with what remains, or to adopt
a slightly different API.

Thank you very much for your suggestions and for spending some time
to help me. I appreciated it.
 
E

Eric Sosman

[...]
Okay, we've simplified by reducing eight possibilities to just
two. (Somebody's sure to say we're wasting space by using a `long'
where a `char' would suffice, but any such Somebody hasn't been
paying attention: Your present scheme uses a `union' of all the
candidate types, so you use a `long's worth of space even for a
`char' variable. Using `long' for everything takes no more space
than you're already using.)

The approach I presented is a small variation of the one I'm really
using.

Oh, g-r-e-a-t! Well, then, the suggestions I offer are "small
variations of helpful."
I need to declare const structures to save precious RAM space on
my embedded platform, but I can't initialize whatever member of
unions/structs (a limit of the compiler). So my real scheme doesn't
use unions but a struct/substruct approach:

typedef struct {
signed char *ptr;
signed char min;
signed char max;
} scnum_t;

typedef struct {
unsigned char *ptr;
unsigned char min;
unsigned char max;
} ucnum_t;

/*...*/

typedef struct {
enum {
NUM_TYPE_SCHAR,
NUM_TYPE_UCHAR,
NUM_TYPE_SSHRT,
NUM_TYPE_USHRT,
NUM_TYPE_SINT,
NUM_TYPE_UINT,
NUM_TYPE_SLONG,
NUM_TYPE_ULONG,
} type;
void *subnum; /* Pointer to scnum_t, ucnum_t, ... */
} num_t;

In this case I can write:

signed char temp; /* The variable */
const scnum_t snTemperature = {&temp, -10, +10 };
const num_t nTemperature = { NUM_TYPE_SCHAR,&snTemperature };

And you do all this to "save precious RAM?" Ye gods! You plan
to "save" RAM by replacing every variable-and-two-limits with a
variable *and* a struct containing the limits *and* a pointer to
the variable *and* a type code?
With an additional pointer (subnum in num_t), I can declare min/max
values as the correct type, so without losing space declaring long or
unsigned long min/max.
R-i-i-i-g-h-t.

Most of the variables have small values, so they are already declared
as signed/unsigned char/short. In that case, on a 16-bit platform
(like the one I'm using), I need one num_t struct (4 bytes) and one
"subnum" struct (4 bytes).

Plus the variable itself, for 9 bytes in all (for chars). If
you jettisoned the damn pointers and the type code and just put the
value and limits into one struct, you'd use 3 bytes per variable --
maybe 4, if the compiler pads structs rather freely -- instead of
the 9 you use in your current scheme (*if* it's truly your current
scheme, and not yet another "small variation").

If RAM is so "precious" that you eagerly seek ways to triple
your usage thereof, I'd like to sell you a truly precious bridge.
If subnum types are incorporated into num_t with min/max as long, I
need only one num_t struct (12 bytes) for all variables.
With my approach I can save 4 bytes for each "small values" variable.

Adding 200% overhead is a funny form of "savings."
Anyway this isn't a very big problem, because I have a lot of "const"
space in non-volatile memory.

Please reconcile "isn't a very big problem" with "precious."
Also, please explain why you need upper/lower range limits to bracket
the values of const variables.
I can't do in this way. The variables are also declared and used in
several parts of the code and in different modules I can't touch.
So the numeric value can't reside inside Signed/Unsigned struct.
I have to use a pointer to the variable.

You don't know how to point at an element of a struct?
typedef struct {
enum {
NUM_TYPE_SCHAR,
NUM_TYPE_SSHRT,
NUM_TYPE_SINT,
NUM_TYPE_SLONG
} type;
void *ptr; /* Pointer to signed char/short/int as in type
*/
signed long min;
signed long max;
} Signed;

typedef struct {
enum {
NUM_TYPE_UCHAR,
NUM_TYPE_USHRT,
NUM_TYPE_UINT,
NUM_TYPE_ULONG
} type;
void *ptr; /* Pointer to unsigned char/short/int as in
type */
unsigned long min;
unsigned long max;
} Unsigned;

But, in this case, the compiler can't give me warning if I wrongly
assign to ptr member of Signed/Unsigned struct a variable with a
different type as indicated in the type member.

Under your current (?) scheme(s?), the compiler can't warn you
about *any* mismatches. Failure to warn about *some* mismatches
is therefore an improvement.
Also this can be a minor problem (in my struct/substruct approach
above, I have the same problem with subnum that is void*).

Non capisco.
 
K

Kenny McCormack

[...]
Okay, we've simplified by reducing eight possibilities to just
two. (Somebody's sure to say we're wasting space by using a `long'
where a `char' would suffice, but any such Somebody hasn't been
paying attention: Your present scheme uses a `union' of all the
candidate types, so you use a `long's worth of space even for a
`char' variable. Using `long' for everything takes no more space
than you're already using.)

The approach I presented is a small variation of the one I'm really
using.

Oh, g-r-e-a-t! Well, then, the suggestions I offer are "small
variations of helpful."
(etc)

The boy has issues.

--
Windows 95 n. (Win-doze): A 32 bit extension to a 16 bit user interface for
an 8 bit operating system based on a 4 bit architecture from a 2 bit company
that can't stand 1 bit of competition.

Modern day upgrade --> Windows XP Professional x64: Windows is now a 64 bit
tweak of a 32 bit extension to a 16 bit user interface for an 8 bit
operating system based on a 4 bit architecture from a 2 bit company that
can't stand 1 bit of competition.
 
K

Keith Thompson

pozz said:
Il 08/04/2011 16:05, Kenny McCormack ha scritto:

Oh yes, I have many issues... but I think a lot of boys are in the same
situations. The difference is I know it, they don't.

I'm sorry if I'm disturbing this ng with my stupid questions or
considerations. I'm not a C guru and I have many things to learn yet
about this language. If newbies can't post here, I stop immediately.

A quick look at Kenny McCormack's posting history here should
tell you whether there's any point in paying any attention to him.
(Hint: there isn't.)
 
K

Kenny McCormack

Il 08/04/2011 16:05, Kenny McCormack ha scritto:

Oh yes, I have many issues... but I think a lot of boys are in the same
situations. The difference is I know it, they don't.

I'm sorry if I'm disturbing this ng with my stupid questions or
considerations. I'm not a C guru and I have many things to learn yet
about this language. If newbies can't post here, I stop immediately.

The boy I was talking about was Eric

Not you.

Pay no attention to the troll "Kiki" Thompson.

--
(This discussion group is about C, ...)

Wrong. It is only OCCASIONALLY a discussion group
about C; mostly, like most "discussion" groups, it is
off-topic Rorsharch [sic] revelations of the childhood
traumas of the participants...
 

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,011
Latest member
AjaUqq1950

Latest Threads

Top