Eine inline-Funktion kann als static-inline oder extern-inline interpretiert werden. Diese Frage wird wichtig, wenn in der Funktion lokale static-Variablen verwendet werden oder wenn die Adresse einer solchen Funktion in einem Vergleich genutzt wird.
Die Beispiele 1 und 2 am Ende dieses Abschnitts zeigen das Problem.
Eine static-inline-Funktion verhält sich aus Sicht des Programmierers genau wie eine static-Funktion. Die Funktion selbst und alle in der Funktion deklarierten Elemente sind lokal zu der aktuellen Übersetzungseinheit. Die Funktion selbst hat keine Interaktion mit Elementen anderer Übersetzungseinheiten; solche müssen von anderen (externen) Funktionen hergestellt werden.
Eine extern-inline-Funktion verhält sich wie eine normale, externe Funktion. Der Rumpf der Funktion muss in jeder benutzenden Übersetzungseinheit auftreten. Dieser muss aber jeweils identisch sein. Das Verhalten lässt sich mit einem Vergleich beschreiben: Es ist so, als ob der Rumpf als einzige Definition in einer zusätzlichen Übersetzungseinheit steht und in allen anderen Übersetzungseinheiten diese Funktion aufgerufen wird.
Zusätzlich zu obiger Definition gibt es den Hinweis an den Optimierer, dass eine Inline-Expansion wünschenswert ist. Diese Optimierung muss aber unter Beibehaltung der Bedeutung erfolgen.
Die Sicht des C++- Standard
Wird eine Funktion mit dem Schlüsselwort inline
deklariert, so gilt sie implizit als extern-inline. Eine static-inline muss mit den Schlüsselwörtern static
und inline
deklariert werden.
Eine Member-Funktion, die innerhalb einer Klasse definiert ist, gilt implizit als extern-inline.
Die Implementierung im Sprachmodus C++ 2017 bzw. C++ 2020
Die Sprachmodi C++ 2017 und C++ 2020 unterstützt extern-inline wie vom Standard verlangt. Siehe hierzu "extern-inline Funktionen".
Die Implementierung in den Sprachmodi Cfront-C++ und C++ V3
In den Sprachmodi Cfront-C++ und C++ V3 kennt der Compiler das Konzept einer extern-inline-Funktion nicht. Alle inline-Funktionen werden von ihm als static-inline interpretiert.
Dies trifft sowohl die mit dem Schlüsselwort inline
deklarierten Funktionen als auch die innerhalb einer Klasse definierten Member-Funktionen.
Die folgenden Abschnitte gelten nur für die Sprachmodi Cfront-C++ und C++ V3.
Die problematischen Konstrukte
In vielen Fällen ist nicht relevant, ob eine Funktion als static-inline oder extern-inline interpretiert wird. Dazu muss eines von drei Konstrukten verwendet werden. Des Weiteren wird es nur problematisch, wenn die Funktion in mehreren Übersetzungseinheiten verwendet wird.
Betroffene Funktionen sind solche, die als extern-inline gedacht sind (siehe Beispiel 3). Diese Funktionen stehen fast immer in einem Header-File, komplett mit Rumpf. Es kann sich dabei um Templates oder um Member-Funktionen handeln.
Bei Templates ist zu beachten, dass der C/C++-Compiler weitere Sourcen inkludiert, wenn dies nicht abgeschaltet wurde. Siehe hierzu die Optionen //MODIFY-SOURCE-PROPERTIES IMPLICIT-INCLUDE = bzw. –K implicit_include
, die im Default-Fall aktiv sind (siehe Beispiel 4).
lokale static-Variablen
Eine lokale
static
-Variable ist in einer solchen Situation in der Compiler-Implementierung mehrfach vorhanden, während der Standard eine einzige Kopie verlangt. Ob dies zu einem Problem führt, hängt davon ab, aus welchem Grund die Variable als lokalestatic
definiert wurde (siehe Beispiel 5).
Wenn tatsächlich eine einzige Kopie erwartet wird, gibt es ein Problem. Ob dies zu Ablauf-Fehlern führt, hängt stark von der realen Benutzung ab.
Eine andere Situation liegt vor, wenn der Grund die Lebensdauer des Speichers ist. Es gibt viel Code, der intern einen String aufbaut, um ihn als Return-Wert zurückzugeben. Hier kann keineauto
-Variable genutzt werden, da der Speicher beim Return freigegeben wird. Ein lokalestatic
bleibt aber bis zum nächsten Aufruf der Funktion gültig. Dies wird häufig genutzt, wenn Texte für eine Ausgabe aufbereitet werden. In diesem Fall wird die Variable zwar verdoppelt, aber dies hat praktisch keine Auswirkung auf den Programmablauf.Vergleich von Adressen
Eine extern-inline-Funktion hat eine eindeutige Adresse, eine static-inline jeweils eine Adresse pro Übersetzungseinheit. Dies ist nicht relevant, solange die Adressen nur zum Aufruf der Funktion genutzt werden.
Kritisch wird es, wenn solche Adressen miteinander verglichen werden. Dies passiert aber eher selten. Denkbar wäre so etwas beim Anmelden von Callback-Funktionen, wo jede Funktion nur einmal eingetragen werden sollte.String-literal
Dieses Konstrukt liefert wohl nur in Ausnahmefällen ein Problem. Der Standard legt nicht fest, ob String-Literale mit gleichem Inhalt auch die gleiche Adresse haben. Auch ist es sehr unüblich, die Adresse eines String-Literal in Vergleichen oder ähnlichem zu benutzen.
Beispiel 1: eine problematische Situation
// file bsp.h #include <stdio.h> class BSP { public: inline int mf1(void); }; inline int BSP::mf1(void) { static int i = 1; return i++; } extern void f(); // file bsp1.c #include “bsp.h” void f() { BSP bsp; printf (“Wert 1: %d\n”, bsp.mf1() ); } // file bsp2.c #include “bsp.h” void g() { BSP bsp; printf (“Wert 2: %d\n”, bsp.mf1() ); } int main() { f(); g(); return 0; }
Erläuterung
Kritisch ist die Funktion mf1
und die darin enthaltene Variable i
. Nach Standard sollte diese nur einmal existieren. Die beiden Aufrufe in bsp1.c
und bsp2.c
sollen also die gleiche Variable betreffen. Die gewünschte Ausgabe ist
Wert 1: 1
Wert 2: 2
In der Implementierung vom C/C++-Compiler wird die Funktion aber in bsp1.c
und bsp2.c
jeweils getrennt bearbeitet. Es wird beim Aufruf in bsp2.c
eine andere Variable angesprochen als beim Aufruf in bsp1.c
. Beide Ausprägungen sind mit 1 initialisiert.
Die Ausgabe ist dann
Wert 1: 1
Wert 2: 1
Beispiel 2: eine problematische Situation mit Templates
// file tmpl.h #include <stdio.h> template <class T> inline int tmpl(T t) { static int i = 1; return i++; } extern void f(); // file tmpl1.c #include “tmpl.h” void f() { printf (“Wert 1: %d\n”, tmpl(5) ); } // file tmpl2.c #include “tmpl.h” void g() { printf (“Wert 2: %d\n”, tmpl(7) ); } int main() { f(); g(); return 0; }
Erläuterung
Es gelten die gleichen Hinweise wie für Beispiel 1. Nur handelt es sich hier um ein Template statt um eine Member-Funktion. Die vom Standard erwartete Ausgabe ist
Wert 1: 1
Wert 2: 2
Die vom C/C++-Compiler gelieferte Ausgabe ist
Wert 1: 1
Wert 2: 1
Beispiel 3: betroffene Funktionen
// file bf.h inline int f1(T) { } // betroffen int f2(T); // nicht betroffen static inline int f3(T) { } // nicht betroffen, da explizit static template <class T> inline int tf1(T) { } // betroffen template <class T> int tf2(T); // nicht betroffen template <class T> static inline int tf3(T) { } // nicht betroffen, da explizit static class BSP { public: inline int mf1(void); // betroffen int mf2(void) { } // betroffen int mf3(void); // nicht betroffen }; inline int BSP::mf1(void) { }
Beispiel 4: Implizites Include
// file ii.h template <class T> inline int ii(T); // file ii.c template <class T> inline int ii(T) // diese Funktion ist betroffen, obwohl sie in einer .c-Datei steht { static int i; return i++; }
Beispiel 5: Problematische static Variablen
Hier wird nur die Nutzung der static
als problematisch / nicht problematisch dargestellt. Um zu einem echten Problem zu werden, muss die Funktion eine problematische inline-Funktion sein.
inline void no_recursion (void) { static int active = 0; // problematisch active ++; if (active > 1) { illegal_recursion (); } else { do_something (); } active --; }
Dies ist problematisch, da die Rekursion nicht zuverlässig erkannt wird.
inline char * debug_text () { static char buffer [200]; // nicht problematisch sprintf (buffer, "...", ...); return buffer; }
Dies ist nicht problematisch, da der Inhalt des buffer
immer vor dem nächsten Aufruf verwendet wurde. Es gibt normalerweise niemand, der die Adresse abspeichert und annimmt, dass sich ihr Inhalt zuverlässig ändert.
inline void f (void) { static int actions = 0; // (siehe unten) actions++; if (actions > 200) { actions = 0; optimize_datastructures (); // Nur aufräumen, keine relevante Änderung } do_something (); }
Ob dies problematisch wird, hängt stark von der Funktion optimize_datastructures
ab. Wenn dort wirklich nur Optimierungen vorgenommen werden, läuft das Programm trotz der Verdoppelung von actions korrekt ab.
Die Performance ist jedoch anders, da die Aufrufe von optimize_datastructures
nicht mehr regelmäßig sind.