10 Stimmen

Die Mechanik der Erweiterung über freie Funktionen oder Mitgliedsfunktionen

Zahlreiche C++-Bibliotheken, einschließlich der Standardbibliotheken, ermöglichen es Ihnen, Ihre Objekte für die Verwendung in den Bibliotheken anzupassen. Die Wahl ist oft zwischen einer Mitgliedsfunktion oder einer freien Funktion im gleichen Namensraum.

Ich würde gerne wissen, Mechanik und Konstrukte der Bibliothek-Code verwendet, um einen Aufruf zu versenden, die eine dieser "Erweiterung" Funktionen aufrufen wird, ich weiß, dass diese Entscheidung während der Kompilierzeit stattfinden muss und Vorlagen beinhaltet. Der folgende Laufzeit-Psuedocode ist nicht möglich/unsinnig, die Gründe liegen außerhalb des Rahmens dieser Frage.

if Class A has member function with signature FunctionSignature
    choose &A.functionSignature(...)
else if NamespaceOfClassA has free function freeFunctionSignature
    choose freeFunctionSignature(...)
else
    throw "no valid extension function was provided"

Der obige Code sieht aus wie Laufzeitcode :/. Also, wie findet die Bibliothek heraus, in welchem Namespace sich eine Klasse befindet, wie erkennt sie die drei Bedingungen, welche anderen Fallstricke gibt es, die vermieden werden müssen.

Die Motivation für meine Frage ist, dass ich die Dispatch-Blöcke in Bibliotheken finden und die Konstrukte in meinem eigenen Code verwenden kann. Detaillierte Antworten werden also helfen.

!!!KOPFGELD ZU GEWINNEN!!

Ok, so dass nach der Antwort von Steve (und die Kommentare) ADL und SFINAE sind die wichtigsten Konstrukte für die Verdrahtung der Versand zur Kompilierungszeit. Ich habe meinen Kopf arround ADL (primitiv) und SFINAE (wieder rudimentär). Aber ich weiß nicht, wie sie zusammen in der Art und Weise orchistrieren, wie ich denke, sie sollten.

Ich möchte ein anschauliches Beispiel dafür sehen, wie diese beiden Konstrukte zusammengefügt werden können, so dass eine Bibliothek zur Kompilierzeit wählen kann, ob sie eine vom Benutzer bereitgestellte Mitgliedsfunktion in einem Objekt oder eine vom Benutzer bereitgestellte freie Funktion im Namensraum desselben Objekts aufrufen soll. Dies sollte nur unter Verwendung der beiden obigen Konstrukte geschehen, ohne irgendeine Art von Laufzeitversand.

Nehmen wir an, das betreffende Objekt heißt NS::Car , und dieses Objekt muss das Verhalten von MoveForward(int units) als eine Mitgliedsfunktion vonc. Wenn das Verhalten aus dem Namespace des Objekts übernommen werden soll, wird es wahrscheinlich wie folgt aussehen MoveForward(const Car & car_, int units) . Definieren wir die Funktion, die versenden soll mover(NS::direction d, const NS::vehicle & v_) wobei direction ein Enum ist und v_ eine Basisklasse von NS::car .

8voto

Steve Jessop Punkte 264569

Die Bibliothek erledigt nichts davon zur Laufzeit, die Abfertigung wird vom Compiler durchgeführt, wenn der aufrufende Code kompiliert wird. Freie Funktionen im gleichen Namensraum wie eines der Argumente werden nach den Regeln eines Mechanismus namens "Argument-Dependent Lookup" (ADL), manchmal auch "Koenig lookup" genannt, gefunden.

In Fällen, in denen Sie die Möglichkeit haben, entweder eine freie Funktion oder eine Mitgliedsfunktion zu implementieren, kann es sein, dass die Bibliothek eine Vorlage für eine freie Funktion bereitstellt, die die Mitgliedsfunktion aufruft. Wenn Ihr Objekt dann eine Funktion mit demselben Namen in der ADL bereitstellt, ist diese besser geeignet als die Instanziierung der Vorlage und wird daher zuerst ausgewählt. Wie Space_C0wb0y sagt, könnte SFINAE verwendet werden, um die Mitgliedsfunktion in der Vorlage zu erkennen und etwas anderes zu tun, je nachdem, ob sie existiert oder nicht.

Sie können das Verhalten von Menschen nicht ändern. std::cout << x; durch Hinzufügen einer Mitgliedsfunktion zu x Ich bin mir also nicht ganz sicher, was Sie damit meinen.

2voto

Paul Groke Punkte 5864

Nun, ich kann Ihnen sagen, wie Sie das Vorhandensein von Mitgliedsfunktionen mit einem bestimmten Namen (und einer bestimmten Signatur) zur Kompilierzeit erkennen können. Ein Freund von mir beschreibt es hier:

Erkennung des Vorhandenseins von Mitgliedsfunktionen zur Kompilierzeit

Das bringt Sie jedoch nicht ans Ziel, denn es funktioniert nur für den statischen Typ. Da Sie einen "Verweis auf ein Fahrzeug" übergeben wollen, gibt es keine Möglichkeit zu prüfen, ob der dynamische Typ (der Typ des konkreten Objekts hinter dem Verweis) eine solche Mitgliedsfunktion hat.

Wenn Sie sich jedoch für den statischen Typ entscheiden, gibt es eine andere Möglichkeit, eine sehr ähnliche Sache zu tun. Sie implementiert "Wenn der Benutzer eine überladene freie Funktion zur Verfügung stellt, rufe diese auf, andernfalls versuche, die Mitgliedsfunktion aufzurufen". . Und das geht so:

namespace your_ns {

template <class T>
void your_function(T const& t)
{
    the_operation(t); // unqualified call to free function
}

// in the same namespace, you provide the "default"
// for the_operation as a template, and have it call the member function:

template <class T>
void the_operation(T const& t)
{
    t.the_operation();
}

} // namespace your_ns

Auf diese Weise kann der Benutzer seine eigene Überladung von "the_operation" bereitstellen, im selben Namespace wie seine Klasse anbieten, so dass sie von ADL gefunden wird. Natürlich ist muss die "the_operation" des Benutzers "spezialisierter" sein als Ihre Standard Implementierung sein - sonst wäre der Aufruf zweideutig. In der Praxis ist das aber kein Problem, da alles, was den den Typ des Parameters mehr einschränkt, als dass er eine Referenz-zu-Konst zu alles wird "spezialisierter" sein.

Beispiel:

namespace users_ns {

class foo {};

void the_operation(foo const& f)
{
    std::cout << "foo\n";
}

template <class T>
class bar {};

template <class T>
void the_operation(bar<T> const& b)
{
    std::cout << "bar\n";
}

} // namespace users_ns

EDIT: nachdem ich Steve Jessops Antwort noch einmal gelesen habe, stelle ich fest, dass das im Grunde das ist, was er geschrieben hat, nur mit mehr Worten :)

1voto

ildjarn Punkte 61204

Wenn Sie nur ein konkretes Beispiel suchen, denken Sie an Folgendes:

#include <cassert>
#include <type_traits>
#include <iostream>

namespace NS
{
    enum direction { forward, backward, left, right };

    struct vehicle { virtual ~vehicle() { } };

    struct Car : vehicle
    {
        void MoveForward(int units) // (1)
        {
            std::cout << "in NS::Car::MoveForward(int)\n";
        }
    };

    void MoveForward(Car& car_, int units)
    {
        std::cout << "in NS::MoveForward(Car&, int)\n";
    }
}

template<typename V>
class HasMoveForwardMember // (2)
{
    template<typename U, void(U::*)(int) = &U::MoveForward>
    struct sfinae_impl { };

    typedef char true_t;
    struct false_t { true_t f[2]; };

    static V* make();

    template<typename U>
    static true_t check(U*, sfinae_impl<U>* = 0);
    static false_t check(...);

public:
    static bool const value = sizeof(check(make())) == sizeof(true_t);
};

template<typename V, bool HasMember = HasMoveForwardMember<V>::value>
struct MoveForwardDispatcher // (3)
{
    static void MoveForward(V& v_, int units) { v_.MoveForward(units); }
};

template<typename V>
struct MoveForwardDispatcher<V, false> // (3)
{
    static void MoveForward(V& v_, int units) { NS::MoveForward(v_, units); }
};

template<typename V>
typename std::enable_if<std::is_base_of<NS::vehicle, V>::value>::type // (4)
mover(NS::direction d, V& v_)
{
    switch (d)
    {
    case NS::forward:
        MoveForwardDispatcher<V>::MoveForward(v_, 1); // (5)
        break;
    case NS::backward:
        // ...
        break;
    case NS::left:
        // ...
        break;
    case NS::right:
        // ...
        break;
    default:
        assert(false);
    }
}

struct NonVehicleWithMoveForward { void MoveForward(int) { } }; // (6)

int main()
{
    NS::Car v; // (7)
    //NonVehicleWithMoveForward v;  // (8)
    mover(NS::forward, v);
}

HasMoveForwardMember (2) ist eine Metafunktion, die prüft, ob eine Mitgliedsfunktion dieses Namens mit der Signatur void(V::*)(int) in einer bestimmten Klasse V . MoveForwardDispatcher (3) verwendet diese Information, um die Mitgliedsfunktion aufzurufen, wenn sie existiert, oder greift auf den Aufruf einer freien Funktion zurück, wenn sie nicht existiert. mover delegiert einfach den Aufruf von MoveForward a MoveForwardDispatcher (5) .

Der Code, so wie er gepostet wurde, ruft Folgendes auf Car::MoveForward (1) aber wenn diese Mitgliedsfunktion entfernt, umbenannt oder ihre Signatur geändert wird, NS::MoveForward wird stattdessen aufgerufen.

Beachten Sie auch, dass mover eine Vorlage ist, muss eine SFINAE-Prüfung durchgeführt werden, um die Semantik beizubehalten, dass nur Objekte erlaubt sind, die von NS::vehicle zur Weitergabe an v_ (4) . Zur Veranschaulichung: Wenn man auskommentiert (7) und unkommentiert (8) , mover wird mit einem Objekt des Typs NonVehicleWithMoveForward (6) die wir nicht zulassen wollen, obwohl sie HasMoveForwardMember<NonVehicleWithMoveForward>::value == true .

( Hinweis : Wenn Ihre Standardbibliothek nicht mit std::enable_if y std::is_base_of verwenden Sie die std::tr1:: o boost:: Varianten stattdessen als verfügbar).

Die Art und Weise, wie diese Art von Code normalerweise verwendet wird, besteht darin, immer die free-Funktion aufzurufen und die free-Funktion in Form von etwas wie MoveForwardDispatcher so dass die Free-Funktion einfach die Member-Funktion des übergebenen Objekts aufruft, wenn sie existiert, ohne Überladungen dieser Free-Funktion für jeden möglichen Typ schreiben zu müssen, der eine entsprechende Member-Funktion haben könnte.

0voto

umlcat Punkte 3993

Obwohl Entwickler manchmal freie Funktionen oder Klassenfunktionen austauschbar verwenden können, gibt es einige Situationen, in denen sie die jeweils andere verwenden müssen.

(1) Objekt-/Klassenfunktionen ("Methoden") werden bevorzugt, wenn die meisten ihrer Zwecke nur das Objekt betreffen oder Objekte dazu bestimmt sind, andere Objekte zusammenzusetzen.

// object method
MyListObject.add(MyItemObject);
MyListObject.add(MyItemObject);
MyListObject.add(MyItemObject);

(2) Freie ("globale" oder "modulare") Funktionen werden bevorzugt, wenn sie mehrere Objekte betreffen und die Objekte nicht miteinander verbunden sind. Oder, wenn die Funktion einfache Daten verwendet (Strukturen ohne Methoden, primitive Typen).

MyStringNamespace.MyStringClass A = new MyStringNamespace.MyStringClass("Mercury");
MyStringNamespace.MyStringClass B = new MyStringNamespace.MyStringClass("Jupiter"); 
// free function
bool X = MyStringNamespace.AreEqual(A, B);

Wenn einige allgemeine Modulfunktionen auf Objekte zugreifen, gibt es in C++ das "friend-Schlüsselwort", das ihnen den Zugriff auf die Methoden des Objekts ermöglicht, ohne den Anwendungsbereich zu berücksichtigen.

class MyStringClass {
  private:
    // ...
  protected:
    // ...
  // not a method, but declared, to allow access
  friend:
    bool AreEqual(MyStringClass A, MyStringClass B);
}

bool AreEqual(MyStringClass A, MyStringClass B) { ... }

In "fast rein objektorientierten" Programmiersprachen wie Java oder C#, wo es keine freien Funktionen gibt, werden freie Funktionen durch statische Methoden ersetzt, was die Sache komplizierter macht.

0voto

Alain Rist Punkte 827

Wenn ich richtig verstanden habe, wird Ihr Problem einfach durch (vielleicht mehrfache) Vererbung gelöst. Sie haben irgendwo eine Namespace freie Funktion:

namespace NS {
void DoSomething()
{
    std::cout << "NS::DoSomething()" << std::endl;
}
} // namespace NS

Verwenden Sie eine Basisklasse, die die gleiche Funktion weiterleitet:

struct SomethingBase
{
    void DoSomething()
    {
        return NS::DoSomething();
    }
};

Wenn eine Klasse A, die von SomethingBase abgeleitet ist, DoSomething() nicht implementiert, wird sie SomethingBase::DoSomething() -> NS::DoSomething() aufrufen:

struct A : public SomethingBase // probably other bases
{
    void DoSomethingElse()
    {
        std::cout << "A::DoSomethingElse()" << std::endl;
    }
};

Wenn eine andere Klasse B, die von SomethingBase abgeleitet ist, DoSomething() implementiert, ruft sie B::DoSomething() auf:

struct B : public SomethingBase // probably other bases

{
    void DoSomething()
    {
        std::cout << "B::DoSomething()" << std::endl;
    }
};

Der Aufruf von DoSomething() auf einem Objekt, das von SomethingBase abgeleitet ist, führt also das Mitglied aus, wenn es existiert, oder andernfalls die free-Funktion. Beachten Sie, dass es nichts zu werfen gibt, Sie erhalten einen Kompilierfehler, wenn es keine Übereinstimmung mit Ihrem Aufruf gibt.

int main()
{
    A a;
    B b;
    a.DoSomething(); // "NS::DoSomething()"
    b.DoSomething(); // "B::DoSomething()"
    a.DoSomethingElse(); // "A::DoSomethingElse()"
    b.DoSomethingElse(); // error 'DoSomethingElse' : is not a member of 'B'
}

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X