Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Qt/Qml and method overloads

Tags:

c++

qt

qml

moc

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.

like image 843
gplusplus Avatar asked Feb 15 '15 18:02

gplusplus


2 Answers

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)
    }
}
like image 179
tro Avatar answered Sep 20 '22 02:09

tro


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);
             
like image 21
gatis paeglis Avatar answered Sep 19 '22 02:09

gatis paeglis