C++23 is feature complete and on track to be released next year. While many people are complaining that it's, after all, a "minor" release (as the pandemic made the Committee work very difficult), C++23 still has a few very significant changes.
In this blog post, I want to talk about what I think is my favorite feature of C++23: thereally keyword. This is a brand new keyword that can be used in a number of different scenarios. Let's explore them together to understand the usefulness of it.
For Function Arguments
Consider a function like:
voidf(int x);
Unfortunately, we all know too well that C++ allows you to call it with non-integer arguments:
f(1234);// OK, intended: called with an intf(3.14);// Not really intended (called with double), still legal
If you're lucky, your compiler may give you a warning. Otherwise, the calls with non-int (double, long, ...) will be valid, and this may cause unexpected data losses and/or undefined behavior. Could it be possible to have a compile-time error instead?
While the C++20 version is a huge readability improvement over the SFINAE version, it's still a mouthful. Also, this makes the function a function template, so now it can't be out-of-line, it impacts our compilation times, etc.
In C++23 this will be possible by addingreally:
voidf(really int x);f(42);// OKf(3.14);// ERROR
Simple and effective! You can also apply it to ordinary variables or to return types:
really int i =getInt();// OKreally int j =getDouble();// ERROR
The compiler is doing something quite peculiar here: it's multiplying len by 42, calling a() with the result of the calculation, then it multiplies len again in order to call a() a second time. How come it's doing the same (relatively) expensive operation twice, rather than simply storing the result and using it for both calls? a() is a const member function, after all, so it can't change len -- or can it?
Well, the short answer is that the compiler is not allowed to do otherwise: the value of len could've been indeed changed by the call to a(). Maybe a() ends up calling something else, and that something else changes *this. Or maybe a() is just doing a const_cast<String *>(this) and then mutating the result. It doesn't change the result: the compiler is forced to reload the data members after a member function call as it can't assume that they haven't been changed. The fact that the target member function is const does not help the compiler in any way.
It may look longer, but we have a couple of extra mov in lieu of an imul. The multiplication result is stored and reused for the second call. And that's a huge win!
What really const does is makes the compiler consider a work on an object declared const. Such objects cannot be mutated, not even by stripping their constness away (that's undefined behavior). This causes better codegen; now the compiler knows that calling a cannot possibly mutate *this.
Closed Enumerations
Suppose you have an enumeration like this:
enumclassMyEnum{ E1, E2, E3
};
Now, suppose you have a function that takes a value of this enumeration. Correctly, you use a switch over the value, in order to handle all the possible cases:
Now, for some reason, your compiler will complain about calculate. Specifically written like this, you will get a warning regarding the possibility for control to reach the end of the function without hitting a return statement, and the function does not return void.
How in the world is that possible? There's clearly a switch in there that is covering all the possible enumerators!
Defeated, you'll change your function to something like this:
intcalculate(MyEnum e){switch(e){case MyEnum::E1:return42;case MyEnum::E2:return51;case MyEnum::E3:return123;// for the love of kittens, DO NOT add a default: !!!}assert(false);return-1;// impossible}
(Yes, do NOT add a default: to the switch -- a default label will prevent the compiler from warning you that the switch is no longer covering all the enumerators, should you decide some day to extend the enumeration.)
So how is "the impossible" actually possible? Is the compiler wrong?
No, the compiler is right. There is the possibility of passing a value which isn't handled by the switch; and that's because C++ allows casting integers to enumerations, as long as the integer "fits" in the enumerator's range. See the wording in [expr.static.cast]:
That means that this is 100% legal C++:
voidbad(){ MyEnum nonsense =static_cast<MyEnum>(-1);int result =calculate(nonsense);// whops!}
But just how often is this feature really useful? While, in general, allowing conversions from integer to enumerations makes sense, people are not really supposed to cast arbitrary integers into an enumeration's type.
Enterreally enum, or, of course, enum really as it needs to be spelled:
enumclassreally MyEnum { E1, E2, E3
};intcalculate(MyEnum e){switch(e){case MyEnum::E1:return42;case MyEnum::E2:return51;case MyEnum::E3:return123;}// no warning, all cases are handled}voidbad(){ MyEnum nonsense =static_cast<MyEnum>(-1);// UB, -1 is not an enumerator}
Basically, a really enum is just like an ordinary enum (or enum class, like in this case), except that converting integers to it requires the integer to match the value of one of the enumerators -- otherwise, undefined behavior. The UB is not really worse than what we had before (walking out of a function without returning) or crashing due to the failed assert. On top if that, thanks to things like ubsan, we can catch the problem (= the illegal conversion) at its source rather than later, when it causes havok.
For Deduced rvalue References
Let's face it: templates arehard. And deduction rules for function templates, combined with value categories, are even harder!
But let's start with a simple example. Any proficient C++ developer should know that it's possible to overload a function f for lvalues and rvalues:
Now suppose you want to make your f generic, so it can work on std::string but also on other string types. That, of course, means turning f into a function template.
Easy enough, right? Well, raise your hand if you ever fell into this trap:
template<typenameT>voidf(const T &obj);// 1) lvalue referencetemplate<typenameT>voidf(T &&obj);// 2) rvalue reference...?
The double ampersand still means rvalue reference, doesn't it? Turns out that, well, yes, but actually no. Since T is deduced, it doesn't; it now means forwarding (or universal) reference. This code:
std::string s ="hello";f(s);// call f on a lvalue
is now actually calling overload n. 2 (!!!), after deducing T = std::string &.
This is incredibly annoying and error-prone: the very same syntax, without deduced template arguments, works correctly. Reusing the same syntax with totally different meanings is a major annoying point (just think about the teachability of such a feature...).
In order to force the second overload only to take rvalue references, we have to again deploy SFINAE or similar techniques:
template<typenameT>voidf(const T &obj);// lvalue referencetemplate<typenameT> std::enable_if_t<!std::is_reference_v<T>>f(T &&obj);// T must not be a reference => T&& is a rvalue reference
I mean... really?! (pun intended) I bet that if we had a time machine, someone would surely propose the usage of different syntax (a triple ampersand?) to specify forwarding references and remove the entire problem.
Luckily for us, in C++23, we can do this:
template<typenameT>voidf(const T really &obj);// 1) lvalue referencetemplate<typenameT>voidf(T really &&obj);// 2) rvalue reference! not forwarding reference
This brings back the "traditional" rules without any of the verbose SFINAE syntax. Technically speaking, it's not needed on 1), but I like the symmetry.
Really Operator Auto
A problem that sometimes affects Qt users is the following:
QString getString();voidprint(QString);voidf(){auto result =QString("The result is: ")+getString();print(result);}
What's the type ofresult? You may think it's a QString -- after all, it's being obtained by concatenating (via operator+) two QString objects.
That's actually correct in some cases, but in (many) others, it's not accurate. Generally speaking, result is actually an object of type QStringBuilder.
What'sQStringBuilder, you ask? It's a private class in Qt that is employed to optimize string concatenations. A concatenation like this:
QString a, b, c, d;QString result = a + b + c + d;
may cause up to 3 memory allocations, if done naively: first, calculate a + b (allocate); then, append c to the result (allocate again); then, append d to the final result (allocate again).
QStringBuilder instead considers the entire "sequence" of operations. When QStringBuilder is in use, a + b does not return a QString directly. It returns a QStringBuilder that remembers a and b (It simply internally stores references to them). Note that no allocations (for the result) or copies are taking place yet.
Then, appending c to the intermediate QStringBuilder yields anotherQStringBuilder that now remembers c, too. Finally, d is appended, yielding one final QStringBuilder.
This final object is then converted to a QString. That's where the magic kicks in: QStringBuilder can now perform one memory allocation of the right size (by asking all the other strings about their sizes, as it's keeping references to all of them), copy their contents in order to perform the concatenation, and produce result.
So what's wrong in the first example? The answer is the usage of auto:
auto result =QString("The result is: ")+getString();// whops!
Here, a QStringBuilder is produced in order to concatenate the two strings. But it's never converted to a QString. Instead, result is an object of type QStringBuilder itself!
That is a problem because, as mentioned before, QStringBuilder merely references the strings that it is supposed to concatenate. These strings are temporaries that will get destroyed at the end of the statement. That means that we now have a QStringBuilder loaded with dangling references!
Again, with a time machine, the solution could be to allow like this:
classQStringBuilder{// invoked when assigning the result to `auto` variable...?operatorauto()const{returnQString(*this);}};
Although operator auto() exists, it's not what the comment above says. operator auto is nothing but an "ordinary" conversion operator, combined with auto deduction for the return type of a function. In other words, the above is just declaring an operator QString() const -- but we already have that one inside QStringBuilder!
Instead, in C++23 we can do this:
classQStringBuilder{// invoked when assigning the result to `auto` variable. really operatorauto()const{returnQString(*this);}};
Note that the position of really is important. Otherwise, it applies to the conversion target type, just like I've shown before:
classC{operator really int()const;};C c;int i = c;// OKdouble d = c;// ERROR
And that's it! I hope you liked this little presentation about the new keyword. I can't wait to start working on C++23 projects to try this one out (and also the other goodies, which you can check out on cppreference).
The KDAB Group is a globally recognized provider for software consulting, development and training, specializing in embedded devices and complex cross-platform desktop applications. In addition to being leading experts in Qt, C++ and 3D technologies for over two decades, KDAB provides deep expertise across the stack, including Linux, Rust and modern UI frameworks. With 100+ employees from 20 countries and offices in Sweden, Germany, USA, France and UK, we serve clients around the world.
12 Comments
1 - Apr - 2022
RJ
This is really great news! Really hope it really happens.
1 - Apr - 2022
Bart
Happy April's Fools.
1 - Apr - 2022
Soroush Rabiei
In the last committee meeting, I remember someone suggested renaming 'really' to 'indeed'. I hope this will be approved before next April.
1 - Apr - 2022
RJ
You're missing the "honestly" proposal. Unlike really, honestly would also allow conversions between types that sound different but are honestly the same, in behavior and layout.
For example, on a MS Windows machine, a long int is honestly an int.
This would also allow conversions from char8_t, which is honestly an unsigned char.
Both "really" and "honestly" could coexist. And honestly, really and honestly would be nice to have.
1 - Apr - 2022
Yacob Cohen-Arazi
I hate this date! ;)
2 - Apr - 2022
nyanpasu64
Basically, a really enum is just like an ordinary enum (or enum class, like in this case), except that converting integers to it requires the integer to match the value of one of the enumerators — otherwise, undefined behavior.
Well that's already the case since MyEnum doesn't have a fixed underlying type (: int), as ubsan impolitely pointed out to me one day when casting a bit flag to an enum.
2 - Apr - 2022
Giuseppe D'Angelo
Well that’s already the case since MyEnum doesn’t have a fixed underlying type (: int), as ubsan impolitely pointed out to me one day when casting a bit flag to an enum.
Each enumeration also has an underlying type.
The underlying type can be explicitly specified using an enum-base.
For a scoped enumeration type, the underlying type is int if it is not explicitly specified.
In both of these cases, the underlying type is said to be fixed.
9 - Apr - 2022
alagner
Didn't you mean std::enable_if_t<!std::is_lvalue_reference_v> instead of std::enable_if_t<!std::is_reference_v>?
10 - Apr - 2022
Giuseppe D'Angelo
Well spotted, and, pedantically, yes -- although usually the idea is to let the template type argument to be deduced (most things, like the std algorithms, don't want users to specify the template arguments).
8 - May - 2022
LorenDB
Now I want this to be a thing. A really keyword would be really (ha!) infinitely handy.
22 - Jun - 2022
Sebastian
Shouldn't syntax highlighting be improved so that 'really' is coloured as a keyword?
22 - Jun - 2022
Giuseppe D'Angelo
Absolutely. It already does, but it works only on April 1st, 2023...
Giuseppe D’Angelo
Senior Software Engineer
Senior Software Engineer at KDAB. Giuseppe is a long-time contributor to Qt, having used Qt and C++ since 2000, and is an Approver in the Qt Project. His contributions in Qt range from containers and regular expressions to GUI, Widgets, and OpenGL. A free software passionate and UNIX specialist, before joining KDAB, he organized conferences on opensource around Italy. He holds a BSc in Computer Science.
Our hands-on Modern C++ training courses are designed to quickly familiarize newcomers with the language. They also update professional C++ developers on the latest changes in the language and standard library introduced in recent C++ editions.
12 Comments
1 - Apr - 2022
RJ
This is really great news! Really hope it really happens.
1 - Apr - 2022
Bart
Happy April's Fools.
1 - Apr - 2022
Soroush Rabiei
In the last committee meeting, I remember someone suggested renaming 'really' to 'indeed'. I hope this will be approved before next April.
1 - Apr - 2022
RJ
You're missing the "honestly" proposal. Unlike really, honestly would also allow conversions between types that sound different but are honestly the same, in behavior and layout.
For example, on a MS Windows machine, a long int is honestly an int.
This would also allow conversions from char8_t, which is honestly an unsigned char.
Both "really" and "honestly" could coexist. And honestly, really and honestly would be nice to have.
1 - Apr - 2022
Yacob Cohen-Arazi
I hate this date! ;)
2 - Apr - 2022
nyanpasu64
Well that's already the case since MyEnum doesn't have a fixed underlying type (: int), as ubsan impolitely pointed out to me one day when casting a bit flag to an enum.
2 - Apr - 2022
Giuseppe D'Angelo
MyEnum does in fact have a fixed underlying type because it's a scoped enumeration. See https://eel.is/c++draft/dcl.enum#5:
9 - Apr - 2022
alagner
Didn't you mean
std::enable_if_t<!std::is_lvalue_reference_v>
instead ofstd::enable_if_t<!std::is_reference_v>
?10 - Apr - 2022
Giuseppe D'Angelo
Well spotted, and, pedantically, yes -- although usually the idea is to let the template type argument to be deduced (most things, like the std algorithms, don't want users to specify the template arguments).
8 - May - 2022
LorenDB
Now I want this to be a thing. A
really
keyword would be really (ha!) infinitely handy.22 - Jun - 2022
Sebastian
Shouldn't syntax highlighting be improved so that 'really' is coloured as a keyword?
22 - Jun - 2022
Giuseppe D'Angelo
Absolutely. It already does, but it works only on April 1st, 2023...