Search code examples
cdesign-patternsinterfaceabstract-data-type

How to create an interface in C that can work on two identical structs with differently named fields


Question Background

Consider a scenario in which I have two structs. Both consist of three fields for doubles. The only difference is the names used to refer to these fields. The first struct is for non-descript Tuples and the components are to be named x, y, and z. The second struct is for RGB values and the components are to be named red, green, and blue. Both of these structs need to have an interface defined for them such that operations such as vector/componentwise addition and scalar multiplication can be performed.

The Question

The problem? It seems terribly inelegant to copy and paste identical code for both of these structs only to change the naming scheme (x vs. red, y vs. green, z vs. blue). Ideally it would be possible in the interface function definitions to reference the fields of the struct (x or red) without having to name the field. Is this possible? If not then what other options are avaiable to me without changing the requirements? I include the struct definitions below along with some examples of their desired use with the possibly impossible interface I describe in this question.

Non-descript Tuple Struct

typedef struct
{
    /** The first coordinate */
    double x;

    /** The second coordinate */
    double y;

    /** The third coordinate */
    double z;

} Tuple;

RGB Tuple Struct

typedef struct
{
    /** The first coordinate */
    double red;

    /** The second coordinate */
    double green;

    /** The third coordinate */
    double blue;
} Tuple;

Example Usage

Tuple * genericTuple = createVector( 1, 2, 3 ); // create a generic Tuple
printf("%lf", genericTuple->x); // should print 1

Tuple * rgbTuple = createColor( 1, 2, 3 ); // create an rgbTuple
printf("%lf", rgbTuple->red); // should also print 1

Solution

  • Your types are in conflict

    If you are trying to use your two different kinds of Tuple in the same function, you aren't allowed to do that. It would fail to compile because the second typedef would be in conflict with the first.

    For the sake of argument, lets say you are willing to give the two different types different names, but you want some code to somehow treat them as if they were the same type. One design pattern that allows this to happen would be the Adapter pattern.

    Adaptor answers your question

    Although you pose your question in the form of creating an interface, what you illustrated was two different interfaces, and you wanted both interfaces to work even when passed conflicting types. That would not be good software engineering practice.

    Instead, what you should do to avoid "copy-and-paste" coding would be to figure out how to create or reuse common code that will work with your specific use case. This can either be through re-factoring commonalities from the specific cases, or by specializing a more general interface to your use case. Both can be achieved by applying Adapter.

    I provide illustrations of the two directions of application below. However, keep in mind that they are merely illustrations, and that there are multiple ways to apply the pattern.

    Adapt a Tuple from a specific use case

    Suppose you have a specific type to represent RGB values.

    struct RGB {
        double red;
        double green;
        double blue;
    };
    

    It is possible to create a Tuple interface to allow arbitrary use cases to be treated the same way. For example, we could use offsets.

    struct Tuple {
        const size_t (*adaptor)[3];
        void *tuple;
    };
    
    struct Tuple make_tuple (void *tuple, const size_t (*adaptor)[3]) {
        return (struct Tuple){adaptor, tuple};
    }
    
    void print_tuple (struct Tuple t) {
        for (int i = 0; i < 3; ++i) {
            printf("%lf\n", *(double *)((char *)t.tuple + (*t.adaptor)[i]));
        }
    }
    

    And you could then use it like this:

    const size_t RGB_to_Tuple_Adaptor[3] = {
        offsetof(struct RGB, red),
        offsetof(struct RGB, green),
        offsetof(struct RGB, blue)
    };
    
    void print_RGB (struct RGB *rgb) {
        print_tuple(make_tuple(rgb, &RGB_to_Tuple_Adaptor));
    }
    

    Adapt a specific use case from a Tuple

    Suppose instead you have a Tuple defined like this.

    struct Tuple {
        double tuple[3];
    };
    

    Then, you would have a print function that looked like this:

    void print_tuple (struct Tuple *t) {
        for (int i = 0; i < 3; ++i) {
            printf("%lf\n", t->tuple[i]);
        }
    }
    

    To use Tuple for your use case, you could define RGB as so:

    struct RGB {
        struct Tuple tuple;
    };
    
    #define RGB_red(rgb)   RGB_to_Tuple(rgb)->tuple[0]
    #define RGB_green(rgb) RGB_to_Tuple(rgb)->tuple[1]
    #define RGB_blue(rgb)  RGB_to_Tuple(rgb)->tuple[2]
    
    #define RGB_to_Tuple(rgb) (&(_Generic(&(rgb), \
               struct RGB * : (&(rgb)),           \
               struct RGB **: (rgb),              \
                     default: 0)->tuple))
    

    And then you could use it like this:

    void print_RGB (struct RGB *rgb) {
        print_tuple(RGB_to_Tuple(rgb));
    }
    
    int main () {
        struct RGB rgb;
    
        RGB_red(rgb) = 1;
        RGB_green(rgb) = 2;
        RGB_blue(rgb) = 3;
    
        print_RGB(&rgb);
    }