4

I'm trying to make an implementation of a Vector in C (yes, I know, this has been done to death), generic across any types used. So far I have an implementation something like:

#define Vector(T) struct Vector_##T

// other routines, initialization, destruction, ...
// an example routine
#define DEFINE_VectorFirst(T)                                                  \
  T VectorFirst_##T(Vector(T) vec) {                                           \
    return vec.data[0];                                                        \
  }
#define DEFINE_VECTOR(T)                                                       \
  struct Vector_##T {                                                          \
    T *data;                                                                   \
    size_t len;                                                                \
    size_t capacity;                                                           \
  };                                                                           \
  DEFINE_VectorFirst(T)

// a few types for now
DEFINE_VECTOR(int)
DEFINE_VECTOR(double)

so I can, for example, declare a Vector(double) and then call VectorFirst_double on it. Ideally I'd like to not have to specify the type name every time, so I can use a generic:

#define GENERIC(F, T) Vector(T) : F##_##T
#define VectorFirst(v)                                                         \
  _Generic((v), GENERIC(VectorFirst, int), GENERIC(VectorFirst, double))(v)

and now VectorFirst automatically calls the right version, but this loses the nice property that I can DEFINE_VECTOR in a bunch of different header files. In this way, the original version is "extensible", letting me make vectors of whatever structs I might declare elsewhere, but in the generic one I need to know all of the vector types I might ever need at one spot. I could just use macros instead of functions for some things (#define VectorFirst(v) v.data[0] or the like) but it seems like this would make debugging annoying and maybe be slow for larger functions. Are there any preprocessor techniques that can be used to restore this extensibility behavior and let me add new entries to a _Generic somewhere else? This is a small-scale test so I'm okay with whatever awful preprocessor hacks people know of.

9
  • 1
    "across any types" --> I suspect #define GENERIC(F, T) Vector(T) : F##_##T will have trouble with type long long (types with a space in them). Your 1st approach apparently does not have that issue. Commented Apr 2 at 17:56
  • 1
    ... which could be worked around by use of typedefs, but that's still a fairly nasty gotcha. Commented Apr 2 at 18:01
  • 1
    Pointer types like int* deserve testing too. (@JohnBollinger and hiding a pointer type in a typedef is an antipattern) Commented Apr 2 at 18:03
  • @Josh Op, I think I would go the with a predefined set of types rather than any type. To fully handle any type you may have to do something like what Stroustrup did. Commented Apr 2 at 18:13
  • Presumably you use C because you don't want the compiler to generate stuff for you, or have different functions use the same name (overloads). Trying to force that into the program anyway, using generic macros, is just going to make you cry. Like other comments say, Bjarne tried that 40-50 years ago, and soon gave up. It doesn't work. Commented Apr 2 at 18:45

3 Answers 3

3

Are there any preprocessor techniques that can be used to restore this extensibility behavior and let me add new entries to a _Generic somewhere else?

Not as such. But you have to understand that _Generic is a (weird) operator. Every appearance is separate, including every instance resulting from expansion of a macro. There is no meaningful sense of "elsewhere" in which one could add entries to a particular generic selection expression.

Macros, on the other hand, can be defined and undefined fairly freely. You don't have to have a common definition that is shared everywhere. "Somewhere else" can provide their own definition that suits them.

You could perhaps facilitate each translation unit defining a suitable set of type-generic macros by leveraging X macros. It might look something like this:

generic_vector.h

// ...

// A suitable GENERIC_OPTS definition must be in scope already
#ifndef GENERIC_OPTS
    #error no definition of GENERIC_OPTS
#endif

#define GENERIC(F, T) Vector(T) : F##_##T

#define VF_DECL(T) , GENERIC(VectorFirst, T)
#define VectorFirst(v)       \
  _Generic((v)               \
    GENERIC_OPTS(VF_DECL)    \
  )(v)

// ... other operations ...

other.c

#define GENERIC_OPTS(G) \
  G(int) \
  G(double) \
  G(uint64_t)

#include "generic_vector.h"

#undef GENERIC_OPTS
// ...

There, the definition of GENERIC_OPTS in other.c controls what element types can be used with the VectorFirst() macro as defined in that file. If you need to generate function definitions as well, then they can be accommodated the same way, but you will want to declare them with internal linkage (i.e. static) if each translation unit is defining its own.


Do be aware, however, that C data type names can be arbitrarily complex. Even among just the (unqualified) standard arithmetic types, a majority have multi-word names, as @chux alluded to in comments. Your GENERIC() macro for building type-specific identifiers from type names will work as you intend only for type names that are single identifiers.

You can work around that by defining typedefs for types whose names are not a single identifier, but that gets nasty fast. If you really want to pursue this idea, then you should consider writing a full-fledged code generator instead of relying on the limited capabilities of the C preprocessor.

Sign up to request clarification or add additional context in comments.

6 Comments

Actually, it is possible to have extensible generics in C with some deep preprocessor magic. See github.com/JacksonAllan/CC/blob/main/articles/….
What "actually", @tstanisl? This answer already shows that it is possible to have extensible generics in C even with not-all-that-deep preprocessor magic. The usage model demonstrated at the end is comparable in complexity to that presented in the article you reference. But that doesn't necessarily mean it's a good idea, and the OP's particular problem and other aspects of their approach present some complications.
The proposed solution can be "extended" only once. The solution in the link allows extending generic multiple times by re-including the header.
Moreover, I'm not sure how VectorFirst is going to work because it relies on expanding VF_DECL which is #undef-ed
Not only that, @tstanisl, but in that approach, hash.h must be included separately for each type to be supported. These differences in usage mode are not altogether inconsequential, but they are pretty minor.
|
1

Are there any preprocessor techniques that can be used to restore this extensibility behavior and let me add new entries to a _Generic somewhere else?

Yes, you can.

You can #undef and #define one macro over and over again inside an #if. This lets you implement state. I'll try to explain the method below using the provided VectorFirst. It could look like the following. We add a "SLOTS" variable to it that expands to nothing. The "adding" creates a new variable type and also defines a new function. With __GNUC__ extensions like __typeof__ or __attribute__((__alias__)) or C23 typeof might be helpful to users.

 // vector.h
 #define VectorFirst(v)  \
      _Generic((v) \
      , GENERIC(VectorFirst, int) \
      , GENERIC(VectorFirst, double) \
      VECTOR_FIRST_SLOTS() \
      )(v)

 #define VECTOR_FIRST_SLOTS()  /* empty */
 #define VECTOR_SLOTS_COUNTER  0 // 

 #define CONCAT(a, b)   a##b
 #define XCONCAT(a, b)  CONCAT(a, b)
 #define VECTOR_FIRST_ADD(TYPE, CB)  \
    /* define type named VECTOR_FIRST_TYPE_N as an alias to TYPE */ \
    typedef TYPE XCONCAT(VECTOR_FIRST_TYPE_, VECTOR_SLOTS_COUNTER); \
    /* define function named VECTOR_FIRST_CB_N as an alias to CB */ \
    static inline TYPE XCONCAT(VECTOR_FIRST_CB_, VECTOR_SLOTS_COUNTER)(Vector(TYPE) t) { \
         return CB(t); \
    }

Then create a header file like the following. This header file, depending on the VECTOR_SLOTS_COUNTER variable value, will add additional values to VECTOR_FIRST_SLOTS. I typically use M4 or Jinja2 to generate this file.

// vector_add.h
#ifndef VECTOR_SLOTS_COUNTER
  #error VECTOR_SLOTS_COUNTER not defined
#elif VECTOR_SLOTS_COUNTER == 0
  #undef VECTOR_SLOTS_COUNTER
  #undef VECTOR_FIRST_SLOTS
  #define VECTOR_SLOTS_COUNTER 1
  #define VECTOR_FIRST_SLOTS() \
       , VECTOR_FIRST_TYPE_1: VECTOR_FIRST_CB_1

#elif VECTOR_SLOTS_COUNTER == 1
  #undef VECTOR_SLOTS_COUNTER
  #undef VECTOR_FIRST_SLOTS
  #define VECTOR_SLOTS_COUNTER 2
  #define VECTOR_FIRST_SLOTS() \
       , VECTOR_FIRST_TYPE_1: VECTOR_FIRST_CB_1 \
       , VECTOR_FIRST_TYPE_2: VECTOR_FIRST_CB_2

#elif VECTOR_SLOTS_COUNTER == 2
  #undef VECTOR_SLOTS_COUNTER
  #undef VECTOR_FIRST_SLOTS
  #define VECTOR_SLOTS_COUNTER 3
  #define VECTOR_FIRST_SLOTS() \
       , VECTOR_FIRST_TYPE_1: VECTOR_FIRST_CB_1 \
       , VECTOR_FIRST_TYPE_2: VECTOR_FIRST_CB_2 \
       , VECTOR_FIRST_TYPE_3: VECTOR_FIRST_CB_3

#else
  #error too many slots - generate more cases
#endif

Now the user can do the following:

 #include <vector.h>

 float vector_first_float(Vector(float) vec); // forward declaration

 VECTOR_FIRST_ADD(float, vector_first_float)
 #include <vector_add.h>  // reassigns VECTOR_FIRST_SLOTS and increments VECTOR_SLOTS_COUNTER

 double vector_first_double(Vector(double) vec);

 VECTOR_FIRST_ADD(double, vector_first_double)
 #include <vector_add.h>

 ...
     VectorFirst(v) // now expands witht he new VECTOR_SLOTS_COUNTER
    

The VECTOR_FIRST_ADD and inclusion of vector_add.h header file can be used by the user in their header files. It makes the code base "dynamically" aware of available types depending on which header files users will include.

I think the VectorFirst does not make a really good example here, as "vector" will have many more operations, but I hope it showcases the usage. I would rather expect that the user defines several callbacks for each vector type, and some VECTOR_ADD(TYPE, PREFIX) macro file would allow user to provide the vector underlying type and a prefix to all required functions with predefined naming convention, like PREFIX##_first PREFIX##_get etc.

You might be interested in other type generic libraries in C, like https://github.com/stclib/STC or https://github.com/JacksonAllan/CC . The method is based on https://github.com/JacksonAllan/CC/blob/main/articles/Better_C_Generics_Part_1_The_Extendible_Generic.md . Example from my unused library ADD SLOTS example.

Comments

0

I'm the author of the article (and library) to which tstanisl and KamilCuk linked earlier.

A good example of integrating the approach described in the article with the pseudo-template approach (which appears to be what you're trying to achieve) is not CC but Verstable (which isn't mentioned in the article because it didn't exist at the time I wrote it). Specifically, we are looking at this section and this section of its header. The trick is to generate specialized functions with prefixed names in addition to wrapper functions to plug into the unified _Generic-based macros.

This approach has the advantage of also exposing type-specific, prefixed functions for users who can't compile with C11 or want to e.g. get function pointers to type-specific functions when necessary. Moreover, although Verstable is a dedicated hash-table library, the same approach can be used to provide a generic API agnostic both to datatype(s) and to container type, yielding macros like Insert and First rather than VectorInsert and VectorFirst.

All the wrapper functions can be generated automatically (again, see Verstable's code) when the user creates/instantiates a new container type by including the container header. This is nicer than what we see in KamilCuk's example, which seems to require the user to manually instantiate each vector function (if I understood him or her correctly).

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.