Is up-casting numeric types in C always reversible?

121 views Asked by At

EDIT: Oh dear I feel foolish. Of course you're not going to be able to cast from longs to doubles and back - longs have more significant bits. Will casting to a long double improve this?

I'm implementing an Array List in C which I want to be type-agnostic. I also don't want it to be so verbose in usage, thus I'd rather my get functions not return void pointers and require type-casting by the user. Therefore I'm storing an enum for the type (one type for each list), which can be INT, FLOAT, LONG, or DOUBLE. Parameters are passed as doubles (so the compiler will implicitly cast if you pass an int, float or long), then depending on the type of data stored in the list, the double which is passed gets casted to the correct type. Will this always work? Will a number, casted to a double, and then back down to its original type ever have a different value? Are there any other issues with the overarching idea? Barebones implementation is below

#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <signal.h>

typedef enum{
    INT,
    LONG,
    FLOAT,
    DOUBLE
}TYPE;

typedef struct arrayList{
    void * data;
    TYPE type;
    size_t capacity;
    size_t num_elements;
}arrayList;

void reallocateArrayList(arrayList * list, size_t capacity){
    TYPE type=list->type;
    if(type == INT){
        void * toAllocate = malloc(capacity*sizeof(int));
        memcpy(toAllocate, list->data, list->num_elements*sizeof(int));
        if(list->data!=NULL){
            free(list->data);
        }
        list->data = toAllocate;
    }
    if(type == FLOAT){
        void * toAllocate = malloc(capacity*sizeof(float));
        memcpy(toAllocate, list->data, list->num_elements*sizeof(float));
        if(list->data!=NULL){
            free(list->data);
        }
        list->data = toAllocate;
    }
    if(type==LONG){
        void * toAllocate = malloc(capacity*sizeof(long));
        memcpy(toAllocate, list->data, list->num_elements*sizeof(long));
        if(list->data!=NULL){
            free(list->data);
        }
        list->data = toAllocate;
    }
    if(type==DOUBLE){
        void * toAllocate = malloc(capacity*sizeof(double));
        memcpy(toAllocate, list->data, list->num_elements*sizeof(double));
        if(list->data!=NULL){
            free(list->data);
        }
        list->data = toAllocate;        
    }
    list->capacity=capacity;
    if(list->data==NULL) raise(ENOMEM);
}

arrayList * createArrayList(TYPE type){
    arrayList * retval = (arrayList *) malloc(sizeof(arrayList));
    retval->capacity=256;
    retval->data = NULL;
    retval->type = type;
    retval->num_elements = 0;
    reallocateArrayList(retval, 256);
    return retval;

}

void add(arrayList * list, double val){
    if(list->capacity<=list->num_elements){
        reallocateArrayList(list, 4*list->capacity);
    }
    TYPE type=list->type;
    if(type == INT){
        ((int *)(list->data))[list->num_elements]=(int) val;
    }
    if(type == FLOAT){
        ((float *)(list->data))[list->num_elements]=(float) val;
    }
    if(type==LONG){
        ((long *)(list->data))[list->num_elements]=(long) val;

    }
    if(type==DOUBLE){
    ((double *)(list->data))[list->num_elements]=val;
    }
    list->num_elements++;
}

double get(arrayList * list, size_t index){
    double retval;
    if(index>=list->num_elements){
        printf("Error: arrayList get index out of range");
        raise(SIGSEGV);
    }
    if(list->type==INT){
        retval=((int*)(list->data))[index];
    }
    if(list->type==FLOAT){
        retval=((float*)(list->data))[index];
    }
    if(list->type==LONG){
        retval=((long*)(list->data))[index];
    }
    if(list->type==DOUBLE){
        retval=((double*)(list->data))[index];
    }
    return retval;
}

2

There are 2 answers

7
gulpr On BEST ANSWER

Is up-casting numeric types in C always reversible?

No, it is implementation-defined behaviour and it is a very bad idea.

I would use voids and some helper macros to make it completely universal. It is completely type agnostic and can be used with any data type (also structs, unions and arrays)

typedef struct arrayList{
    size_t capacity;
    size_t num_elements;
    size_t elemsize;
    unsigned char data[];
}arrayList;

#define listCreate(capacity, type) reallocateArrayList(NULL, capacity, sizeof(type))
#define listReaaloc(list, capacity) reallocateArrayList(list, capacity, list ? list -> elemsize : 0)
#define listGet(list, object, index) get(list, index, &object)
#define listAdd(list, object) add(list, &object)


arrayList *reallocateArrayList(arrayList * list, size_t capacity, size_t elemsize)
{
    size_t new_num_elements = list ? list -> num_elements : 0;
    
    list = realloc(list, sizeof(*list) + capacity * elemsize * sizeof(list -> data[0]));
    if(list)
    {
        list -> capacity = capacity;
        list -> elemsize = elemsize;
        list -> num_elements = new_num_elements;
    }
    return list;
}

int add(arrayList * list, void *val)
{
    int result = -1;
    if(list && list -> num_elements < list -> capacity)
    {
        memcpy(list -> data + list -> num_elements++ * list -> elemsize, val, list -> elemsize);
        result = 0;
    }
    return result;
}

int get(arrayList * list, size_t index, void *element)
{
    int result = -1;
    if(list && index < list -> num_elements)
    {
        memcpy(element, list -> data + index * list -> elemsize, list -> elemsize);
        result = 0;
    }
    return result;
}

and some example usage:

int main(void)
{
    arrayList *al = listCreate(100, double);
    double dbl = 10.3;

    listAdd(al, (double){4.3});
    listAdd(al, dbl);

    printf("%zu\n", al -> num_elements);

    for(size_t index = 0; index < al -> num_elements; index++)
    {
        double y;
        listGet(al, y, index);
        printf("%f\n", y);
    }

    free(al);
}

https://godbolt.org/z/EYezracjT

0
Mark Adler On

To answer the specific question about faithfully storing a long in a long double, if you're on Intel, then yes. The 80-bit long double type has a 64-bit mantissa, and so can store a 64-bit integer (signed or unsigned) exactly. A long double takes more memory though, usually 16 bytes, even though only ten are used.

Most other architectures however do not have a longer long double. Their "long double" is the same as the 64-bit double, with only 52 bits for the mantissa. Such a use would not be portable.

You are implying that your long is 64 bits, which it may or may not be. If you want specific lengths, use #include <stdint.h> and types like int32_t, uint64_t, etc.

You should use a union or equivalent instead to store the different types. Something like:

typedef struct {
    enum {
        INT,
        LONG,
        FLOAT,
        DOUBLE
    } type;
    union {
        int int_val;
        long long_val;
        float float_val;
        double double_val;
    };
} val_t;