C++ Builder 获取任意一个类或对象的类名

C++ Builder 参考手册 ➙ C++ Builder 获取任意一个类或对象的类名


前面写了两篇关于 C++ Builder 反射的文章,应该写第三篇了。前面两篇都是关于从 TObject 继承的类的反射,那么其他类到底和 TObject 继承的类有什么不同?又是因为什么影响了其他类的反射?与前一篇文章《C++ Builder 的反射 (二) - Reflection Factory》比较,其他的类只差一个获取类名,从 TObject 继承的类可以直接通过类名或对象指针得到类名字符串,而其他的类不能。本文程序和例子在 C++ Builder 10.2.3 版本 clang 32位 和 clang64位编译器测试通过。

  1. C++ Builder 获取类名的方法
    1.1. THsuanluClassName 类及使用方法
    1.2. THsuanluClassName 类的实现
  2. 获取类名的示例程序

1. C++ Builder 获取类名的方法

1.1. THsuanluClassName 类及使用方法

class THsuanluClassName
{
public:
    static UnicodeString _TObject_QualifiedClassName(UnicodeString s); // Vcl.StdCtrls.TMemo -> Vcl::Stdctrls::TMemo
    static UTF8String _TypeNameToQualifiedClassName32(UTF8String s);   // $Hsuanlu@Test@THsuanluClass -> Hsuanlu::Test::THsuanluClass
    static UTF8String _TypeNameToQualifiedClassName64(UTF8String s);   // N7Hsuanlu4Test13THsuanluClassE -> Hsuanlu::Test::THsuanluClass
    static UTF8String _TypeNameToQualifiedClassName(UTF8String s);     // Hsuanlu::Test::THsuanluClass
    static UTF8String _TypeNameToClassName(UTF8String s);              // THsuanluClass

    template 
    static UnicodeString GetClassName(std::true_type)
    {
        return T::ClassName(); // 这是从 TObject 继承的类的类名
    }
    template 
    static UnicodeString GetClassName(std::false_type)
    {
        return _TypeNameToClassName(typeid(T).name()); // 不是从 TObject 继承的类的类名
    }
    template 
    static UnicodeString GetClassName(T*) // 通过对象指针获取类名
    {
        return GetClassName(std::bool_constant<__is_base_of(TObject,T)>());
    }
    template 
    static UnicodeString GetClassName(void) // 直接获取类的类名
    {
        return GetClassName(std::bool_constant<__is_base_of(TObject,T)>());
    }

    template 
    static UnicodeString GetQualifiedClassName(std::true_type)
    {
        return _TObject_QualifiedClassName(T::QualifiedClassName()); // 这是从 TObject 继承的类的类名,带命名空间
    }
    template 
    static UnicodeString GetQualifiedClassName(std::false_type)
    {
        return _TypeNameToQualifiedClassName(typeid(T).name()); // 不是从 TObject 继承的类的类名,带命名空间
    }
    template 
    static UnicodeString GetQualifiedClassName(T*) // 通过对象指针获取类名,带命名空间
    {
        return GetQualifiedClassName(std::bool_constant<__is_base_of(TObject,T)>());
    }
    template 
    static UnicodeString GetQualifiedClassName(void) // 直接获取类的类名,带命名空间
    {
        return GetQualifiedClassName(std::bool_constant<__is_base_of(TObject,T)>());
    }
};

这个类里面都是静态函数和静态函数模板,在实际应用上,一般只需要调用这两个成员:
• THsuanluClassName::GetClassName 获取类名;
• THsuanluClassName::GetQualifiedClassName 获取带命名空间的类名;
由于其他函数在某些特定环境可能会使用,所以都放在了 public: 里面。

使用方法:由于类型只能做模板参数,对象指针可以做函数参数,所以带参数的函数版本的参数是对象指针,不带参数的函数利用模板参数直接获取类的类名。

例如:
s = THsuanluClassName::GetClassName();
s = THsuanluClassName::GetClassName(Memo1);
都将得到 L"TMemo" 字符串。

s = THsuanluClassName::GetQualifiedClassName();
s = THsuanluClassName::GetQualifiedClassName(Memo1);
都将得到 L"Vcl::Stdctrls::TMemo" 字符串

通过 this 指针也可以得到当前代码所在类的类名,例如在窗口 Form1 的代码里面 THsuanluClassName::GetClassName(this) 可以得到字符串 "TForm1"

1.2. THsuanluClassName 类的实现

#include 
#include  // $(BDS)\source\cpprtl\Source\libcxxabi\win64\include

UnicodeString THsuanluClassName::_TObject_QualifiedClassName(UnicodeString s)
{
    std::auto_ptr slNames(new TStringList);
    slNames->TrailingLineBreak = false;
    slNames->LineBreak = L".";
    slNames->Text = s;
    slNames->LineBreak = L"::";
    int iCount = slNames->Count;
    for(int iIdx=0; iIdxStrings[iIdx];      // 从 TObject 继承的类的命名空间
        slNames->Strings[iIdx] = s.SubString(1,1).UpperCase()         // 第一个字母大写
                               + s.SubString(2,s.Length()).LowerCase(); // 其余字母小写
    }
    return slNames->Text;
}

UTF8String THsuanluClassName::_TypeNameToQualifiedClassName32(UTF8String s)
{
    UTF8String sReal;
    const char *pChar = s.c_str();
    if(*pChar++ == '$')
    {
        bool bTemplate = false;
        while(*pChar)
        {
            if(bTemplate)
            {
                if(*pChar == '$')
                    break;
                sReal += *pChar;
            }
            else switch(*pChar)
            {
                case '%': bTemplate = true; break;
                case '@': sReal += "::"; break;
                default : sReal += *pChar; break;
            }
            pChar++;
        }
    }
    return sReal;
}

UTF8String THsuanluClassName::_TypeNameToQualifiedClassName64(UTF8String s)
{
    UTF8String sReal;
    char *psname = abi::__cxa_demangle(s.c_str(), nullptr, nullptr, nullptr);
    if(psname)
    {
        sReal = psname;
        free(psname);
    }
    return sReal;
}

UTF8String THsuanluClassName::_TypeNameToQualifiedClassName(UTF8String s)
{
    UTF8String sRealName = _TypeNameToQualifiedClassName32(s);
    if(sRealName.IsEmpty())
        sRealName = _TypeNameToQualifiedClassName64(s);
    if(sRealName.IsEmpty())
        sRealName = s;
    int iPos = sRealName.Pos("<");
    if(iPos > 0)
        sRealName = sRealName.SubString(1, iPos-1);
    return sRealName;
}

UTF8String THsuanluClassName::_TypeNameToClassName(UTF8String s)
{
    UTF8String sRealName = _TypeNameToQualifiedClassName(s);
    const char *pLastColon = std::strrchr(sRealName.c_str(),':');
    if(pLastColon)
        return pLastColon+1;
    return sRealName;
}

标准 C++ 的类,可以通过 typeid(T).name() 获取类型名,对于类来说,不仅仅是类名,还包含了命名空间和一些其他信息,这个格式并没有统一标准,gcc 和 clang 都给出了 abi::__cxa_demangle 函数把类型名转成类名,包含在头文件 cxxabi.h 里面。

C++ Builder 使用 clang 编译器,所以也应该有这个头文件,结果在 C++ Builder 的文件夹里面找到了在 $(BDS)\source\cpprtl\Source\libcxxabi\win64\include 这个文件夹里面,为什么在 Win64 里面,其他平台里面没有?经过测试,Win32 使用这个头文件可以编译通过,但是无法把类型名转成类名,Win64 可以转换成功,所以头文件就在 Win64 文件夹里面。

经过测试,C++ Builder 的 Win32 编译器类的类型名都是 $ 字符开头,命名空间之间用 @ 分割,例如 $Hsuanlu@Test@THsuanluClass 即为 Hsuanlu::Test::THsuanluClass,而 Win64 编译器生成的类名都是字母或数字开头的,所以程序就简单了,先判断如果 $ 开头使用这个规则,不是 $ 开头的使用 abi::__cxa_demangle 函数转换。

由于从 TObject 继承的类与标准 C++ 的类使用不同的 RTTI,TObject 是为了兼容 Delphi 程序,使用的是兼容的 Delphi RTTI,而其他的不从 TObject 继承的类,都是使用的标准 C++ RTTI,这两种 RTTI 不兼容,如果用 typeid(T).name() 获取 TObject 继承的类的类型名会抛出内存访问错误的异常,所以程序必须先判断是否从 TObject 继承,然后再判断是否 $ 开头的类型名。

函数 _TObject_QualifiedClassName 是把 TObject::QualifiedClassName 获取到的带命名空间的类名转成 C++ 格式,因为直接获取到的命名空间和类名之间用 . 分割,需要改成 ::,并且命名空间的大小写也和实际不符,所有这些类的命名空间应该是开头字母大写,其余小写,所以用这个函数转换。

函数 _TypeNameToQualifiedClassName32 把 $ 开头的类型名转成带命名空间的类名,如果类型是模板,类名会在第一个 % 和之后的第一个 $ 之间。

函数 _TypeNameToQualifiedClassName64 把字母和数字开头的类型名,使用 abi::__cxa_demangle 转成类名。

函数 _TypeNameToQualifiedClassName 把类型名转成带命名空间的类名,先使用 32 位那个 $ 开头的转换,如果转换失败,即开头不是 $,那么就使用 64 位那个字母或数字开头的类型名转换,如果都失败了,直接返回类型名。如果是模板,把模板参数去掉,即把第一个 < 和后面的东西删掉。

函数 _TypeNameToClassName 把类型名转成类名,先调用 _TypeNameToQualifiedClassName 得到带命名空间的类名,然后把命名空间去掉,保留最后一个 : 后面的部分。

由于 typeid(T).name() 返回的类型名是 UTF-8 编码的,所以处理类型名的函数都是 UTF8String 类型的参数和返回值,在 GetClassName 和 GetQualifiedClassName 里面调用并转成 UnicodeString 类型的,这也说明类型是可以用汉字或其他非英语语言的。

函数模板 GetClassName 使用 __is_base_of(TObject,T) 判断 T 是为 TObject 或他的子类,如果是,使用 std::true_type 参数的函数,如果否,使用 std::false_type 参数的函数。使用这个方法,而不是使用 if else,原因是 if else 无论是否满足条件都要编译,不满足条件会语法错误,而这个方法,不满足条件不但不会执行,也不会编译和检查语法错误。


2. 获取类名的示例程序

通过自己写的类 (非 TObject 继承) 和控件类 (TObject 继承) 测试获取类名。
包括带命名空间和不带命名空间的,同时也测试了汉字类名。

namespace Hsuanlu {
    namespace Test {
        class THsuanluClass
        {
        public:
            THsuanluClass(){}
            virtual ~THsuanluClass(){}

            class TTest1{};
        };
    }

    class 玄坴测试类
    {
    public:
        玄坴测试类(){}
    };

    template
    class THsuanluTemp : public Test::THsuanluClass
    {
    };
}

using namespace Hsuanlu;
using namespace Hsuanlu::Test;

void __fastcall TForm1::Button1Click(TObject *Sender)
{
    THsuanluClass Obj;
    THsuanluClass::TTest1 t1;
    玄坴测试类 Test;
    const THsuanluClass Obj1;
    THsuanluTemp Temp;

    Memo1->Lines->Add(THsuanluClassName::GetClassName(this));
    Memo1->Lines->Add(THsuanluClassName::GetClassName(Memo1));
    Memo1->Lines->Add(THsuanluClassName::GetClassName(&Obj));
    Memo1->Lines->Add(THsuanluClassName::GetClassName(&Obj1));
    Memo1->Lines->Add(THsuanluClassName::GetClassName(&t1));
    Memo1->Lines->Add(THsuanluClassName::GetClassName(&Test));
    Memo1->Lines->Add(L"");
    Memo1->Lines->Add(THsuanluClassName::GetClassName());
    Memo1->Lines->Add(THsuanluClassName::GetClassName());
    Memo1->Lines->Add(THsuanluClassName::GetClassName());
    Memo1->Lines->Add(THsuanluClassName::GetClassName());
    Memo1->Lines->Add(THsuanluClassName::GetClassName<玄坴测试类>());
    Memo1->Lines->Add(THsuanluClassName::GetClassName>());
    Memo1->Lines->Add(THsuanluClassName::GetClassName(&Temp));
    Memo1->Lines->Add(L"");
    Memo1->Lines->Add(THsuanluClassName::GetQualifiedClassName(this));
    Memo1->Lines->Add(THsuanluClassName::GetQualifiedClassName(Memo1));
    Memo1->Lines->Add(THsuanluClassName::GetQualifiedClassName(&Obj));
    Memo1->Lines->Add(THsuanluClassName::GetQualifiedClassName(&Obj1));
    Memo1->Lines->Add(THsuanluClassName::GetQualifiedClassName(&t1));
    Memo1->Lines->Add(THsuanluClassName::GetQualifiedClassName(&Test));
    Memo1->Lines->Add(L"");
    Memo1->Lines->Add(THsuanluClassName::GetQualifiedClassName());
    Memo1->Lines->Add(THsuanluClassName::GetQualifiedClassName());
    Memo1->Lines->Add(THsuanluClassName::GetQualifiedClassName());
    Memo1->Lines->Add(THsuanluClassName::GetQualifiedClassName());
    Memo1->Lines->Add(THsuanluClassName::GetQualifiedClassName<玄坴测试类>());
    Memo1->Lines->Add(THsuanluClassName::GetQualifiedClassName>());
    Memo1->Lines->Add(THsuanluClassName::GetQualifiedClassName(&Temp));
}

运行结果:

获取类名测试程序的运行结果

相关:

  • C++ Builder 的反射 (三) - 通用 Reflection Factory
  • C++ Builder 的反射 (二) - Reflection Factory
  • C++ Builder 的反射 (一) - Reflection 简单实现
  • 枚举控件所有的属性、事件和方法
  • 枚举窗口内所有的控件
  • C++ Builder 的枚举类型
  • C / C++ 可变参数的函数
  • C / C++ 可变参数的宏,__VA_ARGS__,...
  • C++ 可变参数的模板
  • C++ Builder 的 PME 架构

C++ Builder 参考手册 ➙ C++ Builder 获取任意一个类或对象的类名

你可能感兴趣的:(C++ Builder 获取任意一个类或对象的类名)