C ++ Name des Typs in der Vorlage abrufen

76

Ich schreibe einige Vorlagenklassen zum Parsen einiger Textdatendateien, und als solche ist es wahrscheinlich, dass die große Mehrheit der Analysefehler auf Fehler in der Datendatei zurückzuführen ist, die größtenteils nicht von Programmierern geschrieben wurden und daher benötigt werden eine nette Nachricht darüber, warum die App nicht geladen werden konnte, zB so etwas wie:

Fehler beim Parsen von example.txt. Der Wert ("notaninteger") des Schlüssels [MySectiom] ist kein gültiger int

Ich kann die Datei-, Abschnitts- und Schlüsselnamen aus den Argumenten ermitteln, die an die Vorlagenfunktion und die Mitgliedsvariablen in der Klasse übergeben wurden. Ich bin mir jedoch nicht sicher, wie ich den Namen des Typs ermitteln soll, in den die Vorlagenfunktion konvertieren möchte.

Mein aktueller Code sieht so aus, mit Spezialisierungen für einfache Zeichenfolgen und dergleichen:

template<typename T> T GetValue(const std::wstring &section, const std::wstring &key)
{
    std::map<std::wstring, std::wstring>::iterator it = map[section].find(key);
    if(it == map[section].end())
        throw ItemDoesNotExist(file, section, key)
    else
    {
        try{return boost::lexical_cast<T>(it->second);}
        //needs to get the name from T somehow
        catch(...)throw ParseError(file, section, key, it->second, TypeName(T));
    }
}

Ich muss lieber nicht für jeden Typ, den die Datendateien verwenden, bestimmte Überladungen vornehmen, da es viele davon gibt ...

Außerdem benötige ich eine Lösung, die keinen Laufzeitaufwand verursacht, es sei denn, eine Ausnahme tritt auf, dh ich möchte eine Lösung zur vollständigen Kompilierungszeit, da dieser Code tonnenweise aufgerufen wird und die Ladezeiten bereits etwas lang werden.

EDIT: Ok, das ist die Lösung, die ich mir ausgedacht habe:

Ich habe einen Typ, der Folgendes enthält

#pragma once
template<typename T> const wchar_t *GetTypeName();

#define DEFINE_TYPE_NAME(type, name) \
    template<>const wchar_t *GetTypeName<type>(){return name;}

Dann kann ich das Makro DEFINE_TYPE_NAME verwenden, um CPP-Dateien für jeden Typ zu erstellen, mit dem ich mich befassen muss (z. B. in der CPP-Datei, die den Typ definiert, mit dem begonnen werden soll).

Der Linker kann dann die entsprechende Spezialisierung finden, solange sie irgendwo definiert wurde, oder andernfalls einen Linkerfehler auslösen, damit ich den Typ hinzufügen kann.

Fire Lancer
quelle
1
Nicht wirklich relevant für Ihre Frage, aber Sie möchten möglicherweise auch map.find (Abschnitt) verwenden, wenn Sie auf den Abschnitt zugreifen, es sei denn, Sie möchten absichtlich einen leeren Abschnitt erstellen.
Idan K

Antworten:

40

Die Lösung von Jesse Beder ist wahrscheinlich die beste, aber wenn Sie die Namen, die typeid Ihnen gibt, nicht mögen (ich denke, gcc gibt Ihnen beispielsweise verstümmelte Namen), können Sie Folgendes tun:

template<typename T>
struct TypeParseTraits;

#define REGISTER_PARSE_TYPE(X) template <> struct TypeParseTraits<X> \
    { static const char* name; } ; const char* TypeParseTraits<X>::name = #X


REGISTER_PARSE_TYPE(int);
REGISTER_PARSE_TYPE(double);
REGISTER_PARSE_TYPE(FooClass);
// etc...

Und dann benutze es gerne

throw ParseError(TypeParseTraits<T>::name);

BEARBEITEN:

Sie können die beiden auch kombinieren, namein eine Funktion ändern , die standardmäßig aufgerufen wird, typeid(T).name()und sich dann nur auf Fälle spezialisieren, in denen dies nicht akzeptabel ist.

Logan Capaldo
quelle
Hinweis: Dieser Code wird nicht kompiliert, wenn Sie vergessen, REGISTER_PARSE_TYPE für einen von Ihnen verwendeten Typ zu definieren. Ich habe zuvor einen ähnlichen Trick verwendet (in Code ohne RTTI) und er hat sehr gut funktioniert.
Tom Leys
1
Ich musste den Namen in g ++ 4.3.0 außerhalb der Struktur verschieben, weil "Fehler: Ungültige Initialisierung des statischen Datenelements vom nicht integralen Typ 'const char *' in der Klasse"; und natürlich wird das Schlüsselwort 'struct' zwischen <> und TypeParseTraits benötigt und die Definition sollte mit einem Semikolon abgeschlossen werden.
FuzzyTew
4
Nun, das Auslassen des Semikolons war beabsichtigt, um Sie zu zwingen, es am Ende des Makroaufrufs zu verwenden, aber danke für die Korrekturen.
Logan Capaldo
Ich erhalte den folgenden Fehler:error: '#' is not followed by a macro parameter
Kratsg
@kratsg - das liegt daran, dass '#x' am Ende '#X' sein sollte (Großbuchstaben, um den Makroparametern zu entsprechen) - ich werde die Antwort korrigieren.
Amdn
69

Die Lösung ist

typeid(T).name()

Dies gibt std :: type_info zurück .

Jesse Beder
quelle
6
Beachten Sie, dass es konform ist, für jeden Typ dieselbe Zeichenfolge zurückzugeben (obwohl ich nicht glaube, dass ein Compiler dies tun würde).
Motti
3
Oder um bei verschiedenen Ausführungen eine andere Zeichenfolge für denselben Typ zurückzugeben ... (wieder nicht, dass ich denke, dass ein vernünftiger Compiler das tun würde).
Emily L.
3
Ich möchte nur darauf hinweisen, wie hässlich der Vorname sein kann : typeid(simd::double3x4).name() = "N4simd9double3x4E". typeid(simd::float4).name() = "Dv4_f"C ++ 17, Xcode 10.1.
Andreas verabscheut Zensur
1
Tatsächlich. typeid(T).name()ist der kanonische Weg, dies zu tun, aber nur sehr wenige Compiler geben unverwirrte Namen zurück. Das einzige, mit dem ich persönlich vertraut bin, ist MSVC. Abhängig vom verwendeten Compiler besteht auch die Möglichkeit, dass einige Typinformationen zu Funktionstypen verloren gehen. Dies ist in diesem Fall jedoch wahrscheinlich irrelevant.
Justin Time - Stellen Sie Monica
45

typeid(T).name() ist die Implementierung definiert und garantiert keine vom Menschen lesbare Zeichenfolge.

Lesen von cppreference.com :

Gibt eine implementierungsdefinierte nullterminierte Zeichenfolge zurück, die den Namen des Typs enthält. Es werden keine Garantien gegeben, insbesondere kann die zurückgegebene Zeichenfolge für mehrere Typen identisch sein und zwischen Aufrufen desselben Programms wechseln.

...

Mit Compilern wie gcc und clang kann die zurückgegebene Zeichenfolge durch c ++ filt -t geleitet werden, um in eine für Menschen lesbare Form konvertiert zu werden.

In einigen Fällen gibt gcc jedoch nicht die richtige Zeichenfolge zurück. Zum Beispiel habe ich auf meinem Computer gcc mit -std=c++11und innerhalb der Template-Funktion typeid(T).name()zurückgegeben "j"für "unsigned int". Es ist so genannter verstümmelter Name. Verwenden Sie die Funktion abi :: __ cxa_demangle () (nur gcc), um einen echten Typnamen zu erhalten :

#include <string>
#include <cstdlib>
#include <cxxabi.h>

template<typename T>
std::string type_name()
{
    int status;
    std::string tname = typeid(T).name();
    char *demangled_name = abi::__cxa_demangle(tname.c_str(), NULL, NULL, &status);
    if(status == 0) {
        tname = demangled_name;
        std::free(demangled_name);
    }   
    return tname;
}
Bunkar
quelle
1
Ist es nicht Speicherverlust haben freein if?
Tomáš Zato - Wiedereinsetzung Monica
2
Nein, denn der Zeiger zeigt auf, nullptrwenn der Status nicht 0 ist.
Henry Schreiner
1
Ich möchte hinzufügen, dass es wahrscheinlich am besten ist, nach der Existenz von gcc oder clang zu suchen und wenn nicht standardmäßig, das hier gezeigte Entwirren nicht durchzuführen .
Gott der Lamas
20

Wie von Bunkar Typid (T) erwähnt, ist .name die Implementierung definiert.

Um dieses Problem zu vermeiden, können Sie die Boost.TypeIndex- Bibliothek verwenden.

Zum Beispiel:

boost::typeindex::type_id<T>().pretty_name() // human readable
Andrey
quelle
Dies ist sehr nützlich, um beim Aufrufen von Funktionen die Namen der Vorlagentypen herauszufinden. Es hat ziemlich gut für mich funktioniert.
Fernando
1
Beachten Sie, dass hübsch_name () oder roher_name () noch implementierungsdefiniert ist. Auf MSVC für eine Struktur A; Sie würden erhalten: "struct A" während auf gcc / clang: "A".
Daminetreg
Beeindruckend. boostwieder für den Sieg. erstaunlich , was Boost funktioniert ohne Compiler - Unterstützung ( auto, regex, foreach, threads, static_assert, etc, etc ... Unterstützung vor Compiler / C ++ - Standard - Support).
Trevor Boyd Smith
14

Die Antwort von Logan Capaldo ist richtig, kann aber geringfügig vereinfacht werden, da es nicht erforderlich ist, die Klasse jedes Mal zu spezialisieren. Man kann schreiben:

// in header
template<typename T>
struct TypeParseTraits
{ static const char* name; };

// in c-file
#define REGISTER_PARSE_TYPE(X) \
    template <> const char* TypeParseTraits<X>::name = #X

REGISTER_PARSE_TYPE(int);
REGISTER_PARSE_TYPE(double);
REGISTER_PARSE_TYPE(FooClass);
// etc...

Auf diese Weise können Sie auch die Anweisungen REGISTER_PARSE_TYPE in eine C ++ - Datei einfügen ...

Rhomu
quelle
8

Als Umformulierung von Andreys Antwort:

Die Boost TypeIndex- Bibliothek kann zum Drucken von Typnamen verwendet werden.

In einer Vorlage kann dies wie folgt lauten

#include <boost/type_index.hpp>
#include <iostream>

template<typename T>
void printNameOfType() {
    std::cout << "Type of T: " 
              << boost::typeindex::type_id<T>().pretty_name() 
              << std::endl;
}
chrisb2244
quelle
5

Dieser Trick wurde unter einigen anderen Fragen erwähnt, aber noch nicht hier.

Alle wichtigen Compiler unterstützen __PRETTY_FUNC__(GCC & Clang) / __FUNCSIG__(MSVC) als Erweiterung.

Bei Verwendung in einer Vorlage wie dieser:

template <typename T> const char *foo()
{
    #ifdef _MSC_VER
    return __FUNCSIG__;
    #else
    return __PRETTY_FUNCTION__;
    #endif
}

Es erzeugt Strings in einem compilerabhängigen Format, die unter anderem den Namen von enthalten T.

ZB foo<float>()kehrt zurück:

  • "const char* foo() [with T = float]" auf GCC
  • "const char *foo() [T = float]" auf Clang
  • "const char *__cdecl foo<float>(void)" auf MSVC

Sie können die Typnamen einfach aus diesen Zeichenfolgen analysieren. Sie müssen nur herausfinden, wie viele Junk-Zeichen Ihr Compiler vor und nach dem Typ einfügt.

Sie können dies sogar zur Kompilierungszeit vollständig tun.


Die resultierenden Namen können zwischen verschiedenen Compilern leicht variieren. Beispielsweise lässt GCC Standardvorlagenargumente aus und MSVC stellt Klassen das Wort voran class.


Hier ist eine Implementierung, die ich verwendet habe. Alles wird zur Kompilierungszeit erledigt.

Anwendungsbeispiel:

std::cout << TypeName<float>() << '\n';
std::cout << TypeName(1.2f); << '\n';

Implementierung:

#include <array>
#include <cstddef>

namespace impl
{
    template <typename T>
    constexpr const auto &RawTypeName()
    {
        #ifdef _MSC_VER
        return __FUNCSIG__;
        #else
        return __PRETTY_FUNCTION__;
        #endif
    }

    struct RawTypeNameFormat
    {
        std::size_t leading_junk = 0, trailing_junk = 0;
    };

    // Returns `false` on failure.
    inline constexpr bool GetRawTypeNameFormat(RawTypeNameFormat *format)
    {
        const auto &str = RawTypeName<int>();
        for (std::size_t i = 0;; i++)
        {
            if (str[i] == 'i' && str[i+1] == 'n' && str[i+2] == 't')
            {
                if (format)
                {
                    format->leading_junk = i;
                    format->trailing_junk = sizeof(str)-i-3-1; // `3` is the length of "int", `1` is the space for the null terminator.
                }
                return true;
            }
        }
        return false;
    }

    inline static constexpr RawTypeNameFormat format =
    []{
        static_assert(GetRawTypeNameFormat(nullptr), "Unable to figure out how to generate type names on this compiler.");
        RawTypeNameFormat format;
        GetRawTypeNameFormat(&format);
        return format;
    }();
}

// Returns the type name in a `std::array<char, N>` (null-terminated).
template <typename T>
[[nodiscard]] constexpr auto CexprTypeName()
{
    constexpr std::size_t len = sizeof(impl::RawTypeName<T>()) - impl::format.leading_junk - impl::format.trailing_junk;
    std::array<char, len> name{};
    for (std::size_t i = 0; i < len-1; i++)
        name[i] = impl::RawTypeName<T>()[i + impl::format.leading_junk];
    return name;
}

template <typename T>
[[nodiscard]] const char *TypeName()
{
    static constexpr auto name = CexprTypeName<T>();
    return name.data();
}
template <typename T>
[[nodiscard]] const char *TypeName(const T &)
{
    return TypeName<T>();
}
HolyBlackCat
quelle
2

Wenn Sie einen hübschen Namen möchten, kann die Lösung von Logan Capaldo nicht mit komplexen Datenstrukturen umgehen: REGISTER_PARSE_TYPE(map<int,int>) und typeid(map<int,int>).name()gibt mir ein Ergebnis vonSt3mapIiiSt4lessIiESaISt4pairIKiiEEE

Es gibt eine weitere interessante Antwort mit unordered_mapoder mapvon https://en.cppreference.com/w/cpp/types/type_index .

#include <iostream>
#include <unordered_map>
#include <map>
#include <typeindex>
using namespace std;
unordered_map<type_index,string> types_map_;

int main(){
    types_map_[typeid(int)]="int";
    types_map_[typeid(float)]="float";
    types_map_[typeid(map<int,int>)]="map<int,int>";

    map<int,int> mp;
    cout<<types_map_[typeid(map<int,int>)]<<endl;
    cout<<types_map_[typeid(mp)]<<endl;
    return 0;
}
Voyager
quelle
2

typeid(uint8_t).name() ist nett, aber es gibt "unsigned char" zurück, während Sie "uint8_t" erwarten können.

Dieser Code gibt Ihnen den entsprechenden Typ zurück

#define DECLARE_SET_FORMAT_FOR(type) \
    if ( typeid(type) == typeid(T) ) \
        formatStr = #type;

template<typename T>
static std::string GetFormatName()
{
    std::string formatStr;

    DECLARE_SET_FORMAT_FOR( uint8_t ) 
    DECLARE_SET_FORMAT_FOR( int8_t ) 

    DECLARE_SET_FORMAT_FOR( uint16_t )
    DECLARE_SET_FORMAT_FOR( int16_t )

    DECLARE_SET_FORMAT_FOR( uint32_t )
    DECLARE_SET_FORMAT_FOR( int32_t )

    DECLARE_SET_FORMAT_FOR( float )

    // .. to be exptended with other standard types you want to be displayed smartly

    if ( formatStr.empty() )
    {
        assert( false );
        formatStr = typeid(T).name();
    }

    return formatStr;
}
jpo38
quelle
Das ist großartig, aber warum nicht return #type;stattdessen?
Kleiner Helfer
@LittleHelper: Du hast recht, das würde auch funktionieren ...
jpo38
1

Ich lasse es einfach dort. Wenn jemand es noch braucht, können Sie dies verwenden:

template <class T>
bool isString(T* t) { return false;  } // normal case returns false

template <>
bool isString(char* t) { return true; }  // but for char* or String.c_str() returns true
.
.
.

Dies wird nur CHECK Typ nicht GET es und nur für 1 Typ oder 2.

Xar
quelle