0

I have some code that I am using to convert arbitrary QObject subclasses to JSON. I able to convert them if they are pointers to a subclass, but am curious whether it is possible to convert instances (provided the subclass implements a copy constructor). Is there some crazy way to use something like templates or the type information provided by QMetaType to copy an instance of a QObject subclass without knowing what it is? The ToJson code is in a class that has no knowledge of the subclass.

I think it might be possible with QMetaType::create or something similar but I haven't been able to figure out how to actually copy the properties of the subclass instance.

Here's my code for converting:

QJsonValue ToJson(QVariant value){
    switch(value.type()){
    case QVariant::Int:
    case QVariant::Double:
        return value.toDouble();

    ////Other cases, etc...
    case QVariant::UserType:
        QObject* obj_ptr = qvariant_cast<QObject*>(value);
        if(obj_ptr) // value was originally a pointer to a QObject, works correctly
            return ToJson(obj_ptr);
        else { // value was orginally an instance of a QObject subclass
            std::string t = value.typeName(); //returns "MyQObject"
            int id = QMetaType::type(t.c_str()); //returns the id of the derived class
            void* v = QMetaType::create(id, &value); //passing &value does nothing
            obj_ptr = static_cast<QObject*>(v);
            return ToJson(obj_ptr); //works, but resulting fields are all default 
        }
    }
}

QJsonObject ToJson(QObject* o){
    QJsonObject obj;

    auto mo = o->metaObject();
    for (int i = mo->propertyOffset(); i < mo->propertyCount(); ++i){
        QVariant value = o->property(mo->property(i).name());
        obj[mo->property(i).name()] = ToJson(value);
    }
    return obj;
}

Sample code use case:

qRegisterMetaType<MyQObject>();
MyQObject obj;
obj.db = 11.1;

QVariant test1 = QVariant::fromValue(obj);
QVariant test2 = QVariant::fromValue(&obj);
QJsonValue v1 = ToJson(test1);  // default constructed values
QJsonValue v2 = ToJson(test2);  // db = 11.1

Sample QObject subclass:

class MyQObject : public QObject {
    Q_OBJECT
    Q_PROPERTY(double DB MEMBER db)
    Q_PROPERTY(int I MEMBER i)
    public:
        MyQObject();
        MyQObject(const MyQObject& other) : QObject() { 
            i = other.i;
            db = other.db;
        }
        int i = 50;
        double db = 1.5;
};
Q_DECLARE_METATYPE(MyQObject)

Is there any way to handle the case illustrated by test1 above?

Cobalt
  • 938
  • 9
  • 21

2 Answers2

0

Long-story-short: nope. There is no way to store QObjects by value in containers or QVariant.

Qt forbids the copy of QObjects and all inheriting classes. The mandatory the Q_OBJECT macro will disable any copy constructor also in newly defined classes.

The copy constructor that you are defining in the MyObject class is missing the base class constructor call. If QObject had a copy constructor it would be something like this:

MyQObject(const MyQObject& other) :
    QObject(other) // this will NEVER compile
{ 
   i = other.i;
   db = other.db;
}

Probably, the compiler is giving you a warning, but allows you to have such a constructor, even if it will result in undefined behavior or slicing an instance of MyObject every time it is passed by value.

Furthermore, the Qt docs states the following:

The values stored in the various containers can be of any assignable data type. To qualify, a type must provide a default constructor, a copy constructor, and an assignment operator. This covers most data types you are likely to want to store in a container, including basic types such as int and double, pointer types, and Qt data types such as QString, QDate, and QTime, but it doesn't cover QObject or any QObject subclass (QWidget, QDialog, QTimer, etc.).

So you can't store QObject and derived classes inside a Qt container unless you store them as pointers, as copy of QObjects is disabled by design.

Furthermore, if you want to exploit polymorphic behavior you must use pointers, even if there is no explicit need to cast to derived classes in your code, as far as I can see. If you really need to resort to casting in some place, you could consider making your ToJson a template function.

Gabriella Giordano
  • 1,188
  • 9
  • 10
  • Storing using `map.insert("myobj", QVariant::fromValue(&obj));` and retrieving with `QObject* obj_ptr = qvariant_cast(value);` works correctly. I don't want to cast it to my derived object because the converter doesn't know about the derived class. Also, my question is whether I can handle a map that contains QObject instances instead of pointers. – Cobalt Apr 17 '18 at 14:19
  • Since my derived object has a valid copy constructor it actually can be stored in a container as an instance, but my `ToJson` function can currently only handle it if it's a pointer. I'm going to mess around with a templated version and see what happens – Cobalt Apr 17 '18 at 19:37
  • I edited my question heavily to better explain my intent. Thanks for your input thus far – Cobalt Apr 17 '18 at 22:35
  • Thanks for your clarification. I edited my answer again, hoping it will help. – Gabriella Giordano Apr 18 '18 at 07:12
  • My copy constructor does call the default base class constructor ( `QObject()` ) but my sample code accidentally left that out. I'll add it for completeness – Cobalt Apr 18 '18 at 13:03
  • Also I believe the restrictions regarding the copying of `QObject` subclasses refers to qt classes deriving from `QObject`, not user defined subclasses. In order to use the `Q_DECLARE_METATYPE` macro you *have* to implement a copy constructor or it won't compile. – Cobalt Apr 18 '18 at 13:12
  • Please note that in your code you are invoking the default `QObject()` constructor, not the copy constructor that is private and not accessible. Furthermore, Q_DECLARE_METATYPE is intended to be used with **pointers** for classes inheriting from QObject, that is it should be `Q_DECLARE_METATYPE(MyQObject*)`. You can elide the `*` only if you can define a **public** default, **copy** and destructor. What I'm trying to tell you is that is your code is accidentally compiled, but it violates the framework design and therefore it is not working. Sad but true :( – Gabriella Giordano Apr 18 '18 at 13:47
  • I have to disagree that it is "accidental". The only reason for deriving from `QObject` at all is to make use of the property system. It's perfectly valid to call the default constructor for `QObject()` in the copy constructor considering the underlying `QObject` data is irrelevant – Cobalt Apr 18 '18 at 14:26
  • Anyway, this discussion is getting rather off topic imo. Regardless of whether or not I should, I know that I could...if there was a way to cast a `QVariant` based on its type name or type id. – Cobalt Apr 18 '18 at 15:13
  • Yep, and it all passes by the copy thingy that is not that irrelevant as you believe... :) I redirect you to the [qt documentation] (http://doc.qt.io/qt-5/qobject.html#no-copy-constructor-or-assignment-operator) about the copy constructor story. But when you get this trouble implementing a feature, remember what Yoda says: *If no mistake have you made, yet losing you are… a different game you should play.* :) Sorry I could not help – Gabriella Giordano Apr 18 '18 at 15:34
  • I agree that in general trying to copy a QObject is a bad idea...however if the derived class is never used as a QObject other than to get meta information, there is no harm in constructing a default QObject any time you want to copy the subclass because a default QObject is all that ever exists in the first place. At any rate, I did find a solution if you are interested. Thanks! – Cobalt Apr 18 '18 at 16:49
0

There is a solution, but use caution as it is only reasonable/applicable in the following scenario:

  • Classes in question are primarily data storage classes
  • The classes in question would be entirely copy-able if they didn't inherit from QObject
  • Most importantly, the ONLY reason you have the class inherit from QObject is so that it can have meta properties.

If your code uses the class as a QObject for any reason other than to get meta information, you are almost certainly using it incorrectly if you are trying to store it by value (as explained by G. Giordano in their answer).

Misuse considerations aside, in order to JSON-ify a QVariant that stores a QObject subclass by value, you can use the QMetaType::create method and pass it the user type id and yourQVariant.constData().

Example:

MyQObject obj;
obj.db = 11.1;
QVariant value = QVariant::fromValue(obj);

std::string t = value.typeName();
int id = QMetaType::type(t.c_str());
void* v = QMetaType::create(id, value.constData());
obj_ptr = static_cast<QObject*>(v);
QJsonValue json = ToJson(obj_ptr); //json contains db = 11.1
Cobalt
  • 938
  • 9
  • 21