The last blog post in this series described how to use string-views. This post is about how to design one. In particular, it's about QStringView
's constructors. They evolved through a rapid succession of changes. These changes either fixed ambiguities between QString
and QStringView
overloads, or improved performance. And they all have the same solution: std::enable_if
, the Swiss Army Knife for overload control.
This post will take you from where we naïvely started to where we made the impossible possible: overloading a function for arrays and pointers.
The Naïve Beginning
How do you design a string-view class? We, at least, started by making a list of types that should be implicitly convertible to the string-view:
QString
, of course
QStringRef
std::u16string
char16_t
literals: u"Hello"
const QChar*
and const char16_t*
, with and without an explicit size argument
- Same for
ushort
, because Qt uses that in many low-level APIs
- On Windows:
std::wstring
, wchar_t
literals (L"Hello"
), const wchar_t*
(since wchar_t
is 2 bytes on Windows, and because we need to still support MSVC 2013, which does not know about char16_t
)
- ...
Next, pour that into a list of non-explicit constructors:
Enable_If, The First: Implementation Convenience
If you're like me, you get bored with repeating the same constructors for char16_t
, wchar_t
, ushort
and QChar
, #ifdef
'ing them for compiler support and platforms. And you write a template instead. Which uses std::enable_if
, of course.
We first define some template aliases to make the use of enable_if
a bit more readable:
For all the gory details of QtPrivate::IsCompatibleChar
and QtPrivate::IsCompatibleStdBasicString
see here and here, respectively.
With this, we can implement:
The idea here is that enable_if
only defines the nested typedef type
if its template argument evaluates to true. If it evaluates to false, the request for ::type
will fail, but because of SFINAE, this will not be an error. The template is simply not considered (removed from the set of possible overloads).
It compiles! Ship it!.
Working Around MSVC 2013, The First
Unfortunately, MSVC 2013 does not like that particular use of enable_if
:
With the above error message, MSVC tells us that a static_assert
fails. This assertion is from the QStringView
unit test. It's checking that QStringView
can be constructed from a std::wstring
. And that fails. MSVC 2013 does not support std::u16string
, so it's basically all std::basic_string
constructors that do not work. MSVC 2017 works perfectly well.
Thankfully, The Qt Company recently hired Ville Voutilainen, the C++ Evolution Working Group Chair, so we can now pick his brain on such matters. He suggested to use a slightly different approach, which yours truly folded into the existing type alias as follows:
Do you spot the difference?
Instead of using if_compatible_char
as the default value of the second, unnamed template argument, we make it the second template argument itself. In the success case, if_compatible_char
resolves to bool
, which we then default to true. In the failure case, we hit SFINAE and the template is removed from the overload set, as before.
It compiles! Even on MSVC 2013! Ship it!
Working Around MSVC 2013, The Second
Next, we hit a bug where MSVC 2013 allows two user-defined conversions when matching a function argument to the function's parameter types. This is apparently well-known, but kept around for compatibility reasons, with a gradual removal path over the next few MSVC versions.
The bug manifested itself by QStringView
overloads accepting, say, L'x'
and ushort
s via—brace yourself—QString(QChar(int(L'x'))
and QString(QChar(ushortValue))
, respectively. Yours truly decided to tackle that by adding deleted QStringView
constructors for all the types QString
accepts, but we didn't want:
This way, an attempt to construct a QStringView
from, say, a QChar
, or anything convertible to QChar
, would hit one of the deleted constructors and be rejected by the compiler.
This eventually got merged, and appeared to work for a while.
Until we started to add QStringView
overloads to existing functions taking QString
.
Ambiguous Overloads
Consider a function taking QString
overloaded with a function taking QStringView
:
This situation will come up all the time in the transition period until Qt 6: We weed out QString
parameters for QStringView
ones, but can't remove the QString
overloads because of binary compatibility.
Now consider these perfectly fine existing calls:
As long as there was only the QString
overload, these worked fine. Add a QStringView
overload, and they all become ambiguous, because QStringView(QLatin1String)
is just as good a conversion as QString(QLatin1String)
. That the QStringView
constructor is deleted is only checked after overload resolution. But we never get there, because the overload is ambiguous.
In both cases, the triggering of the MSVC issue of allowing two user-defined conversions, as well as the ambiguous-overload problem, the QStringView(QString)
constructor is root cause. Because it is a normal function, it is susceptible to implicit conversions.
Our problems would be solved if QStringView(QString)
only accepted QString
s, as opposed to "everything convertible to QString
".
Rule of thumb: if you want to rule out implicit conversions, use a template function that takes its argument by reference.
Enable_If, The Second: Managing Conversions
So, let's kill two birds with one stone by making the QStringView(QString)
constructor a template:
where if_compatible_qstring_like
matches only QString
and QStringRef
.
It is worthwhile to pause here and take a look at these two "overloads":
Apart from the name of the template argument, the signatures are identical. The only reason why we can overload them at all is because their respective enable_if
conditions are never both true for the same argument types. Such is the power of enable_if
.
Let's take this idea to the extreme now:
Detecting String Literals
Consider this call:
We want to achieve that the string-view construction is done completely at compile-time. In particular, we want the length of the string to be calculated at compile- and not at runtime.
At our current point in QStringView
development in this article, the call resolves to the following constructor, which reads, with full C++11 decoration and including its helper function:
with Char
deduced as char16_t
.
So, with a C++14 compiler, lengthHelper()
is constexpr
and thus evaluated at compile-time, as we desired.
But in C++11 mode, lengthHelper()
is not constexpr
. Yes, the constructor is constexpr
. But it calls a function which is not constexpr
. How does that even compile?
Well, first of all, it's not a function, but a function template. To a first approximation, you can simply slap a constexpr
onto every function template. The compiler will silently drop it if for a given instantiation it would not be allowed.
Constexpr Magic
But we don't even need that rule. For every Char
this constructor is constexpr
, even in C++11.
The reason this is so (and thankfully even MSVC implements it that way) is a nice (if you're so inclined) special rule for constexpr
functions. If there is even one argument that you could potentially pass to the function to make the body a constant expression, the whole function can be marked as constexpr
.
And this is the case here. If data == nullptr
, we invoke QStringView(nullptr, 0)
, which is a generalised constant expression. This is the only reason why we put the nullptr
check into the constructor instead of lengthHelper()
.
Ok, so where are we?
We have understood why the constexpr
keyword on the QStringView(const Char*)
constructor is allowed (there are two reasons for this) and have seen that the expression QStringView(u"QString")
is constexpr
if and only if we compile in at least C++14 mode.
Towards Constexpr in C++11
But we really really want the expression to be constexpr
even in C++11.
Taking a cue from std::size()
for arrays, we could get the idea to add an array-overload like this:
(the reference is needed, see your favourite C++ templates text book for why; the -1
is to strip trailing NULs). This is indeed constexpr
even in C++11, and for all string literals.
But it doesn't overload with the QStringView(const Char *)
constructor. Compiler diagnostics range from misleading to confusing, see GCC, Clang, MSVC @ godbolt.
But by now, we know what to do when we want to overload, but can't, don't we?
You guessed it: enable_if
to the rescue.
Enable_If, The Third: Distinguish Between Arrays and Pointers
So we need to write enable_if
conditions that distinguish between arrays and pointers. To get that information into enable_if
in the first place, we again need to take a template argument by reference:
Once you have these, writing if_compatible_array
and if_compatible_pointer
and their helpers is rather straightforward (solution in the embedded links).
That's it for today. Thank you for reading this far.
Lessons Learned
- You can overload any two types if you make the function a template, take by reference, and add
enable_if
with disjoint conditions.
- Hiding
enable_if
behind a type alias of the form if_condition<Args...> = true
is by far the most readable way to constrain templates. It may be even nicer than Concepts Lite's verbose requires
clauses.
QString
has way too many implicit constructors.
You can follow QStringView development on this blog and on Gerrit.
Stay tuned!
5 Comments
2 - May - 2017
Michael Vlach
Well that was really fascinating read! Thank you.
3 - May - 2017
Pedantic?
Nice read! Small syntax errors? "...har::value>::type*;" s/*/>/
3 - May - 2017
Marc Mutz
Thanks. No, it's not an error. It's making a pointer out of it (historic reasons). The angle brackets are balanced.
6 - May - 2017
Johannes Schaub
Here's an alternative to avoid the appearance of the array as a pointer:
This would be attractive in code that has not yet gone the way to use enable_if. But since you already have, O might also have preferred your approach over the above. Just for the record.
Another thing that came to mind: You could overload the constructors of the QStringView with "const QChar&", and take the address of that reference and write it into the QStringView pointer. This could work, but unfortunately it would be ambiguous when we overload together with QString. A quick-fix would be to make this constructor overload "explicit QStringView(const QChar& c)". Then passing a QChar to a QString/QStringView overloaded function will pick the QString version.
6 - May - 2017
Johannes Schaub
As for the length determination of the array - I would not do that, because it has surprising behavior:
Every user would expect qv.size() to be much smaller than 254.