Arrays: Wenn C templates hätte...

… dann wäre C++ womöglich arbeitslos.

Naja, vielleicht nicht ganz, also Spaß bei Seite. Aber generische Programmierung mit Templates zählt für mich neben RAII zu den wichtigsten Features von C++.

Also Frage: Wie löst man dieses Manko in reinem C?

Die beiden üblichen und widerlichen Antworten sind:

  1. MACROs
  2. void*

Macros werden extrem lästig, wenn sie mehrere Codezeilen beinhalten, denn kein mir bekannter Debugger schafft es korrekt in Macros zu “springen”.
Seiteneffekte sind meist auch immer “vorprogrammiert”.

Und mit void* schaltet man natürlich sehr effizient alle Hilfestellungen des Compilers aus. Und dann kann aus einem char* schnell ein int* und umgekehrt werden.

… aber wenn man Glück hat, terminiert einem das OS den Prozess noch bevor kritische Daten wegen Typenfehlern zerstört werden.

Auch die automatische Wahl von Konstruktoren, Kopierkonstruktoren und Destruktoren passend zum gewählten Typen ist in C++ ein riesiger Vorteil.

Ein Beispiel:
Wir wollen ein Array eines beliebigen Typen in einem speziellen Speicherbereich aufbauen.
(Wir ignorieren der Einfachheit halber mal alle Formen von Fehlern und deren Behandlung.)

In C++ ist das leicht:

 1
 2struct foo
 3{
 4  int bar;
 5        
 6  foo()
 7  : bar(42)
 8  {
 9  }
10};
11
12template<typename T> T* create_array_list(size_t count)
13{
14  T* ret = (T*)special_fancy_malloc(sizeof(T) * count);
15  T* current = ret;
16  while(count-- != 0)
17  {
18    /* invoke constructor of T at the current address */
19    new ((void*)current) T();
20    ++current;
21  }
22  return ret;
23}
24
25int main()
26{
27  int* special_buffer = create_array_list<foo>(12);        
28  ...
29}

Und was machen wir in C? Naja, da müssen wir Konstruktoren erst mal generisch machen und für jeden Typen einen händisch schreiben.

 1struct foo
 2{
 3  int bar;
 4};
 5
 6
 7typedef void* (*generic_constructor_t)(void* destaddress);
 8
 9void* create_array_list(size_t count, size_t type_length, generic_constructor_t constructor_function)
10{
11  void* ret = special_fancy_malloc(type_length * count);
12  char* current = (char*)ret;
13  while(count-- != 0)
14  {
15    if(constructor_function != NULL)
16    {
17      if(constructor_function(current) == NULL)
18      {
19        /* C++ would throw an exception */
20      }
21    }
22    else
23    {
24      /* no constructor / plain old data -> no initialization required */
25    }
26    current += type_length;
27  }
28  return ret;
29}
30
31void* foo_constructor(void* address)
32{
33  struct foo* foo_address = (struct foo*)address;
34  foo_address->bar = 42;
35  return address;
36}
37
38int main()
39{
40  int* special_buffer = create_array_list(12, sizeof(struct foo), &foo_constructor);        
41  ...
42}

Dass wir mehr schreiben müssen, stört mich gar nicht mal so sehr. Aber dass die Funktion create_array_list immer die Konstruktor-Funktion als Parameter benötigt, ist schon wesentlich ärgerlicher.

All diese Magie erledigt sonst der C++ Compiler für uns.
Wir können uns sogar die C-artigen Konstruktor-Funktion in C++ automatisch generieren lassen.

 1typedef void* (*generic_constructor_t)(void* destaddress);
 2
 3template<typename T> void* generic_constructor(void* address)
 4{
 5  try
 6  {
 7    new (address) T();
 8    return address;
 9  }
10  catch(...)
11  {
12    return NULL;
13  }        
14}
15
16generic_constructor_t foo_constructor = &generic_constructor<foo>;

Naja … man muss es eben so sehen: C ist einfach eine größere Herausforderung. Aber abseits von sportlichen Herausforderungen ist eines klar:

C++ rulez!


Wenn sich eine triviale Erkenntnis mit Dummheit in der Interpretation paart, dann gibt es in der Regel Kollateralschäden in der Anwendung.
frei zitiert nach A. Van der Bellen
... also dann paaren wir mal eine komplexe Erkenntnis mit Klugheit in der Interpretation!