Softwareentwicklung: Argument-Dependent Lookup und Hidden-Friend-Idiom in C++

Argument-Dependent Lookup steht für eine Reihe von Regeln für das Nachschlagen unqualifizierter Funktionen auf der Grundlage ihrer Funktionsargumente.

In Pocket speichern vorlesen Druckansicht 2 Kommentare lesen

(Bild: rawf8/Shutterstock.com)

Lesezeit: 4 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

Hinter Argument-Dependent Lookup (ADL), das auch als Koenig Lookup bezeichnet wird, steht eine Reihe "magischer" Regeln für das Nachschlagen unqualifizierter Funktionen auf der Grundlage ihrer Funktionsargumente. Das Hidden Friend Idiom basiert auf dem Argument-Dependent Lookup (ADL).

Modernes C++ – Rainer Grimm

Rainer Grimm ist seit vielen Jahren als Softwarearchitekt, Team- und Schulungsleiter tätig. Er schreibt gerne Artikel zu den Programmiersprachen C++, Python und Haskell, spricht aber auch gerne und häufig auf Fachkonferenzen. Auf seinem Blog Modernes C++ beschäftigt er sich intensiv mit seiner Leidenschaft C++.

Warum funktioniert das "Hello World"-Programm ?

#include <iostream>

int main() {
    std::cout << "Hello world";
}

Anders gefragt: Warum sollte das Programm nicht funktionieren? Der überladene Ausgabeoperator << ist im std-Namensraum definiert. Die Frage ist also: Wie findet man den passenden überladenen Ausgabeoperator für std::string? Die Antwort ist ADL.

Wikipedia zeigt eine schöne Definition von ADL:

  • Argument-Dependent Lookup: In the C++ programming language, argument-dependent lookup (ADL), or argument-dependent name lookup, applies to the lookup of an unqualified function name depending on the types of the arguments given to the function call. This behavior is also known as Koenig lookup, as it is often attributed to Andrew Koenig, though he is not its inventor.

Hier ist ein einfaches Beispiel für die Anwendung von ADL:

// adl.cpp

namespace MyNamespace {
    struct MyStruct {};
    void function(MyStruct) {}
}   

int main() {

    MyNamespace::MyStruct obj;  
    function(obj);     // (1)

}

Der Aufruf function(obj) in (1) würde ohne Argument-Dependent Lookup fehlschlagen. Dank ADL wird bei der Suche nach unqualifizierten Funktionsnamen zusätzlich zur normalen Suche nach unqualifizierten Namen auch der Namensraum der Argumente berücksichtigt. Folglich wird der Funktionsname im Namesraum MyNamespace gefunden.

Jetzt wissen wir, was ADL bedeutet. Aber damit ist das ursprüngliche Problem noch nicht gelöst. Warum funktioniert das einfache "Hello World"-Programm?

#include <iostream>

int main() {
    std::cout << "Hello world";
}

Probieren wir es mit C++ Insights aus.

#include <iostream>

int main()
{
  std::operator<<(std::cout, "Hallo Welt"); // (1)
  return 0;
}

Der Aufruf std::cout << "Hello World"; ist gleichbedeutend mit dem Funktionsaufruf operator<<(std::cout, "Hallo Welt"); (1). ADL betrachtet den Namespace seiner Argumente, der in unserem konkreten Fall std::cout umfasst.

Argument-Dependend Lookup erweitert die öffentliche Schnittstelle einer Klasse: Funktionen oder Operatoren, die keine Mitglieder sind, erweitern die öffentliche Schnittstelle dieser Klasse. Jetzt kommt das Hidden-Friend-Idiom zum Tragen:

friend-Funktionen oder Operatoren, die innerhalb der Klasse definiert sind, besitzen zwei besondere Eigenschaften:

  • Sie können auf die privaten Mitglieder der Klasse zugreifen
  • Sie sind Nicht-Mitglieder-Funktionen oder -Operatoren

Der zweite Punkt ist ziemlich unbekannt, und ich muss ihn regelmäßig in meinen C++ Klassen erklären. Eine innerhalb der Klasse definierte friend-Funktion hat interessante Konsequenzen für das Überladen von Operatoren. friend-Operatoren, die innerhalb der Klasse definiert sind, können auf die privaten Mitglieder der Klasse zugreifen, sind Nicht-Mitglieder-Funktionen und werden durch Argument-Dependent Lookup gefunden.

// hiddenFriend.cpp

#include <iostream>

class MyDistance{
 public:
    explicit MyDistance(double i):m(i){}

    friend MyDistance operator +(const MyDistance& a,
                                 const MyDistance& b){ // (1)
        return MyDistance(a.m + b.m);
    }
    
    friend MyDistance operator -(const MyDistance& a, 
                                 const MyDistance& b){ // (2)
        return MyDistance(a.m - b.m);
    }

    friend std::ostream& operator<< (std::ostream &out,
                           const MyDistance& myDist){  // (3)
        out << myDist.m << " m";
        return out;
    }
    
 private:
    double m;

};


int main() {

    std::cout << '\n';

    std::cout << "MyDistance(5.5) + MyDistance(5.5): " 
      << MyDistance(5.5) + MyDistance(5.5) << '\n';  // (4)

    std::cout << "MyDistance(5.5) - MyDistance(5.5): " 
      << MyDistance(5.5) - MyDistance(5.5) << '\n';  // (5)

    std::cout << '\n';

}

Alle drei Operatoren in (1), (2) und (3) sind Freunde. Die entsprechenden Operatoren + in (4) und - in (5) werden wie erwartet gefunden.

C++ Insights zeigt einmal mehr die Magie der Operatorüberladung.

Insbesondere die Addition in (4) (MyDistance(5.5) + MyDistance(5.5)) wird umgewandelt in: operator+(MyDistance(5.5), MyDistance(5.5)).

Zum Schluss ist hier die Ausgabe des Programms:

Im Gegensatz dazu möchte ich die friend-Deklaration aus dem überladenen Operator + entfernen.

class MyDistance{
 public:
    explicit MyDistance(double i):m(i){}

    MyDistance operator +(const MyDistance& a, 
                          const MyDistance& b){        
        return MyDistance(a.m + b.m);
    }
    
    friend MyDistance operator -(const MyDistance& a, 
                                 const MyDistance& b){      
        return MyDistance(a.m - b.m);
    }

    friend std::ostream& operator<< (std::ostream &out, 
                                     const MyDistance& myDist){ 
        out << myDist.m << " m";
        return out;
    }
    
 private:
    double m;

};

Jetzt schlägt die Kompilierung des Programms fehl:

Der Compiler bemängelt in der ersten Zeile, dass die Definition des Operators + nur Null oder ein Argument haben kann (hiddenFriend:9:16). Ohne die friend-Deklaration ist der Operator + eine Member-Funktion und hat als solche einen impliziten this-Zeiger. Das bedeutet, dass der Operator + in der Summe drei Argumente besitzt. Folglich findet der Compiler nicht den passenden Operator + (hiddenFriend:32:75).

Neben dem Hidden Friend Idiom gibt es in C++ noch viele weitere Idiome für den Klassenentwurf. In meinem nächsten Artikel werde ich über die Null-, Fünfer- oder Sechser-Regel schreiben. (rme)