Letzte relevante Änderung: 03.08.2015

C++-Programmiertipps

Auf dieser Seite soll auf eine Reihe von Aspekten der Programmierung in C bzw. C++ eingegangen werden, die bei der Programmierung aus Stil-, Performance- und anderen Gründen beachtet werden sollten. Es handelt es sich hierbei nicht um ein Programmier-Tutorial. Es handelt sich lediglich um eine Sammlung von Tipps, Erklärungen und Codeschnippseln, die ich für nützlich halte. Für ein Tutorial zum Thema "Optimierung von Programmen" siehe hier.

Inhaltsverzeichnis:

Funktionspointer bei Klassenmembern

C++, C++11, Funktionspointer

Funktionspointer sehen zwar böse aus (z. B.: void (*funcptr)(int, float, void*);), aber sie sind oft sehr nützlich, um universelle Schnittstellen zu schaffen, oder um Callback-Mechanismen zu realisieren. Da sie aber aus der Sprache C übernommen wurden, scheint ihre Verwendung gemeinsam mit C++-Sprachkonstrukten gewissen Beschränkungen unterworfen. Wer z. B. versucht, einen Zeiger auf eine (nicht-statische) Memberfunktion einer Klasse zu erzeugen, wird mit diesem intuitiven Code scheitern:

struct mystruct {
	void Memberfunc();
};

void (*funcptr)(); // Funktionspointer einer Funktion, die nichts zurückgibt und keine Parameter nimmt.
funcptr = &mystruct::Memberfunc;

Ursache für dieses Verhalten ist, dass in einer nicht-statischen Memberfunktion der this-Zeiger definiert ist und man darum auf die Membervariablen der Instanz der Klasse zurückgreifen kann, von der aus die Funktion aufgerufen wurde. Ein Funktionspointer des o. g. Typs würde man allerdings so aufrufen; die Instanz wäre dabei unbekannt:

(*ptr)();

Für Memberfunktionszeiger kann man zwar Folgendes schreiben, allerdings ist dies nicht besonders flexibel, da es an eine bestimmte Klasse gebunden und daher die Signatur der Funktion von der zuvor Genannten verschieden ist:

void (mystruct::*funcptr)() = &mystruct::Memberfunc; // Definition, Initialisierung
mystruct ms;                                         // Wir brauchen (wie oben angedeutet) eine Instanz der Klasse.
(ms.*funcptr)();                                     // Aufruf der Funktion mit der Instanz ms.

Für dieses Problem gibt es mit dem neuen Standard C++11 eine Lösung: std::function und std::bind():

#include <functional>

std::function<void(void)> funcptr;               // Funktionspointer auf eine Funktion ohne Parameter und Rückgabewert
mystruct ms;                                     // Wir brauchen weiterhin eine Instanz der Klasse.
funcptr = std::bind(&mystruct::Memberfunc, &ms); // Wir erzeugen einen Funktionspointer, der an die Instanz ms gebunden ist.
funcptr();                                       // Der Aufruf erfolgt nun wie bei einem normalen Funktionspointer vom Typ void (*)(void)
                                                 // Beim Aufruf der Funktion, auf die funcptr zeigt, wird ms als Instanz verwendet.

Auch komplexere Probleme sind durch std::bind lös- und beherrschbar geworden:

#include <functional>

struct mystruct {
	void Memberfunc(int, std::string);
};

mystruct ms;
std::function<void(int)> funcptr = std::bind(&mystruct::Memberfunc, &ms, std::placeholders::_1, "blabla");

In diesem Beispiel wird der Parameter des Funktionspointers als erster Parameter an Memberfunc weitergegeben, an den zweiten Parameter von mystruct::Memberfunc wird "blabla" gebunden, d. h. bei jedem Aufruf der Funktion über den Funktionszeiger übergeben.

Eine gesamte Datei auslesen

C++, Dateien

Unter Umständen kann es nötig sein, eine ganze Datei auf einen Schlag auszulesen und im Speicher zu sichern. Die Lösung hierfür sind Iteratoren:

std::ifstream ifs("myfile");
std::string destination((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());

Die zusätzlichen Klammern um den ersten Parameter des Konstruktors sind bei manchen Compilern notwendig, damit dieser Code nicht als Funktionsdeklaration fehlinterpretiert wird.

Deklaration, Definition, Initialisierung

C, C++

Für Funktionen:

void Func();    // Deklaration. Es wird kein Code generiert, aber der Name der Funktion ist damit bekannt gemacht.
void Func() {}  // Definition/Implementation. Die Funktion existiert nun. Diese Definition ist zugleich eine Deklaration.

Für Variablen:

extern int i;  // Deklaration. Wie bei Funktionen wird nur der Name bekannt gemacht, aber kein Objekt erzeugt
int i;         // Definition. Die Variable existiert damit im Speicher. Sie ist zugleich eine Deklaration.
int i = 0;     // Initialisierung. Die Variable exisitert im Speicher und hat zu Beginn den Wert 0. Sie
               // ist zugleich Deklaration und Definition.

Während das Thema Deklarationen, Initialisierungen und Definitionen bei Funktionen eher harmlos ist, kann es bei Variablen Probleme geben. Wenn man in einen Header eine Definition (also int i; oder int i = 0;) schreibt, werden in jeder .c/.cpp-Datei, die diesen Header inkludieren, jeweils eine dieser Variablen erzeugt. Ab dem Punkt gibt es zwei Möglichkeiten, was passieren kann:

  1. Wenn die Variable zugleich static ist, entstehen auch beim Linken mehrere Variablen, nämlich pro Übersetzungseinheit eine, die jeweils unterschiedliche Werte haben können.
  2. Wenn sie nicht static ist, hängt es vom Linker ab, was passiert. GNU ld warnt nicht, gibt auch keinen Fehler aus, sondern baut eine gemeinsame Variable. Der Linker vom MSVC hingegen gibt grundsätzlich einen Linkerfehler aus, dass etwas mehrfach definiert wurde.

Es kann also, Probleme geben, wenn man eine Definition in einen Header schreibt, wenn man nicht genau weiß, was man tut. Um auch den GCC, bzw. GNU ld zu zwingen, einen Fehler zu generieren (was an dieser Stelle empfohlen sei, um Fehler zu vermeiden), sollte man diese Option angeben: --warn-common

Rückgabettypen überladen

C++, Templates

In C++ wird, im Gegensatz zu C, das Überladen von Funktionen ermöglicht:

void Func(int);
void Func(float);
int Func(void*);

Wie Sie sehen, ist es offenbar möglich, bei der Überladung der Parameter auch den Rückgabetyp zu ändern. Dies ist aber gewissen Beschränkungen unterworfen. Folgendes geht nicht:

void Func(int);
int Func(int);
float Func(int);

Dies hat zwei Gründe. Der naheliegendste ist, dass das „Name-Mangeling“, das C++-Compiler bei Funktionsüberladungen einsetzen, den Rückgabetypen nicht zwangsläufig berücksichtigt. Während der MSVC dies tut, vergibt beispielsweise der GCC an die Funktion void Func(int); aus dem ersten Beispiel das Symbol „_Z4Funci“, an die Überladung void Func(float); „_Z4Funcf“. Ändern Sie den Rückgabetypen einer der Funktionen, verändert sich der Symbolname nicht. Also kann der Linker beim Auflösen des Symbols nicht unterscheiden, welche Funktion aufgerufen werden soll, wenn man auschließlich den Rückgabetypen überlädt, denn ihr Symbolname ist identisch. Der zweite Grund ist, dass der Compiler zumeist nur schwer entscheiden kann, welcher Typ gewünscht ist. An dieser Stelle muss man aber nicht aufgeben, und jeder der Funktionen andere Namen geben. Eine Lösung für das Problem können, mit gewissen Einschränkungen, templates sein: Man könnte das erste Codebeispiel ja auch per Template, falls nötig mit entsprechenden Spezialisierungen, umsetzen:

template<typename T>
void Func(T);

template<>
void Func(int);
template<>
void Func(float);
template<>
int Func(void*);

Templates haben in Bezug auf beide o. g. Probleme Vorteile. Beim Name-Mangling von Templates wird der angegebene Typ T in jedem Fall in den Namen eingebaut, ob er nun als Rückgabetyp oder als Parameter genutzt wird. Außerdem kann man ohne Probleme explizit angeben, welche template-Spezialisierung man wünscht. Also kann man folgendes tun:

template<typename T>
T Func(int);

template<>
void Func(int);
template<>
int Func(int);
template<>
float Func(int);

Der Aufruf würde so erfolgen:

float f = Func<float>(123);

Der GCC generiert für die float-Spezialisierung den Namen „_Z4FuncIfET_i“, die int-Spezialisierung bekommt „_Z4FuncIiET_i“. Eine Implementation ist übrigens nur bei den spezialisierten Funktionen nötig. Die Implementation der allgemeinen Deklaration ist nur erforderlich, wenn Typen verwendet werden, für die keine Spezialisierungen vorhanden sind.

Ausführungszeit messen

C, C++, Zeitmessung

Wenn man, z. B. für Benchmarks oder zu Optimierungszwecken, die Ausführungszeit messen will, kann man auf die Funktion std::clock() aus <ctime> zurückgreifen. Diese gibt die Anzahl der Ticks (interne Zeit-Recheneinheit des Betriebssystems) seit Programmstart zurück, sodass die Differenz zur Messung einzelner Codestücke genutzt werden kann. Die Auflösung der Messergebnisse beträgt bei modernen Betriebssystemen i.d.R. bis zu 1 ms. Das Grundprinzip ist denkbar einfach:

std::clock_t start = std::clock();
// Hier den Code einfügen, dessen Laufzeit gemessen werden soll...
std::clock_t diff = std::clock() - start;
std::cout << "Laufzeit: " << diff*1000/CLOCKS_PER_SEC << " ms" << std::endl;

Dabei sollte man übrigens tunlichst vermeiden, versehentlich die Ausgabe des Ergebnisses mit zu messen, weil Textausgaben viel Zeit konsumieren können (vor allem in der langsamen Konsole von Windows).