std::function - behind the scenes

Wie könnte man std::function implementieren? Die spezielle Schwierigkeit besteht darin, zwischen dem allgemeinen Konzept "Kann mit bestimmten Parametern aufgerufen werden und liefert einen Rückgabewert" und der speziellen Implementierung als Funktionszeiger, Funktionsobjekt oder Lambda-Funktionsobjekt zu vermitteln.

Video

Quelltext

#include <iostream>
#include <memory>

template <typename Ret, typename... Args> struct FHBase
{
  virtual ~FHBase() {}
  virtual Ret operator()(Args...) = 0;
};

template <typename F> class Function;

template <typename Ret, typename... Args> class Function<Ret (Args...)>
{

  template <typename F> struct FH : public FHBase<Ret, Args...>
  {
    F mF;
    
    FH(F f)
      : mF(f)
    {
    }
    
    Ret operator()(Args... args) override
    {
      return mF(args...);
    }
  };
  
  std::shared_ptr<FHBase<Ret, Args...>> mFH;
  
public:
  
  template <typename F> Function(F f)
  {
    mFH.reset(new FH<F>(f));
  }
  
  Ret operator()(Args... args)
  {
    return mFH->operator()(args...);
  }
};

template <typename... Args> class Function<void (Args...)>
{
  template <typename F> struct FH : public FHBase<void, Args...>
  {
    F mF;
    
    FH(F f)
      : mF(f)
    {
    }
    
    void operator()(Args... args)
    {
      mF(args...);
    }
  };

  std::shared_ptr<FHBase<void, Args...>> mFH;
  
public:
  
  template <typename F> Function(F f)
  {
    mFH.reset(new FH<F>(f));
  }
  
  void operator()(Args... args)
  {
    mFH->operator()(args...);
  }
};

void print(std::string const &s)
{
  std::cout << "print: " << s << std::endl;
}

struct Callable
{
  void operator()()
  {
    std::cout << "Callable ohne Parameter!\n";
  }
};

int main()
{
  Function<void (std::string const&)> fptr(print);
  fptr("Hallo Funktionszeiger!");
  
  Function<int (int, int)> adder([](int a, int b) { return a+b; });
  std::cout << "adder: " << adder(10, 25) << std::endl;
    
  Callable c;
  Function<void()> call(c);
  call();
}

Erklärung

std::function (und demzufolge hier im Beispiel die Klasse Function) repräsentieren das allgemeine Konzept "kann mit Parametern aufgerufen werden und liefert einen Rückgabewert". Implementiert wird das durch die öffentliche Schnittstelle in Form des operator(). Zu allgemein darf das Konzept aber auch wieder nicht gehalten sein. Zumindest der Typ des Rückgabewertes und Typ und Anzahl der Parameter müssen festgehalten werden. Implementiert wird dies über den Weg der Templatespezialisierung. Function wird als allgemeines Template mit einem Parameter deklariert. Das ist notwendig, damit wir es spezialisieren können. Die allgemeine Deklaration wird allerdings nicht durch eine Definition ergänzt, denn wir wollen eigentlich etwas spezielleres: das Template darf nur dann instanziiert werden, wenn der Templateparameter der Form "Rückgabetyp (Parameter1, Parameter2...)" genügt. Ausgedrückt wird das in Zeile 12 durch den Ausdruck in der Templatespezialisierung. Da Rückgabetyp und Parametertypen natürlich wieder flexibel sein sollen, sind sie ihrerseits wieder wiederum Parameter der spezialisierten Klasse. Das hat gleich noch den Vorteil, dass wir die betreffenden Typen aus der Spezialisierung ableiten können. Wenn wir also das Function-Template mit einem Parameter der Form "Ret (Arg1, Arg2, ...)" instanziieren, dann erhalten die entsprechenden Parameter der Templatespezialisierung die passenden Typen.

Diese Typen können nun verwendet werden, um den operator() passend zu implementieren. Er liefert einen Wert des Typs Ret zurück und nimmt Parameter aus der Parameterliste Args....

Was nun noch fehlt ist die Information, welches tatsächlich aufrufbare Element durch Function verpackt wird. Das ist wesentlich spezieller, als das allgemeine Konzept, was von der Klasse repräsentiert wird. Um ein Objekt zu speichern müsste Function nämlich wissen, welchen Typ dieses Objekt genau hat. Das entsprechende Feld ist schlicht unterschiedlich für Funktionszeiger oder bspw. Lambda-Funktionen. Wir könnten nun natürlich den Typ dieses Feldes wieder als Templateparameter übergeben. Das würde allerdings bedeuten, dass der Nutzer der Klasse diesen bei der Erzeugung des Function-Objektes mit übergeben muss, was klar nicht das ist, was std::function erwartet. std::function scheint sich die notwendige Information irgendwie aus dem Konstruktoraufruf abzuleiten.

Um das zu implementieren muss unser Konstruktor selbst ein Template sein. Als Funktionstemplate (der Konstruktor ist eine spezielle Funktion, aber eben eine Funktion) kann er seinen Typparameter automatisch ableiten. Sehr günstig, so müssen wir ihn nicht angeben. Was der Konstruktor aber nicht kann: er kann in seinem Objekt kein neues Feld eines vorher unbekannten Typs anlegen. Ergo: das Feld der Function-Klasse, in dem wir das aufzurufende Element speichern, kann nichts spezifisches darüber wissen müssen, was an den Konstruktor übergeben wird.

Um dieses Problem zu lösen wenden wir uns der Laufzeitpolymorphie zu. Die ist immer dann praktisch, wenn wir ein allgemeines Konzept ("wird mit Parametern aufgerufen und liefert Wert zurück") durch eine spezifische Implementierung ("ruft intern bspw. eine Lambda-Funktion auf") hinterlegen/implementieren wollen. Wir verpacken also das eigentlich aufzurufende Objekt nochmal: aus Sicht des Function-Objektes halten wir intern einen Pointer auf eine Basisklasse (FHBase), die das allgemeine Konzept der Aufrufbarkeit repräsentiert (durch den operator()). Der Konstruktor, der einzige Teil von Function, der genau weiß, was gespeichert werden soll und welchen Typ das hat, legt ein Objekt einer von FHBase abgeleiteten Klasse an und hängt es an diesem Zeiger auf. Um nun das passende abgeleitete Objekt zu generieren, greifen wir wieder auf ein Klassentemplate zurück, welches den entsprechenden zu speichernden Typ als Parameter bekommt. 

Damit haben wir alle Elemente zusammen: der Konstruktor wird passend vom Compiler beim Aufruf generiert und leitet den zu speichernden Typ ab. Das umgebende Function-Template wird durch den Nutzer instanziiert um die passende Schnittstelle zu repräsentieren. Im Konstruktor wird das Template FH passend instanziiert, um das übergebene Element zu speichern und aufrufbar zu machen. Zuguterletzt kann dieses dann aufgerufen werden, indem der operator() von Function über einen Pointer auf die Basisklasse FHBase per Laufzeitpolymorphie auf die korrekte Implementierung des operator() von FH umleitet, welche dann wiederum den eigentlichen Aufruf des gespeicherten Elements durchführt.  Das funktioniert damit dann sowohl für Funktionstypen, Funktionszeiger, als auch für Lambda-Funktionen (deren Typ ja nichtmal genauer festgelegt ist, aber im Konstruktor eben automatisch abgeleitet werden kann).

Die einzige Besonderheit, die sich im Beispiel noch ergibt ist die zweite Spezialisierung von Function für den Rückgabetyp void. Hier ist das Problem, dass die ursprüngliche Implementierung zu einem Ausdruck der Form "return <irgendwas>" für ein "<irgendwas>" mit dem Typ void führen würde. Das ist in C++ nicht erlaubt und wird vom Compiler mit einem Fehler quittiert. Daher ist die zweite Spezialisierung eigentlich identisch bis auf die Punkte in den operator(), wo das return fehlt.