How to elegantly implement a series of functions in different type versions using pure C?

后端 未结 5 661
北恋
北恋 2020-12-30 09:53

I want to write several functions that are only different in the types of arguments. I know C++ has template to handle this problem well (not very well yet thou

相关标签:
5条回答
  • 2020-12-30 10:17

    If you think using the C preprocessor is awkward and hard to debug, how about writing a script in some more convenient language to generate a .c file you can #include? Most modern scripting languages come with some sort of template engine, but since your requirements are simple enough, it doesn't have to be any more complex than this;

    #/bin/sh
    for t in int double char mytype; do
        cat <<____HERE
        $t add_$t ($t a, $t b) {
            return (a + b);
        }
    ____HERE
    done >generated.c
    

    The resulting file will be plain-jane straightforward C which should be reasonably simple to debug and change.

    0 讨论(0)
  • 2020-12-30 10:21

    I would also propose Solution 4: write a code generation tool.

    Pros:

    • result is a clean debuggable code;
    • unlimited configurability to your needs (if you have time of course);
    • long-term investment to a dev's toolset.

    Cons:

    • takes some time, esp. at start, not always sutable for write-once code;
    • complicates build process a bit.
    0 讨论(0)
  • 2020-12-30 10:23

    In many (if not most) cases the best way to simulate C++ templates in C would be Solution 3: parametrized header file and parametrized implementation file. In your case it would work as follows

    1. Create a meta-header file, which we'll name add.dec, that looks as follows

      TYPE_ CONCAT(add, SUFFIX_)(TYPE_ a, TYPE_ b);
      TYPE_ CONCAT(sub, SUFFIX_)(TYPE_ a, TYPE_ b);
      
    2. Create a meta-implementation file, which we'll name add.def, that looks as follows

      TYPE_ CONCAT(add, SUFFIX_)(TYPE_ a, TYPE_ b){
        return a + b;
      }
      
      TYPE_ CONCAT(sub, SUFFIX_)(TYPE_ a, TYPE_ b){
        return a - b;
      }
      

    These two files are parametrized by two macros: TYPE_ and SUFFIX_, while CONCAT is a traditional implementation of macro concatenation

    #define CONCAT_(a, b) a##b
    #define CONCAT(a, b) CONCAT_(a, b)
    

    Now, imagine you want to instantiate your "template" functions for types int and double. In a "real" header file add.h you simply do

    #define TYPE_ int
    #define SUFFIX_ Int
    #include "add.dec"
    #undef TYPE_
    #undef SUFFIX_
    
    #define TYPE_ double
    #define SUFFIX_ Double
    #include "add.dec"
    #undef TYPE_
    #undef SUFFIX_
    

    and in a "real" implementation file add.c you do

    #define TYPE_ int
    #define SUFFIX_ Int
    #include "add.def"
    #undef TYPE_
    #undef SUFFIX_
    
    #define TYPE_ double
    #define SUFFIX_ Double
    #include "add.def"
    #undef TYPE_
    #undef SUFFIX_
    

    That's it. By doing this you instantiated (declared and defined) addInt, addDouble, subInt and subDouble.

    Of course, you can parametrize the declarations much more. You can add a DECLSPEC_ parameter to be able to declare your sunctions as static, if necessary. You can specify different types for parameters and return values (say, ARG_TYPE_ and RET_TYPE_). You can parametrize lots of other things. Basically, there's no limit to what you can parametrize. With some fairly easy macro techniques you can even parametrize the number of parameters your functions expect.

    This is actually similar to your Solution 1 and Solution 2 combined. This basically takes the best from both of your approaches. And I'd say that this is the most faithful attempt to simulate the behavior of C++ template instantiation.

    Note that each function's body is explicitly typed only once (as opposed to multiple explicit copies in your Solution 1). The actual function bodies are also easily editable, since there's no need to worry about those pesky \ at the end of each line (as is the case in your Solution 2).

    This approach has another interesting benefit: the code in add.def will remain "debuggable", i.e. an ordinary interactive debugger will typically be able to step into these implementations (which is impossible in your Solution 2).

    0 讨论(0)
  • 2020-12-30 10:27

    I don't think you can do much better than your solution 2 in pure C.

    0 讨论(0)
  • 2020-12-30 10:31

    You can use union:

    #include <stdio.h>
    #include <stdarg.h>
    
    typedef enum {Int, Double} NumberType;
    
    typedef struct {
      NumberType type;
      union {
        int i;
        double d;
      };
    } Number;
    
    Number addNumber(Number a, Number b) {
      Number ret;
      Number *numbers[] = {&a, &b};
      if (a.type == Int && b.type == Int ){
        ret.type = Int;
        ret.i = a.i + b.i;
      }
      else {
        ret.type = Double;
        char i;
        for (i = 0; i < 2 && numbers[i]->type == Int; i++) {
          numbers[0]->d = (double) numbers[i]->i;
        }
        ret.d = a.d + b.d;
      }
      return ret;
    }
    
    Number newNumber(NumberType type, ...) {
      va_start(ap, type);
      Number num;
      num.type = type;
      switch (type) {
        case Int: {
          num.i = va_arg(ap, int);
          break;
        }
        case Double: {
          num.d = va_arg(ap, double);
          break;
        }
        default: { /* error */
          num.type = -1;
        }
      }
      va_end(ap);
      return num;
    }
    
    
    int main(void) {
      Number a = newNumber(Int, 1);
      Number b = newNumber(Double, 3.0);
      Number ret = addNumber(a, b);
      switch (ret.type) {
        case Int: {
          printf("%d\n", ret.i);
        }
        case Double: {
          printf("%f\n", ret.d);
        }
      }
      return 0;
    }
    
    0 讨论(0)
提交回复
热议问题