C ++ Отримати ім'я типу в шаблоні


78

Я пишу кілька класів шаблонів для синтаксичного аналізу деяких текстових файлів даних, і, як такий, переважно більшість помилок синтаксичного аналізу будуть пов'язані з помилками у файлі даних, які здебільшого не написані програмістами, і тому вони потребують гарне повідомлення про те, чому програму не вдалося завантажити, наприклад щось на зразок:

Помилка синтаксичного аналізу example.txt. Значення ("notaninteger") ключа [MySectiom] не є дійсним int

Я можу опрацювати імена файлів, розділів та ключів з аргументів, переданих функції шаблону та змінних членів у класі, однак я не впевнений, як отримати ім'я типу, який намагається перетворити функція шаблону.

Мій поточний код виглядає так, що спеціалізується на простих рядках і таких:

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));
    }
}

Мені, скоріше, не доведеться робити конкретні перевантаження для кожного типу, який можуть використовувати файли даних, оскільки їх багато ...

Крім того, мені потрібне рішення, яке не спричиняє жодних накладних витрат на виконання, якщо не виникає виняток, тобто саме те, що я хочу, це рішення часу компіляції, оскільки цей код називається тоннами разів, а час завантаження вже стає дещо довшим.

EDIT: Добре, це рішення, яке я придумав:

У мене є type.h, що стосується наступного

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

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

Тоді я можу використовувати макрос DEFINE_TYPE_NAME для створення файлів cpp для кожного типу, з яким мені потрібно мати справу (наприклад, у файлі cpp, який визначив тип для початку).

Потім компонувальник зможе знайти відповідну спеціалізацію шаблону до тих пір, поки вона була десь визначена, або викинути помилку посилання в іншому випадку, щоб я міг додати тип.


1
насправді не відповідає вашому питанню, але, можливо, ви захочете використовувати map.find (section) при доступі до розділу, якщо ви навмисно не хочете створити порожній розділ.
Ідан К

Відповіді:


41

Рішення Джессі Бедера, мабуть, найкраще, але якщо вам не подобаються імена, які дає вам typeid (я думаю, gcc дає вам спотворені імена), ви можете зробити щось на зразок:

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...

А потім використовуйте це як

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

РЕДАГУВАТИ:

Ви також можете поєднати ці два, змінити nameна функцію, яка за замовчуванням дзвонить, typeid(T).name()а потім спеціалізуватися лише на тих випадках, коли це неприйнятно.


Примітка: Цей код не буде скомпільований, якщо ви забудете визначити REGISTER_PARSE_TYPE для типу, який ви використовуєте. Я вже використовував подібний трюк раніше (у коді без RTTI), і він спрацював дуже добре.
Том Лейс

1
Мені довелося перемістити ім'я за межі структури у g ++ 4.3.0 через "помилку: недійсна ініціалізація в класі статичного елемента даних неінтегрального типу 'const char *'"; і, звичайно, ключове слово 'struct' потрібне між <> і TypeParseTraits, а визначення слід закінчувати крапкою з комою.
fuzzyTew

4
Добре, якщо залишити крапку з комою навмисно, змусити вас використовувати її в кінці виклику макросу, але дякую за виправлення.
Логан Капальдо,

Я отримую наступну помилку:error: '#' is not followed by a macro parameter
kratsg

@kratsg - це тому, що в кінці "#x" має бути "#X" (у верхньому регістрі, щоб відповідати параметру макросу) - я виправлю відповідь.
amdn

71

Рішення є

typeid(T).name()

який повертає std :: type_info .


6
Майте на увазі, що сумісно повертати однаковий рядок для кожного типу (хоча я не думаю, що будь-який компілятор це зробив би).
Мотті

3
Або повернути інший рядок для того самого типу під час різного виконання ... (знову ж таки не те, що я думаю, що це зробить будь-який розумний компілятор).
Емілі Л.

4
Я просто хотів би відзначити, як негарно дане ім'я може бути: typeid(simd::double3x4).name() = "N4simd9double3x4E". typeid(simd::float4).name() = "Dv4_f"C ++ 17, Xcode 10.1.
Андреас ненавидить цензуру

1
Справді. typeid(T).name()це канонічний спосіб зробити це, але дуже мало компіляторів повертає неруковані імена; єдиний, кого я особисто знайомий з цим, це MSVC. Залежно від використовуваного компілятора, також існує ймовірність того, що він може втратити деяку інформацію про типи типів функцій, але це, мабуть, не має значення в цьому випадку.
Час Джастіна -

typeid(T).name()не повертається std::type_info, але char const *.
lmat

46

typeid(T).name() визначена реалізацією і не гарантує читабельний рядок.

Читання cppreference.com :

Повертає визначений реалізацією рядок символів із нулем, що містить ім'я типу. Гарантії не даються, зокрема, повернутий рядок може бути ідентичним для декількох типів і змінюватись між викликами однієї і тієї ж програми.

...

За допомогою компіляторів, таких як gcc і clang, повернутий рядок може бути прокладений через c ++ filt -t для перетворення в зручну для читання форму.

Але в деяких випадках gcc не повертає правильний рядок. Наприклад, на моїй машині у мене є gcc ти -std=c++11і внутрішня функція шаблону typeid(T).name()повертається "j"для "unsigned int". Це так зване спотворене ім'я. Щоб отримати справжнє ім'я типу, використовуйте функцію abi :: __ cxa_demangle () (лише gcc):

#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;
}

1
Це не витік пам'яті , щоб мати freeв if?
Томаш Зато - відновити Моніку

2
Ні, тому що вказівник вказує, nullptrякщо статус не дорівнює 0.
Генрі Шрайнер

2
Я хотів би додати, що, мабуть, найкраще перевірити наявність gcc або clang, а якщо не за замовчуванням, не робити демонтажу, як показано тут .
бог лам

20

Як згадував Bunkar typeid (T) .name визначено реалізацією.

Щоб уникнути цієї проблеми, ви можете використовувати Boost.TypeIndex бібліотеку .

Наприклад:

boost::typeindex::type_id<T>().pretty_name() // human readable

Це дуже корисно для з’ясування імен типів шаблонів при виклику функцій. У мене це спрацювало досить добре.
Фернандо

1
Зверніть увагу, що досить_ім'я () або необроблене_ім'я () все ще визначено реалізацією. На MSVC для структури A; ви отримаєте: "struct A", перебуваючи у gcc / clang: "A".
daminetreg

Ого. boostзнову для перемоги. Дивно те , що підвищення робить без підтримки компілятора ( auto, regex, foreach, threads, static_assertі т.д., і т.д. ... підтримки до того укладачі / C ++ - стандартна підтримка).
Тревор Бойд Сміт

14

Відповідь Логана Капальдо є правильною, але її можна незначно спростити, оскільки не потрібно щоразу спеціалізуватись на класі. Можна написати:

// 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...

Це також дозволяє помістити інструкції REGISTER_PARSE_TYPE у файл C ++ ...


8

Як перефразування відповіді Андрія:

Бібліотеку Boost TypeIndex можна використовувати для друку назв типів.

Усередині шаблону це може виглядати наступним чином

#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;
}

6

Цей фокус згадувався під кількома іншими питаннями, але поки що не тут.

Підтримка всіх основних компіляторів __PRETTY_FUNC__(GCC & Clang) /__FUNCSIG__ (MSVC) як розширення.

При використанні у такому шаблоні:

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

Він створює рядки у залежному від компілятора форматі, які містять, серед іншого, ім'я T .

Напр foo<float>() повертається:

  • "const char* foo() [with T = float]" на GCC
  • "const char *foo() [T = float]" на Кланг
  • "const char *__cdecl foo<float>(void)" на MSVC

Ви можете легко проаналізувати імена типів із цих рядків. Вам просто потрібно з'ясувати, скільки "сміттєвих" символів вставляє ваш компілятор до і після типу.

Ви навіть можете це зробити повністю під час компіляції.


Отримані імена можуть дещо відрізнятися у різних компіляторів. Наприклад, GCC опускає аргументи шаблону за замовчуванням, а MSVC префіксує класи перед словом class.


Ось реалізація, яку я використовував. Все робиться під час компіляції.

Приклад використання:

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

Реалізація:

#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>();
}

2

Якщо ви хочете досить_ім'я, рішення Логана Капалдо не може мати справу зі складною структурою даних: REGISTER_PARSE_TYPE(map<int,int>) і typeid(map<int,int>).name()дає мені результатSt3mapIiiSt4lessIiESaISt4pairIKiiEEE

Існує ще один цікавий відповідь , використовуючи unordered_mapабо mapприходить з 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;
}

2

typeid(uint8_t).name() приємно, але він повертає "unsigned char", тоді як ви можете очікувати "uint8_t".

Цей фрагмент коду поверне вам відповідний тип

#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;
}

Це чудово, але чому б не зробити return #type;замість цього?
Маленький Помічник

@LittleHelper: Ви маєте рацію, це теж могло б спрацювати ...
jpo38

1

Я просто залишаю це там. Якщо комусь це все одно знадобиться, ви можете скористатися цим:

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
.
.
.

Це дозволить перевірити лише тип, а не ОТРИМАТИ, і лише для 1 типу або 2.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.