Generics for multiparameter C functions in C11

前端 未结 5 1327
独厮守ぢ
独厮守ぢ 2020-12-13 07:13

I understand C11 generics for one-parameter functions, like this: (from here)

#define acos(X) _Generic((X), \\
    long double complex: cacosl, \\
    double         


        
5条回答
  •  时光说笑
    2020-12-13 07:58

    Here's a version that only requires you to write a linear amount of code by hand, all of which is directly related to the matter at hand (no large trees of hand-defined types). First, usage example:

    #include 
    
    // implementations of print
    void print_ii(int a, int b) { printf("int, int\n"); }
    void print_id(int a, double b) { printf("int, double\n"); }
    void print_di(double a, int b) { printf("double, int\n"); }
    void print_dd(double a, double b) { printf("double, double\n"); }
    void print_iii(int a, int b, int c) { printf("int, int, int\n"); }
    void print_default(void) { printf("unknown arguments\n"); }
    
    // declare as overloaded
    #define print(...) OVERLOAD(print, (__VA_ARGS__), \
        (print_ii, (int, int)), \
        (print_id, (int, double)), \
        (print_di, (double, int)), \
        (print_dd, (double, double)), \
        (print_iii, (int, int, int)) \
    )
    
    
    #define OVERLOAD_ARG_TYPES (int, double)
    #define OVERLOAD_FUNCTIONS (print)
    
    
    #include "activate-overloads.h"
    
    
    int main(void) {
        print(44, 47);   // prints "int, int"
        print(4.4, 47);  // prints "double, int"
        print(1, 2, 3);  // prints "int, int, int"
        print("");       // prints "unknown arguments"
    }
    

    This is probably the lightest syntax you're going to get for this.

    Now for the disadvantages/restrictions:

    • you need to declare all of the argument types for the overloaded functions in the list OVERLOADED_ARG_TYPES
    • argument types must be one word names (not a huge problem, thanks to typedef, but needs to be remembered)
    • this causes huge code bloat at the actual call site (although it's easy bloat for a compiler to optimize away - GCC will at -O1)
    • relies on a massive PP library (see below)

    You must also define a X_default function which takes no arguments; don't add this to the overload declaration block. This is used for non-matches (if you want to call it directly, call the overload with any non-matching value, like a compound literal anonymous struct or something).

    Here's activate-overloads.h:

    // activate-overloads.h
    #include 
    
    #define ORDER_PP_DEF_8dispatch_overload ORDER_PP_FN( \
    8fn(8N, 8V, \
        8do( \
            8print( 8cat(8(static inline int DISPATCH_OVER_), 8N) ((int ac, int av[]) { return ) ), \
            8seq_for_each_with_idx( \
                8fn(8I, 8T, \
                    8let( (8S, 8tuple_to_seq(8tuple_at_1(8T))), \
                        8print( 8lparen (ac==) 8to_lit(8seq_size(8S)) ), \
                        8seq_for_each_with_idx(8fn(8I, 8T, 8print( (&&av[) 8I (]==) 8cat(8(K_), 8T) )), 0, 8S), \
                        8print( 8rparen (?) 8I (:) ) \
                    )), \
                1, 8V), \
            8print( ( -1; }) ) \
        ) ))
    
    #define TYPES_TO_ENUMS(TS) ORDER_PP ( \
        8do( \
            8seq_for_each(8fn(8T, 8print( 8T (:) 8cat(8(K_), 8T) (,) )), \
                          8tuple_to_seq(8(TS))), \
            8print( (default: -1) ) \
        ) \
    )
    #define ENUMERATE_TYPES(TS) enum OVERLOAD_TYPEK { ORDER_PP ( \
        8seq_for_each(8fn(8V, 8print( 8V (,) )), 8types_to_vals(8tuple_to_seq(8(TS)))) \
    ) };
    #define ORDER_PP_DEF_8types_to_vals ORDER_PP_FN( \
    8fn(8S, 8seq_map(8fn(8T, 8cat(8(K_), 8T)), 8S)) )
    
    
    ENUMERATE_TYPES(OVERLOAD_ARG_TYPES)
    #define OVER_ARG_TYPE(V) _Generic((V), TYPES_TO_ENUMS(OVERLOAD_ARG_TYPES) )
    
    #define OVERLOAD
    ORDER_PP (
        8seq_for_each(
            8fn(8F,
                8lets( (8D, 8expand(8adjoin( 8F, 8(()) )))
                       (8O, 8seq_drop(2, 8tuple_to_seq(8D))),
                    8dispatch_overload(8F, 8O) )),
            8tuple_to_seq(8(OVERLOAD_FUNCTIONS))
        )
    )
    #undef OVERLOAD
    
    #define OVERLOAD(N, ARGS, ...) ORDER_PP ( \
        8do( \
            8print(8lparen), \
            8seq_for_each_with_idx( \
                8fn(8I, 8T, \
                    8lets( (8S, 8tuple_to_seq(8tuple_at_1(8T))) \
                           (8R, 8tuple_to_seq(8(ARGS))) \
                           (8N, 8tuple_at_0(8T)), \
                        8if(8equal(8seq_size(8S), 8seq_size(8R)), \
                            8do( \
                                8print( 8lparen (DISPATCH_OVER_##N) 8lparen 8to_lit(8seq_size(8R)) (,(int[]){) ), \
                                8seq_for_each(8fn(8A, 8print( (OVER_ARG_TYPE) 8lparen 8A 8rparen (,) )), 8R), \
                                8print( (-1}) 8rparen (==) 8I 8rparen (?) 8N 8lparen ), \
                                8let( (8P, 8fn(8A, 8T, \
                                               8print( (_Generic) 8lparen 8lparen 8A 8rparen (,) 8T (:) 8A (,default:*) 8lparen 8T (*) 8rparen (0) 8rparen ) \
                                               )), \
                                    8ap(8P, 8seq_head(8R), 8seq_head(8S)), \
                                    8seq_pair_with(8fn(8A, 8T, 8do(8print((,)), 8ap(8P, 8A, 8T))), 8seq_tail(8R), 8seq_tail(8S)) \
                                ), \
                                8print( 8rparen (:) ) \
                            ), \
                            8print(( )) ) \
                    )), \
                1, 8tuple_to_seq(8((__VA_ARGS__))) \
            ), \
            8print( 8cat(8(N), 8(_default)) (()) 8rparen) \
        ) \
    )
    

    This requires the awesome Order preprocessor library by Vesa K.

    How it actually works: the OVERLOAD_ARG_TYPES declaration is used to build an enum listing all of the argument types in use as constants. Every call to the overloaded name can then be replaced in the caller code by a big ternary operation dispatching between all implementations (of the right argument number). The dispatch works by using _Generic to generate enum values from the types of the arguments, put these in an array, and have an automatically-generated dispatcher function return the ID (position in the original block) for that type combination. If the ID matches, the function is called. If the arguments are of the wrong type, dummy values are generated for the unused call to an implementation to avoid a type mismatch.

    Technically this involves a "runtime" dispatch, but since every type ID is constant and the dispatcher function is static inline, the whole thing should be easy for a compiler to optimize out, except for the wanted call (and GCC does indeed optimize it all away).

    This is a refinement of the technique previously posted here (same idea, now with pretty and ultra-light syntax).

提交回复
热议问题