9

One thing that always intuitively struck me as a positive feature of C (well, actually of its implementations like gcc, clang, ...) is the fact that it does not store any hidden information next to your own variables at runtime. By this I mean that if you for example wanted a variable "x" of the type "uint16_t", you could be sure that "x" will only occupy 2 bytes of space (and won't carry any hidden information like its type etc.). Similarly, if you wanted an array of 100 integers, you could be sure it is as big as 100 integers.

However, the more I am trying to come up with concrete use cases for this feature the more I am wondering if it actually has any practical advantages at all. The only thing I could come up with so far is that it obviously needs less RAM. For limited environments, like AVR chips etc., this is definitely a huge plus, but for everyday desktop / server use cases, it seems to be rather irrelevant. Another possibility I am thinking of is that it might be helpful / crucial for accessing hardware, or maybe mapping memory regions (for example for VGA output and the like) ... ?

My question: Are there any concrete domains that either can't or can only very cumbersomely be implemented without this feature?

P.S. Please tell me if you have a better name for it! ;)

18
  • 1
    recommended reading: Why do 'some examples' and 'list of things' questions get closed?
    – gnat
    Commented Jan 16, 2016 at 20:19
  • @gnat I think I understand what your problem is. It's because there could be multiple answers, right? Well, I get that this question might not suit the way stackexchange works, but I honestly don't know where to ask elsewise ... Commented Jan 16, 2016 at 20:29
  • 1
    @lxrec RTTI is stored in the vtable, and objects only store a pointer to the vtable. Additionally, types only have RTTI if they already have a vtable because they have a virtual member function. So RTTI never increases the size of any objects, it only makes the binary larger by a constant.
    – user7043
    Commented Jan 16, 2016 at 20:37
  • 3
    @ThomasOltmann Every object that has virtual methods needs a vtable pointer. You can't have the functionality virtual methods without that. Moreover, you explicitly opt into having virtual methods (and hence, a vtable).
    – user7043
    Commented Jan 16, 2016 at 20:50
  • 1
    @ThomasOltmann You seem very confused. It's not a pointer to an object that carries a vtable pointer, it's the object itself. I.e., T * is always the same size and the T may contain a hidden field that points to the vtable. And no C++ compiler ever inserted vtables into objects that don't need them.
    – user7043
    Commented Jan 16, 2016 at 20:58

3 Answers 3

5

There are several benefits, the obvious one is at compile time to ensure that things like function parameters match the values being passed in.

But I think you are asking about what is happening at runtime.

Keep in mind that the compiler will create a runtime that embeds knowledge of the data types in the operations it performs. Each chunk of data in memory may not be self describing, but the code inherently knows what that data is (if you have done your job correctly).

At runtime things are a bit different than you think.

For example, don't assume that only two bytes are used when you declare uint16_t. Depending on the processor and word alignment it can occupy 16, 32 or 64 bits on the stack. You may find that your array of shorts consumes much more memory than you expected.

This can be problematic in certain situations where you need to reference data at specific offsets. This happens when communicating between two systems that have different processor architectures, either via a wireless link, or via files.

C allows you to specify structs with bit level granularity:

struct myMessage {
  uint8_t   first_bit: 1;
  uint8_t   second_bit: 1;
  uint8_t   padding:6;
  uint16_t  somethingUseful;
}

This structure is three byte long, with a short defined to start at an odd offset. It will also need to be packed in order to be exactly as you defined it. Otherwise the compiler will word-align the members.

The compiler will generate code behind the scenes to extract this data and copy into a register so you can do useful things with it.

Now you can see that every time my program accesses a member of the myMessage struct, it will know how exactly how to extract it and operate on it.

This can become problematic and hard to manage when communicating between different systems with different versions of software. You must carefully design the system & code to ensure both sides have exactly the same definition of the data types. This can be quite challenging in some environments. This is where you need a better protocol that contains self describing data such as Google's Protocol Buffers.

Lastly, you make a good point to ask how important this is in the desktop/server environment. It really depends on how much memory you are planning to use. If you are doing something like image processing, you may end up using a large amount of memory which may affect the performance of your application. This is definitely always a concern in the embedded environment where memory is restricted and there is no virtual memory.

2
  • 2
    "You may find that your array of shorts consumes much more memory than you expected." This is wrong in C: Arrays are guaranteed to contain their elements in a gap-free fashion. Yes, the array needs to be aligned properly, as does a single short. But this is a one-time requirement for the start of the array, the rest is automatically aligned correctly by virtue of being consecutive. Commented Jan 31, 2016 at 20:01
  • Also, the syntax for the padding is wrong, it should be uint8_t padding: 6;, just like the first two bits. Or, more clearly, just the comment //6 bits of padding inserted by the compiler. The structure, as you have written it, has a size of at least nine bytes, not three. Commented Jan 31, 2016 at 20:07
9

You hit on one of the only reasons this is useful: mapping external data structures. Those include memory-mapped video buffers, hardware registers, etc. They also include data transmitted intact outside the program, like SSL certificates, IP packets, JPEG images, and pretty much any other data structure that has a persistent life outside the program.

5

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-mallocated 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).

4
  • I think you misunderstood my question. I don't want dynamic typing in C. I was curious if this specific property of C is of any practical use. Commented Jan 17, 2016 at 10:47
  • But what exact property of C are you refering to? C data structures are close to the metal, so have no hidden costs (except alignment and size constraints) Commented Jan 17, 2016 at 10:51
  • Exactly that :/ Commented Jan 17, 2016 at 13:01
  • C was invented as a low-level language, but when optimizations are turned on compilers like gcc process a language which uses the low-level syntax but doesn't reliably provide low-level access to platform-provided behavioral guarantees. One needs sizeof to use malloc and memcpy, but use for fancier address calculations may not be supported in "modern" C.
    – supercat
    Commented Mar 7, 2017 at 18:36

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.