Qt 元对象系统 - 反射篇

反射作用

反射的作用就是可以通过字符串创建对象,操作类的成员数据,调用类的方法。

举一个场景例子:

实现一个对象工厂,工厂有个函数叫QObject* createObject(QString className);传递一个className,返回一个该类的对象。

如果没有反射:大概率是这样写

cpp 复制代码
QObject* createObject(QString className){
	if(className == "A"){
		return new A();
	}
	else if(className == "B"){
		return new B();
	}
	//......
}

这里就不是根据字符串创建对象。因为我们把A(),B()明确地写在代码里了。当有类新增时,我们需要添加新的else if分支。

如果有反射:就可以这样写

cpp 复制代码
QObject* createObject(QString className){
	MetaObject o = MataObject::fromName(className);
	return o.newInstance();
}

这里,新增类时,工厂的代码不需要任何改动。

通过字符串新建对象意味着可以运行时才确定创建什么对象。因为字符串可以在运行时才确定,他可能来自用户的输入。没有反射,就只能在工厂里用if else 枚举所有可能的类了。

另一种用途就是可以遍历某个类所有成员变量的名字。这些成员变量的名字可能能用作其他用途,比如在运行时判断一个对象有没有名为xxx的成员变量或者成员函数。比如他们的名字刚好就是对应某个sql语句的数据字段。

QT中的反射

QT的反射是通过MOC编译器进行的,MOC编译器把QT的特殊语法转化为标准编译器认识的C++语法,并添加一些函数实现,从而支持反射。与java的反射不同。java的反射不需要程序员做任何事情就能使用反射,QT的反射则需要程序员给需要被反射的类多写几行代码才能完成。下面的段落会介绍反射类,反射字段,反射方法需要程序员多做哪些事情。

反射字段

反射字段就是通过字符串修改或者访问某个对象的字段,这个字段可以是public,private,protect的。一个字段能被反射,需要具备如下条件。

  1. 所属类直接或者间接继承自QObject。
  2. 类有Q_OBJECT宏定义。
  3. 有Q_PROPERTY指向。
    举例如下:
cpp 复制代码
class Student : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString mName MEMBER mName)
public:
    explicit Student(QObject *parent = nullptr);
    QString mName; //可以被反射,因为有Q_PROPERTY指向。(第3行)

    QString mSex; //不能被反射,因为没有Q_PROPERTY指向。
};

其中mName可以被反射,但mSex不能被反射。

使用反射字段

cpp 复制代码
	Student stu;
    stu.setProperty("mName","张三");
    stu.setProperty("mSex","男");
    qDebug() << stu.mName; //输出张三;
    qDebug() << stu.mSex;  //输出空字符串。因为mSex字段没有被反射。
    qDebug() << stu.property("mSex").toString(); //输出男,这里是因为动态地给stu这个对象添加了一个字段mSex,但是它和原本的mSex没有任何关系。

    stu.mName = "张三变身";
    qDebug() << stu.property("mName").toString(); //输出张三变身。
    stu.mSex = "女";
    qDebug() << stu.property("mSex").toString(); //输出男,之前通过反射添加的字段,并写了男。这里获取的是反射添加的字段的值。

请注意:

Q_PROPERTY(QString mName MEMBER mName)

中,第一个mName是元对象系统中的名字, 第二个才是字段。这个语句把元对象系统中的mName和成员变量进行了关联。 他们名字可以不一样。如果不一样,比如我一开始不小心把第一个mName,打成了nName。结果

cpp 复制代码
stu.setProperty("mName","张三");
 qDebug() << stu.mName; //输出空。

stu.setProperty("nName","张三");  //注意这里是nName。
 qDebug() << stu.mName; //输出张三。  //这里是mName。

反射自定义类型字段

QT的大部分类型都能反射。如果要反射自定义类型,这个自定义类型必须满足如下条件:

  1. 有拷贝构造函数
  2. 有赋值号运算符重载
  3. 有!=(本类型)运算符重载
  4. Q_DECLARE_METATYPE() 申明
    第一个和第二个可以是默认的。但有时编译器不一定能生成拷贝构造函数或者赋值号构造函数。典型的如继承了QObject的类。因为QObject delete了拷贝构造函数。所以编译器不会为它的派生类生成默认的拷贝构造函数。
    举例如下:
    有一个自定义类
cpp 复制代码
class Teacher
{
public:
    explicit Teacher(QObject *parent = nullptr);
    QString mName = "王老师";
    bool operator!=(const Teacher& other);


signals:
};
Q_DECLARE_METATYPE(Teacher)

学生中多了一个字段mTeacher。这个字段的类型是Teacher

cpp 复制代码
class Student : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString mName MEMBER mName)
    Q_PROPERTY(Teacher mTeacher MEMBER mTeacher)
public:
    explicit Student(QObject *parent = nullptr);
    QString mName;

    QString mSex;
    Teacher mTeacher;
};

因为Teacher满足了4个条件。(默认的拷贝构造函数,默认的=重载,以及自己写的!=重载 ,Q_DECLARE_METATYPE(Teacher) 申明 )。

所以mTeacher字段可以反射(注意还要有Q_PROPERTY(Teacher mTeacher MEMBER mTeacher))。

其使用起来如下

cpp 复制代码
	Student stu;

    qDebug() << stu.property("mTeacher").value<Teacher>().mName; //输出王老师  //通过反射得到的字段类型是QVariant,可以通过value模板函数把这个QVariant转化为真实的类型。
    Teacher otherTeacher;
    otherTeacher.mName = "李老师";
    stu.setProperty("mTeacher",QVariant::fromValue<Teacher>(otherTeacher));
    qDebug() << stu.property("mTeacher").value<Teacher>().mName; //输出李老师 //通过反射得到的字段类型是QVariant,可以通过value模板函数把这个QVariant转化为真实的类型。

还有一点需要提的是,反射字段(stu.property(mName))得到的字段对象是复制来的,也就是说修改反射得来的字段并不会影响原字段。要修改原字段,必须使用setproperty。这将导致深复制。这一点挺糟糕的。

拓展:qRegisterMetaType<Teacher>("Teacher");

谈到了Q_DECLARE_METATYPE ,顺便了聊聊qRegisterMetaType 吧。

前者是编译器发挥作用的,后者是运行时发挥作用的。

前者主要解决QVariant 不认识自定义类的问题。有了这个宏, moc就能在QVariant的模板函数中加入这个自定义类的特化。使得编译能过。

后者主要是在运行时把一个键值对加入到一个映射表中。键值对的键是字符串"Teacher"。值是类型Teacher的元对象。这样,qt才能通过字符串创建对象。也就是反射类。后面会说。

反射方法

反射方法就是通过字符串调用一个对象的方法,可以是public,private或者protect。被反射的方法需要具备如下条件。

  1. 类直接或间接继承自QObject
  2. 类有O_OBJECT宏定义
  3. 方法有Q_INVOKABLE 修饰,或者是slots,signals。

举例:

添加了一个showName方法。这是一个无参无返回值的方法。

添加了一个setName方法,这是一个有参数有返回值的方法。

cpp 复制代码
class Student : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString mName MEMBER mName)
    Q_PROPERTY(Teacher mTeacher MEMBER mTeacher)
public:
    Q_INVOKABLE explicit Student(QObject *parent = nullptr);
    QString mName;
    Q_INVOKABLE bool setName(QString name);

    Q_INVOKABLE void showName();

    QString mSex;
    Teacher mTeacher;
};

void Student::showName()
{
    qDebug()<< "my name is "<<mName;
}

bool  Student::setName(QString name)
{
    qDebug()<< "set name " << name;
    mName = name;
    return true;
}

调用

cpp 复制代码
Student stu;
bool result = false;
stu.metaObject()->invokeMethod(&stu,"setName",Qt::DirectConnection,Q_RETURN_ARG(bool,result),Q_ARG(QString,"张三"));
qDebug() << result; //输出true。
stu.metaObject()->invokeMethod(&stu,"showName");

反射类

反射类是通过字符串构建某个类的对象。一个类可以被反射,它必须满足以下条件。

  1. 类直接或间接继承自QObject
  2. 类有O_OBJECT宏定义
  3. 构造函数可以被反射,且是public。

举例:

cpp 复制代码
class Student : public QObject
{
    Q_OBJECT
    Student(const Student& other) = delete;
    Q_PROPERTY(QString mName MEMBER mName)
    //Q_PROPERTY(Teacher mTeacher MEMBER mTeacher)
public:
    Q_INVOKABLE explicit Student(QObject *parent = nullptr);
    QString mName;
    Q_INVOKABLE bool setName(QString name);

    Q_INVOKABLE void showName();

    QString mSex;
    //Teacher mTeacher;
};

使用:

cpp 复制代码
auto metaObj = &Student::staticMetaObject;
QObject* stu =metaObj->newInstance();

bool result = false;
QMetaObject::invokeMethod(stu,"setName",Qt::DirectConnection,Q_RETURN_ARG(bool,result),Q_ARG(QString,"张三"));
qDebug() << result; //输出true。
QMetaObject::invokeMethod(stu,"showName");

上面还不算是真正的反射,应为我们构建对象不是通过字符串的。实际上我们我们可以建立一个全局Map,键是字符串。值是这个字符串代表的类的元对象。

在程序一开始的时候填充这个全局Map。

refect.h

cpp 复制代码
#ifndef REFLECT_H
#define REFLECT_H
#include<QMetaObject>
#include<QMap>
#include<QString>
class Reflect
{
public:
    Reflect();
    template<class T>
    static void registerMetaObject(QString className){
        metaObjects.insert(className,&T::staticMetaObject);
    }
    static const QMetaObject* fromName(QString className){
        if(metaObjects.count(className)==0) return nullptr;
        else return metaObjects[className];
    }
    static void init();
private:
    static QMap<QString,const QMetaObject*> metaObjects;
};

#endif // REFLECT_H

refect.cpp

cpp 复制代码
#include "reflect.h"
#include <student.h>
QMap<QString,const QMetaObject*> Reflect::metaObjects;

#define reg(className) registerMetaObject<className>(#className);
Reflect::Reflect() {}

void Reflect::init()
{
    reg(Student);
}

使用

cpp 复制代码
Reflect::init();
auto metaObj = Reflect::fromName("Student");
QObject* stu =metaObj->newInstance();

// Student stu;
bool result = false;
QMetaObject::invokeMethod(stu,"setName",Qt::DirectConnection,Q_RETURN_ARG(bool,result),Q_ARG(QString,"张三"));
qDebug() << result; //输出true。
QMetaObject::invokeMethod(stu,"showName");

qRegisterMetaType

它的用法是这样的:

cpp 复制代码
qRegisterMetaType<Student>("Student");

作用类似于自己写的

Reflect::registerMetaObject(QString className);

为什么要自己写呢?原因是这个函数它要求Student有拷贝构造函数。但是Student默认是拷贝构造函数的,因为它继承自QObject。而QObject默认没有拷贝构造函数。在有些时候一些类我们不希望提供拷贝构造函数。

我的QT 版本是5.14。更高的版本的qRegisterMetaType可能没有这个问题了。qRegisterMetaType的实现中发生了对类的拷贝,所以要求有拷贝构造函数,这个违反直觉,谁能想到为什么qRegisterMetaType要拷贝对象?

反射的坏处

不会进行编译时检查。反射通过字符串创建一个类,如果字符串拼错了。编译是没有任何报错的。

运行时可能导致程序崩溃,因为不存在这么一个类。返回了一个空指针。后面也没有判空处理。运行后就只有一个崩溃。往往需要很长时间才 能排查到拼错了一个单词。这种在反射字段时更加隐蔽,因为反射字段时拼错一个单词不会造成崩溃,而且程序上没有错误。只是莫名奇妙的有个值没有赋上。

相关推荐
Lenyiin2 分钟前
第146场双周赛:统计符合条件长度为3的子数组数目、统计异或值为给定值的路径数目、判断网格图能否被切割成块、唯一中间众数子序列 Ⅰ
c++·算法·leetcode·周赛·lenyiin
yuanbenshidiaos2 小时前
c++---------数据类型
java·jvm·c++
十年一梦实验室2 小时前
【C++】sophus : sim_details.hpp 实现了矩阵函数 W、其导数,以及其逆 (十七)
开发语言·c++·线性代数·矩阵
taoyong0012 小时前
代码随想录算法训练营第十一天-239.滑动窗口最大值
c++·算法
这是我582 小时前
C++打小怪游戏
c++·其他·游戏·visual studio·小怪·大型·怪物
fpcc2 小时前
跟我学c++中级篇——C++中的缓存利用
c++·缓存
呆萌很3 小时前
C++ 集合 list 使用
c++
诚丞成4 小时前
计算世界之安生:C++继承的文水和智慧(上)
开发语言·c++
东风吹柳4 小时前
观察者模式(sigslot in C++)
c++·观察者模式·信号槽·sigslot
A懿轩A5 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列