C is a low-level language, nearly a portable assembler, so its data structures and language constructs are close to the metal (data structures have no hidden costs - except padding, alignment and size constraints imposed by hardware and ABI). So C indeed does not have dynamic typing natively. But if you need it, you could adopt a convention that all your values are aggregates starting with some type information (e.g. some enum
...); use union
-s and (for array-like things) flexible array member in struct
containing also the size of the array.
(when programming in C, it is your responsibility to define, document, and follow useful conventions - notably pre- and post- conditions and invariants; also C dynamic memory allocation requires expliciting conventions about who should free
some heap-malloc
ated memory zone)
So, to represent values which are boxed integers, or strings, or some kind of Scheme-like symbol, or vectors of values, you'll conceptually use a tagged union (implemented as a union of pointers) -always starting by the type kind-, e.g.:
enum value_kind_en {V_NONE, V_INT, V_STRING, V_SYMBOL, V_VECTOR};
union value_en { // this union takes a word in memory
const void* vptr; // generic pointer, e.g. to free it
enum value_kind_en* vkind; // the value of *vkind decides which member to use
struct intvalue_st* vint;
struct strvalue_st* vstr;
struct symbvalue_st* vsymb;
struct vectvalue_st* vvect;
};
typedef union value_en value_t;
#define NULL_VALUE ((value_t){NULL})
struct intvalue_st {
enum value_kind_en kind; // always V_INT for intvalue_st
int num;
};
struct strvalue_st {
enum value_kind_en kind; // always V_STRING for strvalue_st
const char*str;
};
struct symbvalue_st {
enum value_kind_en kind; // V_SYMBOL
struct strvalue_st* symbname;
value_t symbvalue;
};
struct vectvalue_st {
enum value_kind_en kind; // V_VECTOR;
unsigned veclength;
value_t veccomp[]; // flexible array of veclength components.
};
To get the dynamic type of some value
enum value_kind_en value_type(value_t v) {
if (v.vptr != NULL) return *(v.vkind);
else return V_NONE;
}
Here is a "dynamic cast" to vectors:
struct vectvalue_st* dyncast_vector (value_t v) {
if (value_type(v) == V_VECTOR) return v->vvect;
else return NULL;
}
and a "safe accessor" inside vectors:
value_t vector_nth(value_t v, unsigned rk) {
struct vectvalue_st* vecp = dyncast_vector(v);
if (vecp && rk < vecp->veclength) return vecp->veccomp[rk];
else return NULL_VALUE;
}
You'll typically define most of the short functions above as static inline
in some header file.
BTW, if you can use Boehm's garbage collector you are then able to code quite easily in some higher-level (but unsafe) style, and several Scheme interpreters are done that way. A variadic vector constructor could be
value_t make_vector(unsigned size, ... /*value_t arguments*/) {
struct vectvalue_st* vec = GC_MALLOC(sizeof(*vec)+size*sizeof(value));
vec->kind = V_VECTOR;
va_args args;
va_start (args, size);
for (unsigned ix=0; ix<size; ix++)
vec->veccomp[ix] = va_arg(args,value_t);
va_end (args);
return (value_t){vec};
}
and if you have three variables
value_t v1 = somevalue(), v2 = otherval(), v3 = NULL_VALUE;
you could build a vector from them using make_vector(3,v1,v2,v3)
If you don't want to use Boehm's garbage collector (or design your own one) you should be very careful about defining destructors and documenting who, how, and when memory should be free
-d; see this example. So you could use malloc
(but then test against its failure) instead of GC_MALLOC
above but you need to carefully define and use some destructor function void destroy_value(value_t)
The strength of C is to be low-level enough to make code like above possible and define your own conventions (particular to your software).
virtual
member function. So RTTI never increases the size of any objects, it only makes the binary larger by a constant.T *
is always the same size and theT
may contain a hidden field that points to the vtable. And no C++ compiler ever inserted vtables into objects that don't need them.