Your Browser is not longer supported

Please use Google Chrome, Mozilla Firefox or Microsoft Edge to view the page correctly
Loading...

{{viewport.spaceProperty.prod}}

extern inline vs. static inline

&pagelevel(4)&pagelevel

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 lokale static 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 keine auto-Variable genutzt werden, da der Speicher beim Return freigegeben wird. Ein lokale static 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.