0

I'm not quite into enums, but I need to create a datatype in C that represents numbers with just one digit.

I have tried with enums like:

enum digit {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

Obiously the compiler interpret this as assigning the enumeration to a non-specified value.

Is there any way to "cheat" the compiler to let me do this? Is there any other possible way to bound the range of an integer?

Roberto P. Romero
  • 357
  • 1
  • 3
  • 14

3 Answers3

1

I recomend to use an abstract datatype with setters and getters if you really have to use range checking (otherwise use char/int and make sure not to write buggy code):

/* digit.h */

#include <assert.h>

struct digit
{
   unsigned int value;
};


static inline unsigned int
digit_set(struct digit *d, unsigned int val)
{
    assert(val <= 9);
    d->val = val;
    return val;
}

static inline unsigned int
digit_get(const struct digit *d)
{
    return d->val;
}
HAL9000
  • 2,138
  • 1
  • 9
  • 20
  • Thats not a good use of asserts, see [this post](https://stackoverflow.com/questions/8114008/when-should-we-use-asserts-in-c). – Osiris Jul 26 '18 at 23:27
  • @Osiris, why is this a bad use of assert? If you try to assign the value 11 in your digit there is a bug in your program that you cannot recover from, and an abort should be the right course of action. On the other hand if you use range checking for user input then, yes, you should return an error value rather than abort. But then, verifying input should normally be done when receiving input not when using it. Besides this is totally not the point. The point here was to show how you could put error checking in a setter function. Replace the error checking with whatever method you prefer. – HAL9000 Jul 26 '18 at 23:47
  • I just wanted to mention that it is not a proper usage of asserts. Asserts are replaced with a NOP if the program is compiled with `-DNODEBUG` and it should not be used to check for example user input. It should be only used as debug function and thats not how it is used in your answer. – Osiris Jul 26 '18 at 23:51
  • 2
    Whether the use of `assert` here is wrong depends on whether the argument is unchecked input or whether the setter's interface contract is that the caller must pass a valid digit value. – R.. GitHub STOP HELPING ICE Jul 27 '18 at 00:07
  • @Osiris, the question did not say anything about user input. He just wanted to check assignments. It is impossible to categorically say that this is not a proper usage of assert. It all depends if you want to check for programmer errors or user errors. In case of user error checking, use some other error reporting, the basic solution to the question remains the same. – HAL9000 Jul 27 '18 at 00:07
  • @R.. If it is already checked somewhere else then what is the purpose of the setter function? – Osiris Jul 27 '18 at 00:30
  • If it is unchecked then assert is no good use for it, if the assumption is that it is already checked then it is not an answer at all. – Osiris Jul 27 '18 at 00:35
  • @osiris: The whole point of assertions is **always** validating an invariant that would be a programming error if violated. They're to catch programming errors. – R.. GitHub STOP HELPING ICE Jul 27 '18 at 03:05
  • Whether `assert()` is correct here or not can't be determined by the wording of the question. But the code has an error, the `struct` member is called `value`, not `val`. –  Jul 27 '18 at 07:13
  • asserts give people a sense of safety that is misleading. they only catch errors *if* you go through that code path *in the invalid state* when the code *is compiled with asserts enabled*. I've seen enough bugs in production where there is an assert in the code which would have caught the issue if it had been compiled in. But we have to provide 24x7 service and assert failure is not an option in production – Tom Tanner Jul 27 '18 at 07:30
1

HAL2000's answer is good, still I will elaborate on it. As discussed in the comments, assert() is only the correct way if the setter is expected to be always called with a number between 0 and 9 and failure to do so is considered a programming error.

If you want validation instead (only set the value when it's in the correct range, otherwise report an error to the calling code), you'd change the setter like this:

static inline int
digit_set(struct digit *d, unsigned int val)
{
    if (val > 9) return -1;
    d->value = val;
    return 0;
}

You can pick the returned values freely, -1 for error and 0 for success is very widespread in C, so I picked this. You could also prefer 1 for success and 0 for error, which would make your return value directly usable in a boolean expression.


This whole idea has a little weakness: Nothing will ever prevent you from directly setting the value member, bypassing your setter. Now, there's a solution to this as well, but it comes with costs. You can hide the definition of the struct in a separate translation unit, therefore calling code doesn't ever see its internals. It could look like this:

digit.h:

#ifndef DIGIT_H
#define DIGIT_H

typedef struct Digit Digit;

Digit *Digit_create(int val);
int Digit_setVal(Digit *self, int val);
int Digit_val(const Digit *self);
void Digit_destroy(Digit *self);

#endif

dgit.c:

#include <stdlib.h>
#include "digit.h"

struct Digit {
    int val;
};

Digit *Digit_create(int val)
{
    if (val < 0 || val > 9) return 0;
    Digit *self = malloc(sizeof *self);
    if (!self) return 0;
    self->val = val;
    return self;
}

int Digit_setVal(Digit *self, int val)
{
    if (val < 0 || val > 9) return -1;
    self->val = val;
    return 0;
}

int Digit_val(const Digit *self)
{
    return self->val;
}

void Digit_destroy(Digit *self)
{
    free(self);
}

The cost:

This way of information hiding unfortunately requires a separate translation unit and dynamic allocation of the objects, which is expensive. I personally wouldn't recommend it for such a simple thing. But if you're e.g. writing a library and want to enforce it can't be used in a wrong way, it's worth a thought.

This basically is OOP realized in C, and for more complex structures, it really gives some benefit (also in structuring your program).

In the example above, the "constructor" Digit_create() makes sure to only ever construct valid objects. This is generally a good idea for robust code in more complex scenarios.

If you decide to implement something like this, make sure the deallocation function (here Digit_destroy()) works the same way as free(): It should silently ignore a null pointer. This is trivial here, because there's nothing else to do than calling free().

0

You can't limit the range of values for either a type or variable in C unfortunately.

I would do a typedef char digit or typedef int digit and use that as the type for your variables, being sure not to assign anything besides 0 to 9 to your value. Note that this will not do any typechecking, and merely serves as documentation for you.

CoffeeTableEspresso
  • 2,614
  • 1
  • 12
  • 30