11

Just came across a weird behavior of Qt framework while invoking overloaded C++ methods from within Qml and trying to understand the reason behind it. Let's say I have a QList<QVariant>-like class with the following methods:

...
Q_SLOT void append(const QVariant &item);
Q_SLOT void append(const QVariantList &items);
Q_SLOT void insert(int index, const QVariant &item);
Q_SLOT void insert(int index, const QVariantList &items);
...

Qml:

onclicked: {
  var itemCount = myListObject.size();
  myListObject.insert(itemCount, "Item " + (itemCount + 1));
}

Qt somehow decides to invoke the void insert(int index, const QVariantList &items) overload with items argument set to a null QVariant an empty QVariantList instead of the void insert(int index, const QVariant &item) overload with QString wrapped in QVariant.

Now if I change the order of declarations as follows, it works as expected:

Q_SLOT void insert(int index, const QVariantList &items);
Q_SLOT void insert(int index, const QVariant &item);

I could not find anything under Qt framework's documentation regarding the order of declarations and how it affects the way methods are resolved during invoke.

Can someone please explain? Thank you.

NG_
  • 6,895
  • 7
  • 45
  • 67
gplusplus
  • 336
  • 1
  • 5
  • 12

2 Answers2

4

This question is related to overloading in JavaScript. Once you get to known with it -- you understand reason of "weird behavior" of your code. Just take a look at Function overloading in Javascript - Best practices.

In few words -- I recommend you to do next: since you can operate QVariant variables on both sides (QML and Qt/C++) -- pass variant as parameter, and process it on Qt/C++ side as you wish.

You can use something like this:

Your C++ class created and passed to QML (e.g. as setContextProperty("TestObject", &tc)):

public slots:
    Q_SLOT void insert(const int index, const QVariant &something) {
        qDebug() << __FUNCTION__;
        if (something.canConvert<QString>()) {
            insertOne(index, something.toString());
        } else if (something.canConvert<QStringList>()) {
            insertMany(index, something.toStringList());
        }
    }

private slots:
    void insertOne(int index, const QString &item) {
        qDebug() << __FUNCTION__ << index << item;
    }

    void insertMany(int index, const QStringList &items) {
        qDebug() << __FUNCTION__ << index << items;
    }

Somewhere in your QML:

Button {
    anchors.centerIn: parent
    text: "click me"
    onClicked: {
        // Next line will call insertOne
        TestObject.insert(1, "Only one string")
        // Next line will call insertMany
        TestObject.insert(2, ["Lots", "of", "strings"])
        // Next line will call insertOne
        TestObject.insert(3, "Item " + (3 + 1))
        // Next line will call insertOne with empty string
        TestObject.insert(4, "")
        // Next line will will warn about error on QML side:
        // Error: Insufficient arguments
        TestObject.insert(5)
    }
}
Community
  • 1
  • 1
NG_
  • 6,895
  • 7
  • 45
  • 67
  • 4
    can you mention what specifically in the javascript overload system provoke this behavior? – UmNyobe Feb 15 '15 at 20:23
  • 1
    thanks for trying..I wasnt looking for a solution...solution is simple - just change the order of declarations...I was trying to understand (for future reference) as to how and why Qt decides to invoke the wrong overload and just completely ignores the data types. When the order is changed though, it doesnt have a problem resolving to the correct method signatures. On the side note, your solution limits my class to accept only certain data types. – gplusplus Feb 15 '15 at 20:52
  • Indeed, this does not answer the main question, i.e. *why* the behaviour occurs. Looking to this [old commit](https://qt.gitorious.org/qt/tmartsums-qt/commit/1d7b672fd46abab51a0124ad19aad18e5d14f1a8) there's no (apparent) reason for it not to work. Bug?? – BaCaRoZzo Feb 15 '15 at 21:11
  • @gplusplus **1.** As for me, I can not rely on "somewhere working, somewhere not"-solution. Changing of order of functions in header -- is not serious approach (just imagine, what you need to write in comments above that functions for further code maintainer), **2.** Can you write in details about limitation? – NG_ Feb 15 '15 at 21:49
  • _can not rely on "somewhere working, somewhere not"-solution_: agree with you on this 100%. _Can you write in details about limitation_: see my class is kind of almost the same as [ArrayDataModel](http://developer.blackberry.com/native/reference/cascades/bb__cascades__arraydatamodel.html) class from BB10 SDK, so I have to make sure it's as much generic as possible. I dont know beforehand what kind of data types users would be inserting. It just has to work with whatever is convertable from/to QVariant. – gplusplus Feb 15 '15 at 22:51
  • @gplusplus I looked at ArrayDataModel, and see that `Items can be inserted or appended by passing a single QVariant item or a list of QVariant items`. This cases are covered by my code. I can't understand what is problem here. Please, concretize your problem (or even open new question). – NG_ Feb 16 '15 at 11:42
2

This question is related to overloading in JavaScript. Once you get to known with it -- you understand reason of "weird behavior" of your code.

As far as I understand, the JS does not have overload support defined in the ECMA specification. What is suggested in various places are JS hacks to mock overload support.

The correct answer about behavior in Qt is as follows:

The order dependent behavior is documented only in the source code, as comment. See qv4qobjectwrapper.cpp in qtdeclarative module.

Resolve the overloaded method to call.  The algorithm works conceptually like this:
    1.  Resolve the set of overloads it is *possible* to call.
        Impossible overloads include those that have too many parameters or have parameters
        of unknown type.
    2.  Filter the set of overloads to only contain those with the closest number of
        parameters.
        For example, if we are called with 3 parameters and there are 2 overloads that
        take 2 parameters and one that takes 3, eliminate the 2 parameter overloads.
    3.  Find the best remaining overload based on its match score.
        If two or more overloads have the same match score, call the last one.  The match
        score is constructed by adding the matchScore() result for each of the parameters.

The implementation:

static QV4::ReturnedValue CallOverloaded(const QQmlObjectOrGadget &object, const QQmlPropertyData &data,
                                         QV4::ExecutionEngine *engine, QV4::CallData *callArgs, const QQmlPropertyCache *propertyCache,
                                         QMetaObject::Call callType = QMetaObject::InvokeMetaMethod)
{
    int argumentCount = callArgs->argc();

    QQmlPropertyData best;
    int bestParameterScore = INT_MAX;
    int bestMatchScore = INT_MAX;

    QQmlPropertyData dummy;
    const QQmlPropertyData *attempt = &data;

    QV4::Scope scope(engine);
    QV4::ScopedValue v(scope);

    do {
        QQmlMetaObject::ArgTypeStorage storage;
        int methodArgumentCount = 0;
        int *methodArgTypes = nullptr;
        if (attempt->hasArguments()) {
            int *args = object.methodParameterTypes(attempt->coreIndex(), &storage, nullptr);
            if (!args) // Must be an unknown argument
                continue;

            methodArgumentCount = args[0];
            methodArgTypes = args + 1;
        }

        if (methodArgumentCount > argumentCount)
            continue; // We don't have sufficient arguments to call this method

        int methodParameterScore = argumentCount - methodArgumentCount;
        if (methodParameterScore > bestParameterScore)
            continue; // We already have a better option

        int methodMatchScore = 0;
        for (int ii = 0; ii < methodArgumentCount; ++ii) {
            methodMatchScore += MatchScore((v = QV4::Value::fromStaticValue(callArgs->args[ii])),
                                           methodArgTypes[ii]);
        }

        if (bestParameterScore > methodParameterScore || bestMatchScore > methodMatchScore) {
            best = *attempt;
            bestParameterScore = methodParameterScore;
            bestMatchScore = methodMatchScore;
        }

        if (bestParameterScore == 0 && bestMatchScore == 0)
            break; // We can't get better than that

    } while ((attempt = RelatedMethod(object, attempt, dummy, propertyCache)) != nullptr);

    if (best.isValid()) {
        return CallPrecise(object, best, engine, callArgs, callType);
    } else {
        QString error = QLatin1String("Unable to determine callable overload.  Candidates are:");
        const QQmlPropertyData *candidate = &data;
        while (candidate) {
            error += QLatin1String("\n    ") +
                     QString::fromUtf8(object.metaObject()->method(candidate->coreIndex())
                                       .methodSignature());
            candidate = RelatedMethod(object, candidate, dummy, propertyCache);
        }

        return engine->throwError(error);
    }
}

I found overload support being mentioned in Qt 4.8 documentation (https://doc.qt.io/archives/qt-4.8/qtbinding.html). It does not go into any details:

QML supports the calling of overloaded C++ functions. If there are multiple C++ functions with the same name but different arguments, the correct function will be called according to the number and the types of arguments that are provided.

Note that Qt 4.x series are really old and archived. I see that the same comment exists in Qt 5.x source code as well:

src/qml/doc/src/cppintegration/exposecppattributes.qdoc:QML supports the calling of overloaded C++ functions. If there are multiple C++

I agree that it is really strange to have this order dependent logic "by design". Why would that be desired behavior? If you read the source code of that function very carefully, there are more surprises in there (at least initially). When you provide too little arguments at the call site, you get an error message listing available overloads, when you provide too many arguments, the extra arguments are silently ignored. Example:

Q_INVOKABLE void fooBar(int) {
    qDebug() << "fooBar(int)";
};
Q_INVOKABLE void fooBar(int, int) {
    qDebug() << "fooBar(int, int)";
};

Qml call site.

aPieChart.fooBar();

The error message.

./chapter2-methods 
qrc:/app.qml:72: Error: Unable to determine callable overload.  Candidates are:
    fooBar(int,int)
    fooBar(int)

Qml call site.

aPieChart.fooBar(1,1,1);

The run-time output (the last arg is ignored as the two arg overload is selected) :

fooBar(int, int)

According to What happens if I call a JS method with more parameters than it is defined to accept? passing too many args is a valid syntax.

It is also worth noting that default parameter values are supported by the overloading mechanism. Details are as follows:

Implementation in Qt Quick relies on Qt Meta Object system to enumerate the function overloads. Meta object compiler creates one overload for each argument with default values. For example:

void doSomething(int a = 1, int b = 2);
             

becomes (CallOverloaded() will consider all three of these) :

void doSomething();
void doSomething(a);
void doSomething(a, b);
             
gatis paeglis
  • 541
  • 5
  • 7