[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