Search code examples
cmacrosstructcycletype-conversion

Macro to cycle through and allocate data to members of structs incorrectly recognises struct member as pointer


My problem is that atoi is converting a string's hexadecimal memory address to decimal, instead of what's contained within the string. It is doing this during a macro. Why is it interpreting struct->member as a pointer, when the macro definition makes it an int? Pseudocode below:

if (struct.member == int)? struct.member = atoi(data) : struct.member = data;

The aim of this section of the program is to retrieve data from a .csv file containing information about a structs attributes. I can take in an "id" and store each cells string into an array of strings (csvRowSplit).

However, I then want to transfer the contents of the array to a structure that contains different data types (a method that I want to use to retrieve players saved attributes, attack methods, shop items etc.). It's easy to hard code:

opponent->att = atoi(csvSplitRow[0]);
opponent->= atoi(csvSplitRow[1]);
opponent->hitpoints = atoi(csvSplitRow[2]);
opponent->description = csvSplitRow[3]);

However this clutters with more struct members and is not very flexible or reproducible.

I have defined a macro to cycle through the elements of a structure, and pair up the csvSplitRow[] to the variables, converting if required with atoi.

#define X_FIELDS \
    X(char*, description, "%s") \
    X(int, xpreward, "%d") \
    X(int, att, "%d") \
    /* ... */
    X(int, crit, "%d")

typedef struct
{
    #define X(type, name, format) type name;
        X_FIELDS
    #undef
}

void update_opp(char** csvSplitRow, opp* opponent)
{
    int i = 0;
    #define X(type, name, format) \
        if (strcmp(format, "%d") == 0) \          // if an int, convert it
            opponent->name = atoi(csvSplitRow[i]); \
        else \                                    // otherwise put it straight in
            opponent->name = csvSplitRow[i]; \
        i++;
    X_FIELDS
    #undef X
}

The assignment directly to the string members work (ie. no conversion), but atoi results in a conversion of the hexadecimal memory address to an integer, instead of the string it's pointing to.

// before conversion
csvRowSplit[1] == 0x501150 "20"

// practice
atoi(csvRowSplit[1]) == 20

// after conversion and storing in struct int member
opponent->xpreward = atoi(csvSplitRow[1]);
opponent->xpreward == 5247312           // the decimal equivalent of 0x501150

I don't know what I can do now, other than hard code each time I want to match up a parsed line of csv with a structure. Please help!

EDIT: I get a compile time error with -Werror:

error: assignment makes integer from pointer without a cast [-Werror]

Error is within the macro of the update_opp function. However, I know it's not a pointer because I defined it to be an int earlier? So why isn't it recognizing that? and I can't cast it, so what can I do?


Solution

  • Your problem is in this code:

    #define X(type, name, format) \
        if (strcmp(format, "%d") == 0) \          // if an int, convert it
            opponent->name = atoi(csvSplitRow[i]); \
        else \                                    // otherwise put it straight in
            opponent->name = csvSplitRow[i]; \
        i++;
    

    For any given attribute name (att say), you get:

        if (strcmp("%d", "%d") == 0)
            opponent->att = atoi(csvSplitRow[i]);
        else
            opponent->att = csvSplitRow[i];
        i++;
    

    Except that it is all on one line. But, the key point is that you either assign an int (from atoi()) to a string or you assign a string to an int in every invocation, and one of those two is wrong. The code has to be correct before it is optimized.

    How to fix? That's tricky. I think I'd probably use something like this:

    #include <stdlib.h>
    
    #define CVT_INT(str)    atoi(str)
    #define CVT_STR(str)    str
    
    #define X_FIELDS \
        X(char*, description, "%s", CVT_STR) \
        X(int, xpreward, "%d", CVT_INT) \
        X(int, att, "%d", CVT_INT) \
        /* ... */ \
        X(int, crit, "%d", CVT_INT)
    
    typedef struct opp
    {
    #define X(type, name, format, converter) type name;
        X_FIELDS
    #undef X
    } opp;
    
    extern void update_opp(char** csvSplitRow, opp* opponent);
    
    void update_opp(char** csvSplitRow, opp* opponent)
    {
        int i = 0;
    
    #define X(type, name, format, converter) \
        opponent->name = converter(csvSplitRow[i++]);
    
        X_FIELDS
    
    #undef X
    }
    

    This compiles without warnings under very stringent compiler flags:

    gcc -pedantic -g -std=c99 -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes \
        -Wold-style-definition -c xm.c
    

    The CVT_INT and CVT_STR macros could be redefined when required to do other things.


    An alternative version of the code exploits CVT_INT and CVT_STR (renamed to X_INT and X_STR) more extensively:

    #include <stdlib.h>
    
    #define X_FIELDS \
        X(X_STR, description) \
        X(X_INT, xpreward) \
        X(X_INT, att) \
        /* ... */ \
        X(X_INT, crit)
    
    typedef struct opp
    {
    #define X_INT char *
    #define X_STR int
    #define X(code, name) code name;
        X_FIELDS
    #undef X
    #undef X_INT
    #undef X_STR
    } opp;
    
    extern void update_opp(char** csvSplitRow, opp* opponent);
    
    void update_opp(char** csvSplitRow, opp* opponent)
    {
        int i = 0;
    
    #define X_INT(str)    atoi(str)
    #define X_STR(str)    str
    #define X(converter, name) \
        opponent->name =  converter(csvSplitRow[i++]);
    
        X_FIELDS
    
    #undef X
    #undef X_INT
    #undef X_STR
    }
    

    I'm not 100% convinced this is better because of the multiple #define and #undef operations, but it is more nearly minimal in some respects (it doesn't need the "%d" vs "%s" fields, for example — at least, not in the code shown).