[Development] Views in APIs (was: Re: QString and related changes for Qt 6)
Marc Mutz
marc.mutz at kdab.com
Tue May 12 18:59:35 CEST 2020
Hi Lars,
Let me pick important points one per email here:
On 2020-05-12 09:49, Lars Knoll wrote:
[...]
> For String related classes:
>
> * All methods not taking ownership of the passed arguments take a
> QStringView
Almost (see below).
> * If the method stores a pointer to the passed data it should take a
> QString to not surprise users.
... in addition ... (though see below).
> Exceptions can be done where it makes
> sense, but then the method naming has to give clear indications that
> this happens (like e.g. fromRawData())
> * Return a QString in QString itself and when doing conversions,
> return QStringView from QStringView
> * No QStringRef!
> * QLatin1String for backwards compatibility, can be disabled with a
> macro (similar to QT_NO_CAST_FROM_ASCII)
> * Remove or deprecate overloads taking a (const Char *, length) pair.
> Replace them with QStringView
+1 to the above
> Most other classes:
>
> * Only take and return QString
This is wrong. By taking a QString in the API of non-string-related
APIs, you expose QString as an implementation detail of the class. Think
QRegion, which, before I added begin()/end(), had a SSO-like internal
container (1 QRect or QVector<QRect>) which forced most users to code
around the owning-container API like this:
if (r.rectCount() == 1) {
use(r.boundingRect());
} else {
const auto rects = r.rects();
for (const QRect &rect : rects)
use(rect);
}
If rects() had been a view (today, we'd use gsl/std::span<const QRect>),
all users could just do
for (const QRect &rect : r.rects())
use(rect);
This is objectively a better API, for two reasons: a) the user doesn't
need to care about some weird idiosyncrasies of the class to avoid
performance penalties and b) the class author is now free to extend the
SSO buffer from one to, say, four, without changing the API, not even
those affected by Hyrum.
It _seems_ your solution is to fold views into owning containers, and
while that may seem to work, it's dangerous:
Assume QRegion::rects() returned a QVector-acting-as-view. Then this
would silently fail:
QRegion region();
for (const QRect &rect : region().rects())
use(rect);
because, clearly, QVector is an owning container, so we don't care that
the QRegion went out of scope. Whereas with a view, it will be
immediately obvious (to a tool like Clazy, at least) that this can't
work.
So, I'd argue for the complete opposite here: we would increase
encapsulation of our APIs if they stopped trafficking in owning
container types. I call this the NOI pattern (Non-Owning Interface). By
not having to serve QString everywhere, we'll be much more free to use
alternative storage types in the implementation (e.g.
QVarLengthArray<char16_t>, or - the horror! - std::pmr::u16string).
Handling-wise, QStringView makes all these choices equal, so the
implementation of a class can use whatever is objectively optimal
instead of being bound to QString.
AsidE: If you think that CoW is still a thing today: no. SSO is a thing
these days, and it seems that QString will not have it in Qt 6, either.
NOI favours SSO, QString-everywhere cements the naïve CoW world of the
1990s for yet another decade.
Learn from QRegion!
I have spoken on many conferences (at least QtWS, Meeting C++, emBO++)
on this, if anyone wants to learn more.
Thanks,
Marc
More information about the Development
mailing list