inline-FAQ



  • Voraussetzungen zum Verständnis:

    • Grundlagen in C++
    • Grundlegende Idee von der Arbeitsweise eines Compilers als Übersetzer von C++ nach Maschinencode
    • Funktionsweise des Präprozessors
    • Kenntnis des Übersetzungsmodells, getrennte Übersetzung, Übersetzungseinheiten

    Frage: Was ist inline und was bringt es?

    Antwort: "Inline-Expansion" ist eine bestimmte Art, einen Funktionsaufruf zu übersetzen. Ein Funktionsaufruf wird normalerweise so oder so ähnlich in Maschinencode umgesetzt:

    1. Argumente auf den Stack legen
    2. Zur ersten Adresse der Funktion springen
    3. Funktion ausführen ...
    4. An die dem Aufruf folgende Anweisung zurückspringen
    5. Argumente wieder vom Stack runternehmen

    Das ist an sich ziemlich clever, da eine Funktion so nur einmal im Speicher existiert, egal, an wie vielen Stellen im Programm sie aufgerufen wird.

    Nun kommt es aber, gerade in der objektorientierten Programmierung, häufig vor, dass man viele kleine Funktionen hat, die fast nichts tun. Man denke etwa an get- und set-Funktionen. Der Overhead an Laufzeit und Codegröße, der durch den Funktionsaufruf entsteht, übersteigt die Kosten der eigentlichen Arbeit der Funktion um ein vielfaches. Hier kommt die Inline-Expansion ins Spiel: Statt an die Adresse der Funktion zu springen, wird der Code der Funktion an der Aufrufstelle eingefügt. Ein Beispiel:

    int square(int n) {
      return n*n;
    }
    ...
      a = square(42);
    

    Wenn der Funktionsaufruf von square inline expandiert wird, dann wird ein zu

    a = 42*42;
    

    äquivalenter Code generiert. Und mehr noch: Der Compiler sieht jetzt sogar die Möglichkeit, weiter zu optimieren, und berechnet den Ausdruck gleich zur Übersetzungszeit.

    Bei aller Euphorie sollte man aber daran denken, dass sich das nur bei relativ kleinen Funktionen lohnt, da sich der Code sonst ziemlich aufblähen kann.

    Es gibt auch Funktionen, bei denen Inline-Expansion technisch schwierig bis unmöglich ist (z.B. rekursive Funktionen).

    Frage: Wie bringe ich den Compiler dazu, Funktionsaufrufe inline zu expandieren?

    Antwort: Sofern der Compiler hinreichend modern ist und nicht durch irgendwelche Einstellungen (z.B. Debug-Modus) oder Beschränkungen daran gehindert wird, kann er das in der Regel von alleine. Er wägt bei jedem Funktionsaufruf ab, ob es sich lohnen würde, die Funktion inline zu expandieren oder 'normal' anzuspringen.

    Frage: Wenn der Compiler das einfach so kann, wozu dann noch das inline-Schlüsselwort? Ist es veraltet und überflüssig, etwa wie auto oder register?

    Antwort: Das ist aufgrund des Übersetzungsmodells nicht so einfach. Die meisten Compiler können die Inline-Expansion nur durchführen, wenn die Definition der Funktion (also der Funktionskörper) an der Aufrufstelle sichtbar ist. Das heißt im Klartext, dass die Funktion in der gleichen Übersetzungseinheit definiert sein muss.

    Das kann man bei Memberfunktionen z.B. erreichen, indem man sie direkt in der Klasse implementiert. Denn um sie überhaupt aufrufen zu können, muss die Klassendefinition sichtbar sein, und damit ist automatisch die Funktionsdefinition auch sichtbar.

    Funktionen außerhalb von Klassen, seien es Memberfunktionen oder freie Funktionen, können aber nicht einfach so über Header in alle Übersetzungseinheiten, in denen sie mitsamt Definition sichtbar sein sollen, eingebunden werden. Der Linker weist einen dann mit einer Reihe von Fehlermeldungen auf die Verletzung der "One-Definition-Rule" hin. Diese Regel besagt, dass eine Definition in einem Programm nur ein einziges Mal vorkommen darf. Ausnahme: Es handelt sich um die Definition einer Klasse, einer Aufzählung, eines Templates oder einer inline-Funktion, vorausgesetzt, diese Definitionen sind identisch.

    inline dient also dazu, eine Funktion, deren Definition man gerne in mehreren Übersetzungseinheiten sichtbar haben will, zu dieser One-Definition-Rule konform zu machen. Anders herum gesehen: inline erlaubt es, Funktionen in Headern zu definieren. Und das wiederum ermöglicht dem Compiler ihre Inline-Expansion.

    Frage: Kannst du das nochmal in ein paar einfachen Faustregeln zusammenfassen?

    Antwort: Wenn du willst, dass deine Funktion inline expandiert werden kann, dann definiere sie auf eine dieser Arten:

    1. als Memberfunktion, die direkt innerhalb der Klasse definiert ist
    2. als Templatefunktion in einem Header (egal ob Member oder nicht)
    3. Funktion in einem Header (egal ob Member oder nicht), die mit dem inline-Schlüsselwort gekennzeichnet ist

    Frage: Dann sorge ich ab jetzt also bei jeder Funktion, die klein genug ist, dafür, dass sie inline expandiert werden kann?

    Antwort: Wenn du mit den dadurch zusätzlich entstehenden Abhängigkeiten leben kannst ...

    Zum einen hängt jeglicher Code, der die inline-Funktion aufruft, von der Implementation dieser Funktion ab. Wenn sie verändert wird, muss dieser ganze Code neu compiliert werden.

    Zum anderen erbt der aufrufende Code die Abhängigkeiten der inline-Funktion (transitive Abhängigkeit). Mal ein abstraktes Beispiel:

    // Header a.h
    class B;
    class A {
      B* b;
     public:
      void f();
    };
    
    // Implementationsdatei
    #include "b.h"
    void A::f() {
      b = new B;
      ...
    }
    

    Würde man A::f jetzt inline machen, also sie entweder innerhalb von A oder aber mit 'inline' im Header definieren, dann müßte auch b.h in den Header eingebunden werden. Wer jetzt a.h einbindet, wird also auch von b.h abhängig.

    Frage: Eine fast philosophische Frage noch zum Abschluss: Warum ist das überhaupt so? Warum macht "inline" nicht einfach, dass eine Funktion immer inline expandiert wird?

    Antwort: Die Semantik der Sprache C++ definiert zu jedem (konformen) Programm ein beobachtbares Verhalten. Das heißt praktisch, dass es dem Compiler komplett freigestellt ist, wie er das Programm übersetzt, solange die gleiche Ausgabe erzeugt wird. Da es ihm damit auch freigestellt ist, ob er einen Funktionsaufruf inline expandiert, kann es kein Sprachmittel geben, mit dem man dieses Verhalten erzwingen kann (Ausnahmen sind natürlich proprietäre Erweiterungen der Compilerhersteller wie __forceinline).

    Das inline-Schlüsselwort dient nun lediglich dazu, dem Programmierer zu ermöglichen, die Voraussetzungen herbeizuführen, unter denen der Compiler Funktionsaufrufe inline expandieren kann. Ohne dieses Wissen ist die Semantik von inline vielleicht etwas rätselhaft.

    --------------------------------------------------------------------
    Anmerkungen: Die vorliegende FAQ wurde darauf ausgerichtet, einige grundlegende Missverständnisse im Gebrauch des inline-Schlüsselwortes aufzuklären. Es wird kein Anspruch auf Vollständigkeit erhoben, insbesondere Templates sind ein ganz eigenes Gebiet mit vielen Fallstricken.

    Zur One-Definition-Rule gibt es unter http://fara.cs.uni-potsdam.de/~kaufmann/?page=GenCppFaqs&faq=ODR#Answ einen wesentlich ausführlicheren Text.

    In http://www.gotw.ca/gotw/033.htm wird einiges zu dem Thema, wann sich inline-Expansion lohnt, gesagt.

    Danke für die Hilfe beim Abrunden des Textes an elise und HumeSikkins


Anmelden zum Antworten