Qt Core Application:元对象/属性系统/反射
有限的RTTI
运行时类型信息(Run-Time Type Info,RTTI)是C++等静态语言的一个重要特性,C++的类型检查基本在编译期完成,仅提供了typeid、dynamic_cast两种用于支持运行时的类型信息,它们在前面的文章介绍过了:typeid用于获取类名,然而在不同的编译平台上这个方法得到的结果并不统一,尤其是gcc、clang等平台,需要进行转换;dynamic_cast常用于避免大指针去操控小内存(例如子类指针去操作父类),这是非安全的下行转换行为;
可见C++对RTTI的支持实在非常有限,而恰巧Qt中对RTTI的需求是较高的,例如一个基类指针Animal* 去保存若干派生类如Monkey* 、 Panda*,一旦编译完成,在已编译信息中就无法区分哪些函数、成员变量是属于哪个类的了,C++ RTTI唯一可以做的是通过上述typeid获取到其类名,而不能特别地访问某些类成员变量或者函数,莫谈如何操作;但同为静态语言的Java、C#等就有强大的RTTI支持,乃至成为其引以为傲的动态特性,这种支持运行时动态创建、访问类型的特性,称为反射(Reflection)机制。
由于存储反射需要的信息,需要耗费内存和性能,因此C++没有完整地兼容此特性以保证程序的性能,Qt采用了自己一套机制来实现RTTI,这种类似反射的机制由称为元对象系统(Meta-Object System)提供强大的支持。
Qt中的元对象系统
实现元对象系统,简单而言就是保存类的所有信息,例如成员函数、成员变量信息,并且通过某些方法就能够在运行时访问甚至操作这些实例。Qt的元对象系统至少实现了三件事:
对象间通信:即信号与槽机制,某个信号发出时触发某个槽函数,比回调函数方法更加简洁和灵活,我们需要做的仅是将二者connect,而无需考虑如何管理那些回调函数集合;
RTTI:类的信息可以保存在元对象中,在运行时通过访问元对象访问;
动态属性系统:提供了运行时访问和操作变量的灵活方法,在某些场景如QML等编程中极为有用。
使用而言实现这样的元对象系统并不复杂,Qt使用元对象编译器(Meta-Object Complier,MOC)来处理这样的C++源码,它根据代码额外生成moc_xxx.cpp,该文件会和源文件一起被编译和链接,以支持上述特性,其次它还会去除emit、slot、signal等标准C++编译器不能处理的关键字来实现正常编译。
值得注意的是使用MOC编译器是有条件的,需要满足:
直接或者间接继承QObject,QObject类也是Qt所有类的父类;
在头文件中类添加Q_OBJECT标识;
满足此两条件,创建一个类实例,元对象系统会记录最近基本信息,例如类名:
1
2
3
4Person* person = new Person;
const QMetaObject* metaPerson = person->metaObject();
qDebug()<<metaPerson->className(); //Person
属性系统
一个类文件是这样构造的: 1
2
3
4
5
6
7
8
9
10
11
12
class Person:public QObject{
Q_OBJECT
public:
void getName();
int getId();
private:
QString name;
int id;
double height;
};
反射成员变量字段
通过Q_PROPERTY可以将变量字段反射到属性系统,首先查看起始时有哪些属性:
1
2
3
4
5
6const QMetaObject* metaPerson = person->metaObject(); //person为一个类
for(int i=0; i<metaPerson->propertyCount(); i++){
QMetaProperty property = metaPerson->property(i);
qDebug() << i <<property.name();
}
//输出:0 objectName1
person->setObjectName("Eden");
现在尝试将double height反射到元对象系统: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person:public QObject{
Q_OBJECT
Q_PROPERTY(double height READ getHeight WRITE setHeight NOTIFY heightChanged)
public:
//属性访问函数:
double getHeight(){return height;}
void setHeight(double newheight){
height = newheight;
emit heightChanged(newheight);
}
signals:
void heightChanged(int height);
private:
double height;
};
此处添加了的Q_PROPERTY属性声明,使用了最常用的三个字段,指向读写函数和修改信号,当一个属性被设置成可写时,Qt建议应该使用Notify,重新执行会看到新增的反射字段:
1
2
3
4
5
6const QMetaObject* metaPerson = person->metaObject(); //person为一个类
for(int i=0; i<metaPerson->propertyCount(); i++){
QMetaProperty property = metaPerson->property(i);
qDebug() << i <<property.name();
}
//1 height QVariant::double1
2
3
4
5
6
7class Person:public QObject{
Q_OBJECT
Q_PROPERTY(double height MEMBER height) //将height和属性height绑定
private:
double height;
};
在main函数中通过setProperty和property就能够读写成员变量的值,其中读写的中间类型都是QVariant,通过这种方式不仅省去了调用读写接口的麻烦,还省略了成员是否私有化的问题。
1
2
3
4person->setProperty("height",QVariant::fromValue(180.0)); //设置属性,即写
//qDebug() << person->height; //通过成员变量读
QVariant m_height = person->property("height"); //属性读
qDebug() << m_height.toDouble();
反射成员函数
使用QMetaObject::invokeMethod
能够直接通过字符串来调用成员函数,因此Qt项目管理时跳转源码,不仅要关注那些通过函数名调用的位置,还需要留意那些通过元对象系统反射的函数,这些函数要么通过Q_INVOKABLE来声明反射,要么将其声明成slot函数(槽函数底层实际也是反射),例如一个简单的无参无返回值函数如下:
在成员类中定义: 1
2
3
4
5
6
7
8
9Q_INVOKABLE void test(){
qDebug() << "For Invoking!!";
}
//or
public slots:
void test(){
qDebug() << "For Invoking!!";
}1
2Person* person = new Person();
QMetaObject::invokeMethod(person,"test");
特别的,将test声明成signals
,编译上也是没问题的(注意使用空实现),但是此时调用QMetaObject::invokeMethod(person,"test")
,就不是调用函数的用法了,而是相当于emit
这个test
信号;
QMetaObject::invokeMethod
支持调用带参数(最多十个)和返回值的函数,使用Q_RETURN_ARG和Q_ARG指定类型和变量即可:
1
2
3
4
5
6
7
8
9
10
11//成员函数:
Q_INVOKABLE int add(int a,int b){
return a+b;
}
//调用:
Person* person = new Person();
int result; //声明结果变量
bool r = QMetaObject::invokeMethod(person,"add",Q_RETURN_ARG(int,result),Q_ARG(int,5),Q_ARG(int,10));
if(r)
qDebug() << result;
此外,invokeMethod支持同步、移步、跨线程调用等,这和信号和槽连接方法有关,例如通过事件队列(注意单线程中使用并不合适):
1
2Person* person = new Person();
QMetaObject::invokeMethod(person, "add", Qt::QueuedConnection, Q_RETURN_ARG(int,result),Q_ARG(int,5),Q_ARG(int,10));
反射类
定义这样一个类,这个类至少满足几个条件:
1.
间接/直接继承QObject
,且使用Q_Object
修饰类,如果是cmake构建确保MOC is ON
;
- 具有
public
的构造函数,且使用Q_INVOKABLE
进行反射;那么在其他函数调用时,就可用通过元对象系统创建一个类实例,且能够操作类内的成员变量和函数,尽管它们可能是1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person : public QObject{
Q_OBJECT
Q_PROPERTY(int num MEMBER num) //反射成员字段
public:
Q_INVOKABLE Person(){}
private:
Q_INVOKABLE void getNum(){ //反射成员函数
qDebug()<<"num ="<<num;
}
int num = 3;
};private
的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main(int argc, char* argv[])
{
QCoreApplication app(argc, argv);
const QMetaObject* metaObj = &Person::staticMetaObject;
QObject* obj = metaObj->newInstance();
obj->setProperty("num",QVariant::fromValue(30));
QMetaObject::invokeMethod(obj,"getNum"); //成功执行:num = 30
qDebug() << "done";
return app.exec();
}
qRegisterMetaType
上面我们仍然使用了&Person::staticMetaObject
来访问其静态对象,此处可以通过qRegisterMetaType
,真正将类反射到系统,达到通过字符串也可以访问的目的。注意这里的实现是有差异的,例如Qt 5.14以下
版本使用type来标识对象类型,5.14以上
使用fromName
、Qt6
使用fromType
等,此处仅展示5.14及以下
版本写法:
仍然采用上述的类函数,使用: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main(int argc, char* argv[])
{
QCoreApplication app(argc, argv);
qRegisterMetaType<Person>("Person");
int typeId = QMetaType::type("Person*");
const QMetaObject* metaObj = QMetaType::metaObjectForType(typeId);
QObject* obj = metaObj->newInstance();
obj->setProperty("num",30);
qDebug()<<obj->property("num");
QMetaObject::invokeMethod(obj,"getNum");
qDebug() << "done";
return app.exec();
}
发现此处是有错误的,提示: 1
2
3
4
5D:\Qt\5.12.12\msvc2017_64\include\QtCore\qmetatype.h:804: error: C2280: “Person::Person(const Person &)”: 尝试引用已删除的函数
D:\Qt_code\MyQtTest\MyQtTest.h(16): note: 编译器已在此处生成“Person::Person”
D:\Qt_code\MyQtTest\MyQtTest.h(16): note: “Person::Person(const Person &)”: 由于 基类 调用已删除或不可访问的函数“QObject::QObject(const QObject &)”,因此已隐式删除函数
D:\Qt\5.12.12\msvc2017_64\include\QtCore/qobject.h(449): note: “QObject::QObject(const QObject &)”: 已隐式删除函数
D:\Qt\5.12.12\msvc2017_64\include\QtCore/qmetatype.h(802): note: 在编译 类 模板 成员函数“void *QtMetaTypePrivate::QMetaTypeFunctionHelper<T,true>::Construct(void *,const void *)”时
因为QObject
的子类默认是禁用拷贝的,而这里意外地调用了拷贝构造函数,这里只能使用指针声明注册:
1
qRegisterMetaType<Person*>("Person*");
QVariant
等装载,在头文件中也增加注册Q_DECLARE_METATYPE(Person*)
;
总结
Qt通过MOC编译器实现了一种反射机制,这种反射实际上是一种静态反射,还不是真正意义上的反射机制,其通过底层原理通过一些特定的关键字、标识符等作为标记,为其添加moc源码实现,达到可以将成员函数、变量、类等注册到元对象系统的目的,使得其运行时可访问和进行各种数据操作。
参考链接: