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 an empty null QVariant
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.
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)
}
}
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With