Gibt es 5 Jahre später etwas Besseres als die „schnellstmöglichen C ++ - Delegierten“?


72

Ich weiß, dass das Thema "C ++ - Delegierte" zu Tode gebracht wurde und sowohl http://www.codeproject.com als auch http://stackoverflow.com die Frage ausführlich behandeln.

Im Allgemeinen scheint Don Clugstons schnellster Delegierter für viele Menschen die erste Wahl zu sein. Es gibt einige andere beliebte.

Ich bemerkte jedoch, dass die meisten dieser Artikel alt sind (um 2005) und viele Designentscheidungen unter Berücksichtigung alter Compiler wie VC7 getroffen wurden.

Ich benötige eine sehr schnelle Delegate-Implementierung für eine Audioanwendung.

Ich brauche es immer noch portabel (Windows, Mac, Linux), aber ich verwende nur moderne Compiler (VC9, der in VS2008 SP1 und GCC 4.5.x).

Meine Hauptkriterien sind:

  • es muss schnell sein!
  • Es muss mit neueren Versionen der Compiler vorwärtskompatibel sein. Ich habe einige Zweifel an Dons Implementierung, weil er ausdrücklich erklärt, dass sie nicht standardkonform ist.
  • Optional ist eine KISS-Syntax und Benutzerfreundlichkeit schön zu haben
  • Multicast wäre schön, obwohl ich überzeugt bin, dass es wirklich einfach ist, es um eine Delegatenbibliothek herum aufzubauen

Außerdem brauche ich keine exotischen Features. Ich brauche nur das gute alte Zeiger-zu-Methode-Ding. Keine Notwendigkeit, statische Methoden, freie Funktionen oder ähnliches zu unterstützen.

Was ist ab heute der empfohlene Ansatz? Verwenden Sie immer noch Dons Version ? Oder gibt es einen "Community-Konsens" über eine andere Option?

Ich möchte Boost.signal / signal2 wirklich nicht verwenden, da dies in Bezug auf die Leistung nicht akzeptabel ist. Eine Abhängigkeit von QT ist ebenfalls nicht akzeptabel.

Außerdem habe ich beim Googeln einige neuere Bibliotheken gesehen, wie zum Beispiel cpp-events, aber ich konnte kein Feedback von Benutzern finden, auch nicht zu SO.


4
Und außerdem mehr als ein Jahr später: Fügt C ++ 11 etwas zu diesem Problem hinzu? Es scheint nicht, aber ich könnte etwas Offensichtliches verpasst haben :)
Dinaiz

Antworten:


120

Update: Ein Artikel mit dem vollständigen Quellcode und einer ausführlicheren Diskussion wurde auf The Code Project veröffentlicht.

Das Problem mit Zeigern auf Methoden ist, dass sie nicht alle gleich groß sind. Anstatt Zeiger auf Methoden direkt zu speichern, müssen wir sie so "standardisieren", dass sie eine konstante Größe haben. Dies versucht Don Clugston in seinem Artikel über das Code-Projekt zu erreichen. Er nutzt dazu die vertrauten Kenntnisse der beliebtesten Compiler. Ich behaupte, dass es möglich ist, dies in "normalem" C ++ zu tun, ohne solche Kenntnisse zu erfordern.

Betrachten Sie den folgenden Code:

void DoSomething(int)
{
}

void InvokeCallback(void (*callback)(int))
{
    callback(42);
}

int main()
{
    InvokeCallback(&DoSomething);
    return 0;
}

Dies ist eine Möglichkeit, einen Rückruf mit einem einfachen alten Funktionszeiger zu implementieren. Dies funktioniert jedoch nicht für Methoden in Objekten. Lassen Sie uns das beheben:

class Foo
{
public:
    void DoSomething(int) {}

    static void DoSomethingWrapper(void* obj, int param)
    {
        static_cast<Foo*>(obj)->DoSomething(param);
    }
};

void InvokeCallback(void* instance, void (*callback)(void*, int))
{
    callback(instance, 42);
}

int main()
{
    Foo f;
    InvokeCallback(static_cast<void*>(&f), &Foo::DoSomethingWrapper);
    return 0;
}

Jetzt haben wir ein System von Rückrufen, die sowohl für freie als auch für Mitgliedsfunktionen funktionieren können. Dies ist jedoch ungeschickt und fehleranfällig. Es gibt jedoch ein Muster - die Verwendung einer Wrapper-Funktion, um den statischen Funktionsaufruf an einen Methodenaufruf auf der richtigen Instanz weiterzuleiten. Wir können Vorlagen verwenden, um dies zu unterstützen. Versuchen wir, die Wrapper-Funktion zu verallgemeinern:

template<typename R, class T, typename A1, R (T::*Func)(A1)>
R Wrapper(void* o, A1 a1)
{
    return (static_cast<T*>(o)->*Func)(a1);

}

class Foo
{
public:
    void DoSomething(int) {}
};

void InvokeCallback(void* instance, void (*callback)(void*, int))
{
    callback(instance, 42);
}

int main()
{
    Foo f;
    InvokeCallback(static_cast<void*>(&f),
        &Wrapper<void, Foo, int, &Foo::DoSomething> );
    return 0;
}

Dies ist immer noch äußerst umständlich, aber zumindest müssen wir jetzt nicht jedes Mal eine Wrapper-Funktion ausschreiben (zumindest für den Fall mit 1 Argument). Eine andere Sache, die wir verallgemeinern können, ist die Tatsache, dass wir immer einen Zeiger auf übergeben void*. Warum nicht zusammenfügen, anstatt es als zwei verschiedene Werte zu übergeben? Und während wir das tun, warum nicht auch verallgemeinern? Hey, lass uns eine einwerfen, operator()()damit wir sie wie eine Funktion aufrufen können!

template<typename R, typename A1>
class Callback
{
public:
    typedef R (*FuncType)(void*, A1);

    Callback(void* o, FuncType f) : obj(o), func(f) {}
    R operator()(A1 a1) const
    {
        return (*func)(obj, a1);
    }

private:
    void* obj;
    FuncType func;
};

template<typename R, class T, typename A1, R (T::*Func)(A1)>
R Wrapper(void* o, A1 a1)
{
    return (static_cast<T*>(o)->*Func)(a1);

}

class Foo
{
public:
    void DoSomething(int) {}
};

void InvokeCallback(Callback<void, int> callback)
{
    callback(42);
}

int main()
{
    Foo f;
    Callback<void, int> cb(static_cast<void*>(&f),
        &Wrapper<void, Foo, int, &Foo::DoSomething>);
    InvokeCallback(cb);
    return 0;
}

Wir machen Fortschritte! Aber jetzt ist unser Problem die Tatsache, dass die Syntax absolut schrecklich ist. Die Syntax erscheint redundant. Kann der Compiler die Typen nicht vom Zeiger auf die Methode selbst herausfinden? Leider nein, aber wir können es weiterhelfen. Denken Sie daran, dass ein Compiler in einem Funktionsaufruf Typen über die Ableitung von Vorlagenargumenten ableiten kann. Warum fangen wir nicht damit an?

template<typename R, class T, typename A1>
void DeduceMemCallback(R (T::*)(A1)) {}

Innerhalb der Funktion, wissen wir , was R, Tund A1ist. Was ist, wenn wir eine Struktur erstellen können, die diese Typen "hält" und von der Funktion zurückgibt?

template<typename R, class T, typename A1>
struct DeduceMemCallbackTag
{
};

template<typename R, class T, typename A1>
DeduceMemCallbackTag2<R, T, A1> DeduceMemCallback(R (T::*)(A1))
{
    return DeduceMemCallbackTag<R, T, A1>();
}

Und da DeduceMemCallbackTagwir über die Typen Bescheid wissen, warum nicht unsere Wrapper-Funktion als statische Funktion einfügen? Und da die Wrapper-Funktion darin enthalten ist, warum nicht den Code einfügen, um unser CallbackObjekt darin zu konstruieren ?

template<typename R, typename A1>
class Callback
{
public:
    typedef R (*FuncType)(void*, A1);

    Callback(void* o, FuncType f) : obj(o), func(f) {}
    R operator()(A1 a1) const
    {
        return (*func)(obj, a1);
    }

private:
    void* obj;
    FuncType func;
};

template<typename R, class T, typename A1>
struct DeduceMemCallbackTag
{
    template<R (T::*Func)(A1)>
    static R Wrapper(void* o, A1 a1)
    {
        return (static_cast<T*>(o)->*Func)(a1);
    }

    template<R (T::*Func)(A1)>
    inline static Callback<R, A1> Bind(T* o)
    {
        return Callback<R, A1>(o, &DeduceMemCallbackTag::Wrapper<Func>);
    }
};

template<typename R, class T, typename A1>
DeduceMemCallbackTag<R, T, A1> DeduceMemCallback(R (T::*)(A1))
{
    return DeduceMemCallbackTag<R, T, A1>();
}

Mit dem C ++ - Standard können wir statische Funktionen für Instanzen (!) Aufrufen:

class Foo
{
public:
    void DoSomething(int) {}
};

void InvokeCallback(Callback<void, int> callback)
{
    callback(42);
}

int main()
{
    Foo f;
    InvokeCallback(
        DeduceMemCallback(&Foo::DoSomething)
        .Bind<&Foo::DoSomething>(&f)
    );
    return 0;
}

Trotzdem ist es ein langer Ausdruck, aber wir können das in ein einfaches Makro (!) Setzen:

template<typename R, typename A1>
class Callback
{
public:
    typedef R (*FuncType)(void*, A1);

    Callback(void* o, FuncType f) : obj(o), func(f) {}
    R operator()(A1 a1) const
    {
        return (*func)(obj, a1);
    }

private:
    void* obj;
    FuncType func;
};

template<typename R, class T, typename A1>
struct DeduceMemCallbackTag
{
    template<R (T::*Func)(A1)>
    static R Wrapper(void* o, A1 a1)
    {
        return (static_cast<T*>(o)->*Func)(a1);
    }

    template<R (T::*Func)(A1)>
    inline static Callback<R, A1> Bind(T* o)
    {
        return Callback<R, A1>(o, &DeduceMemCallbackTag::Wrapper<Func>);
    }
};

template<typename R, class T, typename A1>
DeduceMemCallbackTag<R, T, A1> DeduceMemCallback(R (T::*)(A1))
{
    return DeduceMemCallbackTag<R, T, A1>();
}

#define BIND_MEM_CB(memFuncPtr, instancePtr) \
    (DeduceMemCallback(memFuncPtr).Bind<(memFuncPtr)>(instancePtr))

class Foo
{
public:
    void DoSomething(int) {}
};

void InvokeCallback(Callback<void, int> callback)
{
    callback(42);
}

int main()
{
    Foo f;
    InvokeCallback(BIND_MEM_CB(&Foo::DoSomething, &f));
    return 0;
}

Wir können das CallbackObjekt verbessern, indem wir einen sicheren Bool hinzufügen. Es ist auch eine gute Idee, die Gleichheitsoperatoren zu deaktivieren, da zwei CallbackObjekte nicht verglichen werden können. Noch besser ist es, eine teilweise Spezialisierung zu verwenden, um eine "bevorzugte Syntax" zu ermöglichen. Dies gibt uns:

template<typename FuncSignature>
class Callback;

template<typename R, typename A1>
class Callback<R (A1)>
{
public:
    typedef R (*FuncType)(void*, A1);

    Callback() : obj(0), func(0) {}
    Callback(void* o, FuncType f) : obj(o), func(f) {}

    R operator()(A1 a1) const
    {
        return (*func)(obj, a1);
    }

    typedef void* Callback::*SafeBoolType;
    operator SafeBoolType() const
    {
        return func != 0? &Callback::obj : 0;
    }

    bool operator!() const
    {
        return func == 0;
    }

private:
    void* obj;
    FuncType func;
};

template<typename R, typename A1> // Undefined on purpose
void operator==(const Callback<R (A1)>&, const Callback<R (A1)>&);
template<typename R, typename A1>
void operator!=(const Callback<R (A1)>&, const Callback<R (A1)>&);

template<typename R, class T, typename A1>
struct DeduceMemCallbackTag
{
    template<R (T::*Func)(A1)>
    static R Wrapper(void* o, A1 a1)
    {
        return (static_cast<T*>(o)->*Func)(a1);
    }

    template<R (T::*Func)(A1)>
    inline static Callback<R (A1)> Bind(T* o)
    {
        return Callback<R (A1)>(o, &DeduceMemCallbackTag::Wrapper<Func>);
    }
};

template<typename R, class T, typename A1>
DeduceMemCallbackTag<R, T, A1> DeduceMemCallback(R (T::*)(A1))
{
    return DeduceMemCallbackTag<R, T, A1>();
}

#define BIND_MEM_CB(memFuncPtr, instancePtr) \
    (DeduceMemCallback(memFuncPtr).Bind<(memFuncPtr)>(instancePtr))

Anwendungsbeispiel:

class Foo
{
public:
    float DoSomething(int n) { return n / 100.0f; }
};

float InvokeCallback(int n, Callback<float (int)> callback)
{
    if(callback) { return callback(n); }
    return 0.0f;
}

int main()
{
    Foo f;
    float result = InvokeCallback(97, BIND_MEM_CB(&Foo::DoSomething, &f));
    // result == 0.97
    return 0;
}

Ich habe dies auf dem Visual C ++ - Compiler (Version 15.00.30729.01, die mit VS 2008 geliefert wird) getestet, und Sie benötigen einen relativ neuen Compiler, um den Code zu verwenden. Durch die Überprüfung der Demontage konnte der Compiler die Wrapper-Funktion und den DeduceMemCallbackAufruf optimieren und sich auf einfache Zeigerzuweisungen reduzieren.

Es ist einfach für beide Seiten des Rückrufs zu verwenden und verwendet nur (was ich glaube) Standard-C ++. Der oben gezeigte Code funktioniert für Elementfunktionen mit 1 Argument, kann jedoch auf weitere Argumente verallgemeinert werden. Es kann auch weiter verallgemeinert werden, indem statische Funktionen unterstützt werden.

Beachten Sie, dass für das CallbackObjekt keine Heap-Zuordnung erforderlich ist. Dank dieses "Standardisierungs" -Verfahrens haben sie eine konstante Größe. Aus diesem Grund ist es möglich, dass ein CallbackObjekt Mitglied einer größeren Klasse ist, da es einen Standardkonstruktor hat. Es ist auch zuweisbar (die vom Compiler generierten Kopierzuweisungsfunktionen sind ausreichend). Dank der Vorlagen ist es auch typsicher.


1
Ich habe dies vor einigen Jahren versucht und die Verwendung von Zeiger-auf-Mitglied-Funktionen als Vorlagenargumente, obwohl dies vom Standard zugelassen wurde, verursachte interne Compilerfehler in Visual C ++ (ich glaube, es war Version 2005 SP1). Mit der Einschränkung, dass dies wirklich die Verwendung aktueller Compilerversionen erfordert, ist dies ein sehr guter Ansatz.
Ben Voigt

2
@Dinaiz: Die 0-5 Argument Version ist wirklich fast das gleiche wie oben, aber mit Dingen wie DeduceMemCallbackTag5mit Parametern wie A1, A2, A3usw. Das partielle Template - Spezialisierung Geschäft macht es transparent. Es sprengt sich in Hunderte von Codezeilen, was es zu einem guten Kandidaten für eine eigene Bibliothek macht. Ich bin im Moment etwas beschäftigt, aber später werde ich einen Artikel mit dem vollständigen Quellcode schreiben. Diese Antwort auf den Stapelüberlauf ist so lang wie sie ist.
In silico

2
@j_random_hacker: Gute Frage. Die Wrapper<>()Funktion benötigt den Zeiger auf die Methode zur Kompilierungszeit , daher muss der Benutzercode einen Methodenzeiger über einen Nicht-Typ- Vorlagenparameter bereitstellen , dh den einzigen Nicht-Typ-Vorlagenparameter von Bind<>(). Der einzige Zweck DeduceMemCallback()besteht darin, die Typen abzuleiten, aus denen der Methodenzeiger besteht, und ein Dummy-Objekt zurückzugeben, das die abgeleiteten Typen im Prozess "kennt". Dies ermöglicht Bind<>()es, einen Methodenzeiger als Nicht-Typ-Argument zu akzeptieren, ohne die Typen, aus denen der Methodenzeiger besteht, explizit akzeptieren zu müssen.
In silico

3
@ John: Eigentlich gibt es eine Möglichkeit, diese Delegierten vergleichbar zu machen, indem man die One-Definition-Regel nutzt . Mir war nicht bewusst, dass ODR für das Aufnehmen von Adressen von Funktionsvorlagen gilt, als ich dies schrieb (beachten Sie, dass die verknüpfte Frage später als meine gestellt wurde). Ich habe einige Tests durchgeführt und es scheint zu funktionieren. Sie müssen lediglich den Funktionszeiger und den Objektzeiger vergleichen. Ich kann diese Antwort und den Artikel in Zukunft aktualisieren, wenn ich Zeit habe.
In silico

1
Dies ist die epischste Antwort, die ich je auf S / O gesehen habe. Bravo!
Reuben Scratton

10

Ich wollte der Antwort von @ Insilico mit ein paar meiner eigenen Sachen folgen.

Bevor ich auf diese Antwort gestoßen war, versuchte ich auch schnelle Rückrufe herauszufinden, die keinen Overhead verursachten und nur durch Funktionssignatur eindeutig vergleichbar / identifiziert waren. Was ich letztendlich erstellt habe - mit ernsthafter Hilfe von Klingonen, die zufällig beim Grillen waren - funktioniert für alle Funktionstypen (außer Lambdas, es sei denn, Sie speichern den Lambda, aber versuchen Sie es nicht, weil es wirklich schwierig und schwierig ist und kann dazu führen, dass ein Roboter Ihnen beweist, wie schwierig es ist, und Sie dazu bringt, die Scheiße dafür zu essen ). Vielen Dank an @sehe, @nixeagle, @StackedCrooked, @CatPlusPlus, @Xeo, @DeadMG und natürlich an @Insilico für die Hilfe beim Erstellen des Ereignissystems. Fühlen Sie sich frei, wie Sie es wünschen.

Wie auch immer, ein Beispiel ist auf ideone verfügbar, aber der Quellcode ist auch für Sie da (da Liveworkspace ausfällt, vertraue ich ihnen nicht auf zwielichtige Kompilierungsdienste. Wer weiß, wann ideone ausfällt?!). Ich hoffe, dies ist nützlich für jemanden, der nicht damit beschäftigt ist, Lambda / Function-Objecting the World in Stücke zu bringen:

WICHTIGER HINWEIS: Ab sofort (28.11.2012, 21:35 Uhr) funktioniert diese variable Version nicht mit dem Microsoft VC ++ 2012 November CTP (Mailand). Wenn Sie es damit verwenden möchten, müssen Sie alle variadischen Dinge loswerden und die Anzahl der Argumente explizit aufzählen (und möglicherweise den Typ mit 1 Argumenten für Eventfor spezialisieren void), damit es funktioniert. Es ist ein Schmerz, und ich konnte es nur für 4 Argumente aufschreiben, bevor ich müde wurde (und entschied, dass das Übergeben von mehr als 4 Argumenten etwas schwierig war).

Quellenbeispiel

Quelle:

#include <iostream>
#include <vector>
#include <utility>
#include <algorithm>

template<typename TFuncSignature>
class Callback;

template<typename R, typename... Args>
class Callback<R(Args...)> {
public:
        typedef R(*TFunc)(void*, Args...);

        Callback() : obj(0), func(0) {}
        Callback(void* o, TFunc f) : obj(o), func(f) {}

        R operator()(Args... a) const {
                return (*func)(obj, std::forward<Args>(a)...);
        }
        typedef void* Callback::*SafeBoolType;
        operator SafeBoolType() const {
                return func? &Callback::obj : 0;
        }
        bool operator!() const {
                return func == 0;
        }
        bool operator== (const Callback<R (Args...)>& right) const {
                return obj == right.obj && func == right.func;
        }
        bool operator!= (const Callback<R (Args...)>& right) const {
                return obj != right.obj || func != right.func;
        }
private:
        void* obj;
        TFunc func;
};

namespace detail {
        template<typename R, class T, typename... Args>
        struct DeduceConstMemCallback { 
                template<R(T::*Func)(Args...) const> inline static Callback<R(Args...)> Bind(T* o) {
                        struct _ { static R wrapper(void* o, Args... a) { return (static_cast<T*>(o)->*Func)(std::forward<Args>(a)...); } };
                        return Callback<R(Args...)>(o, (R(*)(void*, Args...)) _::wrapper);
                }
        };

        template<typename R, class T, typename... Args>
    struct DeduceMemCallback { 
                template<R(T::*Func)(Args...)> inline static Callback<R(Args...)> Bind(T* o) {
                        struct _ { static R wrapper(void* o, Args... a) { return (static_cast<T*>(o)->*Func)(std::forward<Args>(a)...); } };
                        return Callback<R(Args...)>(o, (R(*)(void*, Args...)) _::wrapper);
                }
        };

        template<typename R, typename... Args>
        struct DeduceStaticCallback { 
                template<R(*Func)(Args...)> inline static Callback<R(Args...)> Bind() { 
                        struct _ { static R wrapper(void*, Args... a) { return (*Func)(std::forward<Args>(a)...); } };
                        return Callback<R(Args...)>(0, (R(*)(void*, Args...)) _::wrapper); 
                }
        };
}

template<typename R, class T, typename... Args>
detail::DeduceConstMemCallback<R, T, Args...> DeduceCallback(R(T::*)(Args...) const) {
    return detail::DeduceConstMemCallback<R, T, Args...>();
}

template<typename R, class T, typename... Args>
detail::DeduceMemCallback<R, T, Args...> DeduceCallback(R(T::*)(Args...)) {
        return detail::DeduceMemCallback<R, T, Args...>();
}

template<typename R, typename... Args>
detail::DeduceStaticCallback<R, Args...> DeduceCallback(R(*)(Args...)) {
        return detail::DeduceStaticCallback<R, Args...>();
}

template <typename... T1> class Event {
public:
        typedef void(*TSignature)(T1...);
        typedef Callback<void(T1...)> TCallback;
        typedef std::vector<TCallback> InvocationTable;

protected:
        InvocationTable invocations;

public:
        const static int ExpectedFunctorCount = 2;

        Event() : invocations() {
                invocations.reserve(ExpectedFunctorCount);
        }

        template <void (* TFunc)(T1...)> void Add() {
                TCallback c = DeduceCallback(TFunc).template Bind<TFunc>();
                invocations.push_back(c);
        }

        template <typename T, void (T::* TFunc)(T1...)> void Add(T& object) {
                Add<T, TFunc>(&object);
        }

        template <typename T, void (T::* TFunc)(T1...)> void Add(T* object) {
                TCallback c = DeduceCallback(TFunc).template Bind<TFunc>(object);
                invocations.push_back(c);
        }

        template <typename T, void (T::* TFunc)(T1...) const> void Add(T& object) {
                Add<T, TFunc>(&object);
        }

        template <typename T, void (T::* TFunc)(T1...) const> void Add(T* object) {
                TCallback c = DeduceCallback(TFunc).template Bind<TFunc>(object);
                invocations.push_back(c);
        }

        void Invoke(T1... t1) {
                for(size_t i = 0; i < invocations.size() ; ++i) invocations[i](std::forward<T1>(t1)...); 
        }

        void operator()(T1... t1) {
                Invoke(std::forward<T1>(t1)...);
        }

        size_t InvocationCount() { return invocations.size(); }

        template <void (* TFunc)(T1...)> bool Remove ()          
        { return Remove (DeduceCallback(TFunc).template Bind<TFunc>()); } 
        template <typename T, void (T::* TFunc)(T1...)> bool Remove (T& object) 
        { return Remove <T, TFunc>(&object); } 
        template <typename T, void (T::* TFunc)(T1...)> bool Remove (T* object) 
        { return Remove (DeduceCallback(TFunc).template Bind<TFunc>(object)); } 
        template <typename T, void (T::* TFunc)(T1...) const> bool Remove (T& object) 
        { return Remove <T, TFunc>(&object); } 
        template <typename T, void (T::* TFunc)(T1...) const> bool Remove (T* object) 
        { return Remove (DeduceCallback(TFunc).template Bind<TFunc>(object)); } 

protected:
        bool Remove( TCallback const& target ) {
                auto it = std::find(invocations.begin(), invocations.end(), target);
                if (it == invocations.end()) 
                        return false;
                invocations.erase(it);
                return true;
        }
};

Wären Sie bereit, die nicht-variadische Version mit bis zu 4 Argumenten für diejenigen von uns mit älteren Compilern zu teilen? Danke!
Dave

@supertwang Uh. Nicht wirklich, denn dann müsste ich aufhören, alle ausgefallenen Definitionen zu verwenden, die ich erstellt habe, und den Code bereinigen, um mich nur auf die Standardbibliothek zu verlassen, und das ist ein Schmerz. Sie können eine Version mit zwei Argumenten unter: stackoverflow.com/questions/15032594/…
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.