Last Update:
C++20 Oxymoron: constexpr virtual
Table of Contents
Two keywords, constexpr
and virtual
- can those two work together? Virtual implies runtime polymorphism, while constexpr
suggests constant expression evaluation. It looks like we have a contradiction, does it?
Read on and see why those conflicting terms might help us get simpler code.
A basic example
Imagine that you work with some product list, and you want to check if a product fits in a given box size:
#include <cassert>
struct Box {
double width{0.0};
double height{0.0};
double length{0.0};
};
struct Product {
virtual ~Product() = default;
virtual Box getBox() const noexcept = 0;
};
struct Notebook : public Product {
Box getBox() const noexcept override {
return {.width = 30.0, .height = 2.0, .length = 30.0};
}
};
struct Flower : public Product {
Box getBox() const noexcept override {
return {.width = 10.0, .height = 20.0, .length = 10.0};
}
};
bool canFit(const Product &prod, const Box &minBox) {
const auto box = prod.getBox();
return box.width < minBox.width && box.height < minBox.height &&
box.length < minBox.length;
}
int main() {
Notebook nb;
Box minBox{100.0, 100.0, 100.0};
assert(canFit(nb, minBox));
}
Play @Compiler Explorer
The code above works at runtime and checks if a given product can fit into minBox
.
If you wanted similar code to be executed at compile-time in C++17, it wouldn’t be straightforward. The main issue is with the virtual keyword and runtime polymorphism. In C++17, you’d have to replace this with some static polymorphism.
But… in C++20 we can just throw constexpr
and everything will work:
struct Box {
double width{0.0};
double height{0.0};
double length{0.0};
};
struct Product {
constexpr virtual ~Product() = default;
constexpr virtual Box getBox() const noexcept = 0;
};
struct Notebook : public Product {
constexpr ~Notebook() noexcept {};
constexpr Box getBox() const noexcept override {
return {.width = 30.0, .height = 2.0, .length = 30.0};
}
};
struct Flower : public Product {
constexpr Box getBox() const noexcept override {
return {.width = 10.0, .height = 20.0, .length = 10.0};
}
};
constexpr bool canFit(const Product &prod, const Box &minBox) {
const auto box = prod.getBox();
return box.width < minBox.width && box.height < minBox.height &&
box.length < minBox.length;
}
int main() {
constexpr Notebook nb;
constexpr Box minBox{100.0, 100.0, 100.0};
static_assert(canFit(nb, minBox));
}
Play @Compiler Explorer
As you can see, it’s almost a “natural” runtime code but executed at compile time! (checked with static_assert
).
The main advantage of the new feature is that you can easily convert your existing code into a compile-time version!
We’re still at the compile-time level, so all types must be known up-front. A similar thing can happen when the compiler performs de-virtualization. But now, the code is explicit and can generate almost no code and work in constant expressions.
More examples and details The Performance Benefits of Final Classes | C++ Team Blog - devirtualization.
Some details
The proposal P1064 added into C++20 simply removes the requirement on constexpr
functions:
What’s more, a constexpr
function may override a non-constexpr
function and vice versa. Depending on the best viable function selection, the compiler can emit an error if the selected function cannot be run at compile-time.
Additionally, there’s a change to the way default destructor is generated:
If this satisfies the requirements of a constexpr destructor, the generated destructor is
constexpr
An example
Here’s another example where the new functionality enables us to write simpler code.
There’s a bunch of classes that derive from SectionHandler
- each handler works on a different group of tags (for example, tags in some file format). We’d like to see if the tags are not conflicting and unique as a quick compile-time check.
struct SectionHandler {
virtual ~SectionHandler() = default;
constexpr virtual std::vector<int> getSupportedTags() const = 0;
};
struct GeneralHandler : public SectionHandler {
constexpr virtual std::vector<int> getSupportedTags() const override {
return { 1, 2, 3, 4, 5, 6 };
}
};
constexpr std::vector<SectionHandler*> PrepareHandlers() {
return {
new GeneralHandler(),
new ShapesHandler()
};
}
constexpr size_t checkUniqueTags() {
auto allHandlers = PrepareHandlers();
size_t maxTag = 0;
for (const auto& handler : allHandlers) {
for (const auto& tag : handler->getSupportedTags())
if (tag > maxTag)
maxTag = tag;
}
std::vector<int> uniqueTags(maxTag + 1);
for (const auto& handler : allHandlers) {
for (const auto& tag : handler->getSupportedTags())
uniqueTags[tag]++;
}
for (auto& handler : allHandlers)
delete handler;
auto ret = std::ranges::find_if(uniqueTags, [](int i) { return i >= 2;});
return ret == uniqueTags.end();
}
int main() {
static_assert(checkUniqueTags());
}
Play @Compiler Explorer
And here’s another version with two techniques (sorting + std::unique
): @Compiler Explorer
Would you like to see more?
I wrote a constexpr
string parser and it's available for C++ Stories Premium/Patreon members.
See all Premium benefits here.
Even better - parsing expressions
For the purpose of this article I even contacted with the authors of the propsal. And I got a very interesting example:
constexpr char const * expr = "(11+22)*(33+44)";
static_assert( evaluate( expr ) == 2541 );
The code is a basic expression parser that works on compile-time in C++20.
What’s best is that it was converted from a runtime version by just “adding” constexpr
here and there :)
Here’s the code funtion, runtime:
int evaluate( std::string_view expr ) {
char const * first = expr.data();
char const * last = expr.data() + expr.size();
Node* n = parse_expression( first, last );
int r = n->evaluate();
delete n;
return r;
}
And compare it with the constexpr
version:
constexpr int evaluate( std::string_view expr ) {
char const * first = expr.data();
char const * last = expr.data() + expr.size();
Node* n = parse_expression( first, last );
int r = n->evaluate();
delete n;
return r;
}
See the runtime version @Compiler Explorer, and the constexpr approach @Compiler Explorer.
With the permission of Peter Dimov.
Potential optimization
This feature is very fresh, and the early implementations are interesting. For example, under MSVC, you have even experimental flags.
under /experimental:constevalVfuncVtable
and /experimental:constevalVfuncNoVtable
Once a decision is made on how to proceed, we’ll bring that capability under
/std:c++20
and/std:c++latest
.
See more in: MSVC C++20 and the /std:c++20 Switch | C++ Team Blog
Summary
While adding constexpr
to a virtual
function sounds scary at first sight, it looks like the new technique allows us to reuse code from the runtime version.
For now, I can imagine use cases where you can write some compile-time checks for your classes and class hierarchies. For example, with those file tag handling. The final production code is executed at runtime, but you might have some benefits of early “pre-flight” checks for development.
And another use case is for porting of existing algorithms from the runtime version to compile-time.
You can read more in the proposal P1064
Back to you
- Do you try to make your types and classes
constexpr
-ready? - Do you have any use cases where
constexpr
helped?
Let us know in the comments below the article.
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here: