629 Stimmen

Vorgefertigte Prüfung auf das Vorhandensein einer Klassenmitgliedfunktion?

Ist es möglich, eine Vorlage zu schreiben, die das Verhalten ändert, je nachdem, ob eine bestimmte Member-Funktion auf eine Klasse definiert ist?

Hier ist ein einfaches Beispiel dafür, was ich schreiben würde:

template<class T>
std::string optionalToString(T* obj)
{
    if (FUNCTION_EXISTS(T->toString))
        return obj->toString();
    else
        return "toString not defined";
}

Also, wenn class T hat toString() definiert ist, dann wird sie verwendet, ansonsten nicht. Der magische Teil, von dem ich nicht weiß, wie er funktioniert, ist der "FUNCTION_EXISTS"-Teil.

382voto

Nicola Bonelli Punkte 7933

Ja, mit SFINAE können Sie prüfen, ob eine bestimmte Klasse eine bestimmte Methode bereitstellt. Hier ist der Arbeitscode:

#include <iostream>

struct Hello
{
    int helloworld() { return 0; }
};

struct Generic {};    

// SFINAE test
template <typename T>
class has_helloworld
{
    typedef char one;
    struct two { char x[2]; };

    template <typename C> static one test( decltype(&C::helloworld) ) ;
    template <typename C> static two test(...);    

public:
    enum { value = sizeof(test<T>(0)) == sizeof(char) };
};

int main(int argc, char *argv[])
{
    std::cout << has_helloworld<Hello>::value << std::endl;
    std::cout << has_helloworld<Generic>::value << std::endl;
    return 0;
}

Ich habe es gerade mit Linux und gcc 4.1/4.3 getestet. Ich weiß nicht, ob es auf andere Plattformen mit anderen Compilern übertragbar ist.

302voto

Xeo Punkte 126280

Diese Frage ist alt, aber mit C++11 haben wir eine neue Möglichkeit, die Existenz einer Funktion (oder die Existenz eines beliebigen Nicht-Typ-Members) zu überprüfen, indem wir wieder auf SFINAE zurückgreifen:

template<class T>
auto serialize_imp(std::ostream& os, T const& obj, int)
    -> decltype(os << obj, void())
{
  os << obj;
}

template<class T>
auto serialize_imp(std::ostream& os, T const& obj, long)
    -> decltype(obj.stream(os), void())
{
  obj.stream(os);
}

template<class T>
auto serialize(std::ostream& os, T const& obj)
    -> decltype(serialize_imp(os, obj, 0), void())
{
  serialize_imp(os, obj, 0);
}

Nun zu einigen Erklärungen. Erstens: Ich verwende Ausdruck SFINAE zum Ausschluss der serialize(_imp) Funktionen von der Überladungsauflösung, wenn der erste Ausdruck innerhalb von decltype nicht gültig ist (d.h. die Funktion existiert nicht).

El void() wird verwendet, um den Rückgabetyp all dieser Funktionen void .

El 0 Argument wird verwendet, um die os << obj Überlast, wenn beide verfügbar sind (wörtliche 0 ist vom Typ int und daher ist die erste Überladung die bessere Wahl).


Nun wollen Sie wahrscheinlich eine Eigenschaft, die prüft, ob eine Funktion existiert. Glücklicherweise ist es einfach, so etwas zu schreiben. Beachten Sie jedoch, dass Sie einen Trait schreiben müssen selbst für jeden anderen Funktionsnamen, den Sie brauchen könnten.

#include <type_traits>

template<class>
struct sfinae_true : std::true_type{};

namespace detail{
  template<class T, class A0>
  static auto test_stream(int)
      -> sfinae_true<decltype(std::declval<T>().stream(std::declval<A0>()))>;
  template<class, class A0>
  static auto test_stream(long) -> std::false_type;
} // detail::

template<class T, class Arg>
struct has_stream : decltype(detail::test_stream<T, Arg>(0)){};

Live-Beispiel.

Und nun zu den Erklärungen. Erstens, sfinae_true ist ein Hilfstyp und läuft im Grunde auf dasselbe hinaus wie das Schreiben von decltype(void(std::declval<T>().stream(a0)), std::true_type{}) . Der Vorteil ist einfach, dass er kürzer ist.
Als nächstes wird die struct has_stream : decltype(...) erbt entweder von std::true_type o std::false_type je nachdem, ob die decltype einchecken test_stream scheitert oder nicht.
Zuletzt, std::declval gibt Ihnen einen "Wert" des übergebenen Typs, ohne dass Sie wissen müssen, wie Sie ihn konstruieren können. Beachten Sie, dass dies nur innerhalb eines unbewerteten Kontextes möglich ist, wie z.B. decltype , sizeof und andere.


Beachten Sie, dass decltype ist nicht unbedingt erforderlich, da sizeof (und alle nicht bewerteten Kontexte) haben diese Erweiterung erhalten. Es ist nur so, dass decltype liefert bereits einen Typ und ist als solcher einfach sauberer. Hier ist ein sizeof Version einer der Überladungen:

template<class T>
void serialize_imp(std::ostream& os, T const& obj, int,
    int(*)[sizeof((os << obj),0)] = 0)
{
  os << obj;
}

El int y long Parameter sind aus demselben Grund noch immer vorhanden. Der Array-Zeiger wird verwendet, um einen Kontext bereitzustellen, in dem sizeof verwendet werden können.

206voto

Morwenn Punkte 20210

C++20 - requires Ausdrücke

Mit C++20 kommen Konzepte und verschiedene Werkzeuge wie requires Ausdrücke die eine eingebaute Möglichkeit sind, die Existenz einer Funktion zu prüfen. Mit ihnen können Sie Ihre optionalToString wie folgt funktionieren:

template<class T>
std::string optionalToString(T* obj)
{
    constexpr bool has_toString = requires(const T& t) {
        t.toString();
    };

    if constexpr (has_toString)
        return obj->toString();
    else
        return "toString not defined";
}

Pre-C++20 - Erkennungs-Toolkit

N4502 schlägt ein Erkennungs-Toolkit zur Aufnahme in die C++17-Standardbibliothek vor, das es schließlich in die Bibliotheksgrundlagen TS v2 geschafft hat. Es wird höchstwahrscheinlich nie in den Standard aufgenommen werden, da es von requires Ausdrücke, aber es löst das Problem immer noch auf eine elegante Weise. Das Toolkit führt einige Metafunktionen ein, darunter std::is_detected die verwendet werden kann, um auf einfache Weise Typ- oder Funktionserkennungs-Metafunktionen darüber zu schreiben. Hier ist, wie Sie es verwenden können:

template<typename T>
using toString_t = decltype( std::declval<T&>().toString() );

template<typename T>
constexpr bool has_toString = std::is_detected_v<toString_t, T>;

Beachten Sie, dass das obige Beispiel ungetestet ist. Das Erkennungs-Toolkit ist noch nicht in den Standardbibliotheken verfügbar, aber der Vorschlag enthält eine vollständige Implementierung, die Sie leicht kopieren können, wenn Sie sie wirklich brauchen. Es spielt gut mit dem C++17-Feature if constexpr :

template<class T>
std::string optionalToString(T* obj)
{
    if constexpr (has_toString<T>)
        return obj->toString();
    else
        return "toString not defined";
}

C++14 - Boost.Hana

Boost.Hana baut offenbar auf diesem speziellen Beispiel auf und bietet in seiner Dokumentation eine Lösung für C++14, die ich daher direkt zitieren werde:

[...] Hana bietet eine is_valid Funktion, die mit generischen C++14-Lambdas kombiniert werden kann, um eine viel sauberere Implementierung der gleichen Sache zu erhalten:

auto has_toString = hana::is_valid([](auto&& obj) -> decltype(obj.toString()) { });

Damit haben wir ein Funktionsobjekt has_toString die zurückgibt, ob der angegebene Ausdruck für das Argument, das wir ihr übergeben, gültig ist. Das Ergebnis wird als IntegralConstant Da das Ergebnis der Funktion ohnehin als Typ dargestellt wird, ist constexpr-ness hier kein Thema. Jetzt ist die Funktion nicht nur weniger langatmig (das ist ein Einzeiler!), sondern die Absicht ist auch viel klarer. Weitere Vorteile sind die Tatsache, dass has_toString kann an Algorithmen höherer Ordnung weitergegeben werden und kann auch im Funktionsbereich definiert werden, so dass es nicht notwendig ist, den Namensraum mit Implementierungsdetails zu verschmutzen.

Boost.TTI

Ein anderes, etwas idiomatischeres Toolkit zur Durchführung einer solchen Prüfung - wenn auch weniger elegant - ist Boost.TTI , eingeführt in Boost 1.54.0. Für Ihr Beispiel müssten Sie das Makro BOOST_TTI_HAS_MEMBER_FUNCTION . So können Sie es verwenden:

#include <boost/tti/has_member_function.hpp>

// Generate the metafunction
BOOST_TTI_HAS_MEMBER_FUNCTION(toString)

// Check whether T has a member function toString
// which takes no parameter and returns a std::string
constexpr bool foo = has_member_function_toString<T, std::string>::value;

Dann könnten Sie die bool um einen SFINAE-Check zu erstellen.

Erläuterung

Das Makro BOOST_TTI_HAS_MEMBER_FUNCTION erzeugt die Metafunktion has_member_function_toString die den geprüften Typ als ersten Template-Parameter annimmt. Der zweite Schablonenparameter entspricht dem Rückgabetyp der Mitgliedsfunktion, und die folgenden Parameter entsprechen den Typen der Funktionsparameter. Das Mitglied value enthält true wenn die Klasse T hat eine Mitgliedsfunktion std::string toString() .

Alternativ dazu, has_member_function_toString kann einen Zeiger auf eine Mitgliedsfunktion als Template-Parameter annehmen. Daher ist es möglich, die has_member_function_toString<T, std::string>::value von has_member_function_toString<std::string T::* ()>::value .

173voto

C++ ermöglicht SFINAE verwendet werden (beachten Sie, dass dies mit C++11 einfacher ist, weil es erweiterte SFINAE auf fast beliebige Ausdrücke unterstützt - das unten stehende wurde so gestaltet, dass es mit gängigen C++03-Compilern funktioniert):

#define HAS_MEM_FUNC(func, name)                                        \
    template<typename T, typename Sign>                                 \
    struct name {                                                       \
        typedef char yes[1];                                            \
        typedef char no [2];                                            \
        template <typename U, U> struct type_check;                     \
        template <typename _1> static yes &chk(type_check<Sign, &_1::func > *); \
        template <typename   > static no  &chk(...);                    \
        static bool const value = sizeof(chk<T>(0)) == sizeof(yes);     \
    }

Die obige Schablone und das Makro versuchen, eine Schablone zu instanziieren, indem sie ihr einen Zeigertyp für eine Mitgliedsfunktion und den eigentlichen Zeiger auf die Mitgliedsfunktion geben. Wenn die Typen nicht übereinstimmen, wird die Vorlage von SFINAE ignoriert. Verwendung wie diese:

HAS_MEM_FUNC(toString, has_to_string);

template<typename T> void
doSomething() {
   if(has_to_string<T, std::string(T::*)()>::value) {
      ...
   } else {
      ...
   }
}

Beachten Sie aber, dass Sie das nicht einfach aufrufen können toString Funktion in dieser if Zweigstelle. Da der Compiler in beiden Zweigen auf Gültigkeit prüft, würde dies in Fällen, in denen die Funktion nicht existiert, fehlschlagen. Eine Möglichkeit ist, SFINAE noch einmal zu verwenden ( enable_if kann auch von boost bezogen werden):

template<bool C, typename T = void>
struct enable_if {
  typedef T type;
};

template<typename T>
struct enable_if<false, T> { };

HAS_MEM_FUNC(toString, has_to_string);

template<typename T> 
typename enable_if<has_to_string<T, 
                   std::string(T::*)()>::value, std::string>::type
doSomething(T * t) {
   /* something when T has toString ... */
   return t->toString();
}

template<typename T> 
typename enable_if<!has_to_string<T, 
                   std::string(T::*)()>::value, std::string>::type
doSomething(T * t) {
   /* something when T doesnt have toString ... */
   return "T::toString() does not exist.";
}

Viel Spaß damit. Der Vorteil ist, dass es auch für überladene Memberfunktionen funktioniert, und auch für const Mitgliedsfunktionen (denken Sie daran, mit std::string(T::*)() const als Zeigertyp für die Mitgliedsfunktion!).

60voto

FireAphis Punkte 6430

Obwohl diese Frage schon zwei Jahre alt ist, möchte ich es wagen, meine Antwort hinzuzufügen. Hoffentlich verdeutlicht sie die vorherige, unbestreitbar ausgezeichnete Lösung. Ich habe die sehr hilfreichen Antworten von Nicola Bonelli und Johannes Schaub zu einer Lösung zusammengefasst, die IMHO besser lesbar und klarer ist und nicht die typeof Erweiterung:

template <class Type>
class TypeHasToString
{
    // This type won't compile if the second template parameter isn't of type T,
    // so I can put a function pointer type in the first parameter and the function
    // itself in the second thus checking that the function has a specific signature.
    template <typename T, T> struct TypeCheck;

    typedef char Yes;
    typedef long No;

    // A helper struct to hold the declaration of the function pointer.
    // Change it if the function signature changes.
    template <typename T> struct ToString
    {
        typedef void (T::*fptr)();
    };

    template <typename T> static Yes HasToString(TypeCheck< typename ToString<T>::fptr, &T::toString >*);
    template <typename T> static No  HasToString(...);

public:
    static bool const value = (sizeof(HasToString<Type>(0)) == sizeof(Yes));
};

Ich habe es mit gcc 4.1.2 getestet. Die Anerkennung geht hauptsächlich an Nicola Bonelli und Johannes Schaub, also gebt ihnen eine Stimme nach oben, wenn meine Antwort euch hilft :)

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