Interim Reflections
I want to step back for a moment and look at the original aims of the projects, and what can be said at this point. Originally this was (and continues to be) about the application of the newer features of C++ (i..e. C++11 and forward) to some concrete programming problems. So where am I so far as regards general conclusions?
The below assumes (or would like to assume) familiarity with the Lakos et. al. Embracing Modern C++ Safely, which is an extensive treatment of the functionality and risks attendant on it. It has a different and more limited aim: looking at features in terms of how they have come up in the course of working through concrete projects.
This is somewhat constrained. Neither of the two projects had many use cases for elaborate template metaprogramming; neither has any use cases for parallel processing; only one involves threading (and that to support using the base library in a GUI context, and we haven't covered that yet). Both make use of containers in different ways - one biases towards arrays, one towards vectors.I sort the changes made to C++ after 03 into three piles: new domains, nice to have, and significant enough to be considered game changers.
The first category can be summed up easily, just because it is made up of obviously fundamental additions. The threading-related features - atomics, futures and promises, plus threading proper - are fundamental extensions (although you could do the same thing with pthreads, it took a lot more care and a lot more expertise). The metaprogramming extensions make things that used to be very intricate indeed relatively simple. Even the filesystem extensions are thoroughgoing (though you could get the equivalent in C++03 using boost, in the main).
"Nice to have" are minor syntax improvements. The ability to write
if (theSet.contains(foo))
rather than
if (theSet.count(foo) > 0)
is a minor improvement to clarity. The same is true of
if (std::any_of(vec.begin(), vec.end(), test))
rather than
if (std::find_if(vec.begin(), vec.end(), test) != vec.end())
You save a few keystrokes and the intention of the code is clearer. Likewise, auto, in many contexts, just saves typing long type names (there are some contexts where it's critical for enabling other features).
The ability to declare local variables in if and switch scopes doesn't even save keystrokes, but it improves clarity and locality.
Collectively, these make a significant improvement to maintainability of code, and are not to be sneezed at.
Gamechangers are a vaguer category.
The whole set of range and view functionality seems to me to fall into this category - not because
if (std::ranges::any_of(vec, test))
is shorter and simpler (though it is) but because the ability to compose views, with lazy evaluation, and then pass them around for later use (multiple times) as though they were first-class containers (along with string_view and span) really does make a lot of things different. (There are some restrictions, notably with the inability in some cases to use const views, but that is a qualification.)
Likewise, although in one sense lambdas are syntactic sugar over the ability to write functor classes, in another sense combining lambdas with auto variables and the std::function capabilities enables an entire style of functional programming, along with monadic functions for std::optional and std::expected.
Modules seem to me to have a significant potential to change ways of thinking about managing dependencies in C++, but full compiler support is slow in coming. Having namespaces orthogonal to modules allows conceptual and concrete groupings to operate at different levels of granularity.
Coroutines allow for some really significant ways of using parallelism in domains where that's helpful. A classic example would be processing all the elements in a vector in parallel. On their own, absent threads, they mainly enable different code structuring; combined with threads you can get some significant parallelism benefits, notably combining future/promise threading with coroutines. The domains I'm working in don't heavily lend themselves to this style.
If you group together POD extensions, tuples, RVO optimizations encouraging return by value, auto capture of tuple or tuple-like members, and initialization of simple structs and aggregates with initializer-lists in the absence of constructors you also get considerably enhanced facilities for dealing with straight data.
You could perfectly reasonably send a message consisting of a tuple containing an integral ID, a std::view having a reference to a "safe" (long lifetime, heap-allocated) collection underneath but also encapsulating chained views such as take and filter, and a lambda which would be executed using, in part, data in the receiving context. You can get the same effect with enough effort in C++03 but the effort is enough to prevent it from being idiomatic.
I'm not sure about std:expected. It allows for a cleaner style of error-handling when not using exceptions, especially when using monadic functions to avoid if/else tests, but its domain of application is that where input errors (usually some form of user input) are common enough not to be a good fit with exceptions.
std::variant (which was available with boost) is also somewhere in-between. There is a whole style of polymorphism which you can get by combining std::variant with functional programming which is useful for message-based execution models, or for some implementations of the visitor pattern. But in a lot of domains - especially those where you essentially own the domain and can ensure that interfaces do what you want them to - it's unnecessary, though it can be rather more efficient. (The same generally applies to std::any's thoroughgoing type erasure. std::any is the class to reach for when you want type erasure you would otherwise use void* for: messages with arbitrarily extensible types, for example.)
Once you get down to very fine-grained handling of detailed cases, as has been the case in the last few posts, the version of the language doesn't make much difference at all, aside from being able to use override instead of virtual on derived functions and other such minor syntax improvements. It's once you're able to deal with notional or concrete sets of data or behaviours that the changes really start to make a difference.
Comments
Post a Comment