C++ 20: The Core Language
modernescpp.com
modernescpp.com
I've really enjoyed programming in c++17 for the last three years (I had the luxury of starting from an empty buffer) and find the language pretty expressive. If you are able to use it as a "new" language, ignoring its C roots and features that exist pretty much just for back compatibility, it's really quite expressive, powerful, and orthogonal.
I'm actually pretty glad the c++ committee has been willing to acknowledge and deprecate mistakes (e.g. auto_ptr), to squeeze out special cases (e.g. comma in subscripting) and to attempt to maintain generality. Conservatism on things like the graphics standard is reducing the chance of the auto_ptr or std::map mistakes.
Expressive and powerful are definitely true, but I'm not so sure about 'orthogonal'.
Most C++ features seem to depend heavily on each other, and library decisions can impose restrictions/boilerplate on application code in many areas.
Especially as the language has evolved, features like exceptions, destructors, copy and move constructors, and operator overloading have become more and more indispensable. Template functions are still required for any kind of variance (e.g. you still can't have a non-template function that works on a const std::shared_ptr<const *base_type> or any derived type).
None of these is unbearable, but I do believe that even the modern subset of C++ is still the least orthogonal mainstream language in use (and definitely the most complicated by far). To be fair, it's also likely the most expressive mainstream language too, and still the most performant.
I guess you meant, you can't have a function that accepts a shared_ptr to the base type without indirectly using template machinery?
Because you can most definitely have a function that does this that is not a function template.
#include <memory>
#include <iostream>
struct foo {
virtual ~foo() = default;
virtual void bark() const { std::cout << "fork\n"; }
};
struct bar : foo {
void bark() const override { std::cout << "bark\n"; }
};
void poke_so_it_barks(std::shared_ptr<foo const> const thing) {
thing->bark();
}
int main() {
poke_so_it_barks(std::make_shared<foo>());
poke_so_it_barks(std::make_shared<bar>());
}If I had stuck with std::vector my point would have been better. Even though theoretically a `const std::vector<derived* >` IS-A `const std::vector<base* >`, that is, a read-only view of a collection of derived elements behaves the same as a read-only view of a collection of base elements, you can't pass the first to a function expecting the second in C++, unless you make your own function a template.
But I don't see how this could be implemented without completely changing large parts of the language.
In the one case of covariance that actually exists, overriding `virtual base* factory()` with `derived* factory() override`, the overridden function knows that the function it overrides returns a `base* `, so it can just return a `base* `, too, and callers through the subclass pointer know that they can adjust the return value back to a `derived* `.
But even the closest cousin, argument contravariance (i.e. overriding `virtual foo(derived* )` with `foo(base* ) override`) doesn't seem feasible. Callers through the base pointer will pass a `derived* `, so the overridden function has to expect to be passed a `derived* `, and there is no adjustment that can be made to a `base* ` to turn it into a valid `derived* ` statically.
The vector example is even more impossible. Since the function expects all members of the `std::vector<base* > const` argument to be `base* `, a caller with a `std::vector<derived* >` would have to allocate a new vector and adjust each individual pointer, which would be a terrible hidden runtime cost. (If they had added a constructor to accept a range to complement the one that takes an iterator pair, that's exactly what would have happened, though.)
I don't think I understand your point. A function which expects a `base* ` can already be passed a `derived* ` without any problems. Doing the same for a collection of `base* ` to a collection of `derived* ` is not fundamentally different, but it does come with significant complications. For one, the language would have to be sure that the collection is read-only, since a function which wants to add something to the collection can't be called with a collection of a different type.
More significantly, the language itself has no idea that `std::vector<T>` is a collection of Ts, while `std::function<T>` is not a collection. In principle, it may be possible that it could look at all of the members and decide each template argument's variance. Even if that is theoretically possible (I'm not sure it is), it is certainly far too complicated to be a useful feature.
That leaves us with the option of some kind of annotation for variance in template parameters (or at use-time, as in Java).
Since C++ has multiple inheritance, derived* differs from base* not just by reinterpretation, the pointer value could be different.
A single pointer can trivially be adjusted, but a heap-allocated, potentially huge array of pointers would require an equally huge heap allocation to store the adjusted pointers.
More realistically, it could allow the programmer to express it explicitly, as in C# (C<in T, out V> is covariant in T and contravariant in V) or Java (C<? extends T,? super V> is covariant in the first parameter, contravariant in the second). This would not solve the problem that the co-/contra-variance of some types may depend on their const-ness, but it would allow at least a covariant, always read-only, std::vector_view<T> (similar to IEnumerable<in T> in C#, which can also be implemented by the invariant List<T>/std::vector<T>).
Sadly the C++ ABIs are standardized the way that C ABIs are (I'm OK with why but it's unfortunate in that it creates a barrier) so you have to have separate libraries compiled with g++ and clang++ if you use both on your platform (we use both because they catch different bugs, and for that matter exhibit different bugs). But it means you can't simply install, say, fmt in any system-wide directory like /usr/lib or /usr/local/lib
Just as an amusing side note: Common Lisp used to be criticized for the massive size of its libraries and later likewise C++. It was true they were quite large. Now both are criticized for their tiny libraries. Which by today's standards they are.
Yes, gcc and clang use the same platform API so if they use the same headers (libstc++ or libc++) then they will indeed use identical structure layout etc.
I meant a "gcc-native" toolchain (gcc + libstdc++) vs "llvm native" (clang++ + libc++) having different layout (and there is even some interoperability between them thanks to work by the llvm team). I realize my need to do this (to try to minimize opting bugs) is a special case, and probably unusual.
Actually I would bet that even among clang users, libstdc++ is used more commonly on GNU/Linux (IDK for sure, but it’s my hunch).
Part of "size of libraries" is "mental size of libraries".
And C++ and Lisp and have very large mental spaces for their main core libraries. A "String", for example, carries a huge amount of mental baggage in those languages. In most other languages, a string is extremely straightforward because it was designed into the language from the start.
It's essentially a std::vector<char> with a few random convenience features bolted on.
I guess some of the confusing points are: not unicode aware, string literals aren't std::strings by default, c_str returns a pointer to a buffer with length one greater than the string length, and the usual C++ quirks like why is there both data and c_str?
The usual C++ response: for backwards compatibility, because data was not required to null-terminate prior to C++11.
I think one of the classic advantages of C++ over C is that you have the option of std::string instead of char arrays.
I don't program in C++ so I don't really know but I do a lot of pure C and the strings truly are a mess (despite being VERY straightforward)
“Modern, dependency-heavy environments” are a symptom of the fact that certain ecosystems make it easy to get addicted to dependencies; I don’t think they’re a goal to strive towards.
And not only for the libraries:
Similarly, Cargo in Rust is an absolute dream to work with, you just run `cargo new <project>` and then `cargo build`. That's it. If you want to add a dependency you can do so from the public crates.io repository or a sub-path or a git repository.
No language should be without this core feature, so it'd be great to see C++ get it too.
I already have a dependency management tool for C++; it is called the Ubuntu package repositories. I don’t need another one baked into the language.
BTW modules won't address this. I'm not sure you did but some other down thread comments implied that some people thought it would.
There is also C. The tooling and the "package manager for C++" would be expected to seamlessly work for C and be expected to be accepted and used by the C community.
(personally I use CMake + OS package manager)
The cmake language might not be beautiful, but it does allow for simple sub-projects/library scripts once the main cmake script is set up properly.
This is currently really easy to do cleanly though CMake by using Hunter and toolchain files (don't use submodules or addexternalproject)
External tools like Conan are also unnecessary bc they introduce redundancy and extra maintenance
My advice about build tooling is to use buck (from Facebook) or bazel (from Google). If you have an ex-Googler nearby, use Bazel. If you have an ex-Facebooker nearby, use buck. Otherwise flip a coin.
Buck and Bazel were created for complex internal dependencies.
You'll have just as easy/hard of a time getting Boost installed with Bazel as you would for Make.
Or publishing an accessible library that depends on Boost.
> Or publishing an accessible library that depends on Boost.
This is patently false. Here is the bazel dependency for boost (put this in your workspace file):
git_repository(
name = "com_github_nelhage_rules_boost",
commit = "6d6fd834281cb8f8e758dd9ad76df86304bf1869",
shallow_since = "1543903644 -0800",
remote = "https://github.com/nelhage/rules_boost",
)
load("@com_github_nelhage_rules_boost//:boost/boost.bzl", "boost_deps")
boost_deps()
No other installation necessary. If someone has a C++ compiler and Bazel installed that will allow you to build against Boost deps (e.g. "@boost//:callable_traits"). No downloading boost by hand, no building boost by hand, no managing it's build, bazel does all of that.Bazel does have a dependency ecosystem, it's based off of hashed versions of publicly available git repos and http archives (and anything else you want really). Which means any code anywhere can be a dependency while still being reproducible. Additionally you can provide local build instructions for the external code, so you don't even need to rely on their build. The better way is to find someone who maintains a BUILD file for a repo (or mirror and maintain it yourself) but still.
To beetwenty's point, the C++ ecosystem in general lacks the the level of support for dependency ecosystems that you find in Maven, pip, gem, npm, etc.
Bazel external dependencies are in fact a real pain point. See the 3+ year old Bazel Recursive WORKSPACE proposal. [1]
I was at the first Bazel Conf in 2017, and the external dependencies meeting was quite the row.
[1] https://bazel.build/designs/2016/09/19/recursive-ws-parsing....
Also as demonstrated Boost is way easier to include in bazel than make which was the original issue under discussion.
I make a library that depends on boost, here is how you use it:
git_repository(
name="foo"
...
)
load("@foo//:foo/foo.bzl", "foo_deps")
foo_deps()
magicSo, bringing such multiple third party bandaid is currently not such a great idea. It would be a bit better if boost itself provided it.
How is this different than most other build systems? You always have to rely on others.
I also use boost when the compiler is behind (e.g. boost::variant until apple's compiler caught up).
I can't wait to have a boost-free tree. but I suspect it will be a while.
I am quoting the word "dimension", because applying a precisely defined mathematics concept to something messier (like programming) will require a certain amount of hand waving and squinting.
Perhaps it's easiest to give examples of what it's not. At first in C you could only assign scalars; to assign a struct or union you had to call memcpy() or something like it. Eventually you could assign aggregates, but you still can't assign arrays unless you hide them in structs. It took a while before you could pass or return an aggregate.
Exceptions of that sort are non-orthogonalities.
"In computer engineering, an orthogonal instruction set is an instruction set architecture where all instruction types can use all addressing modes. It is "orthogonal" in the sense that the instruction type and the addressing mode vary independently. An orthogonal instruction set does not impose a limitation that requires a certain instruction to use a specific register[1] so there is little overlapping of instruction functionality.[2]"
Its use here is more general, but I hope the specific example helps.
In describing features orthogonal is basically jargon for unrelated and adds no additional clarity imo.
I.e. you can understand one aspect of a programming language (memory allocation, generic programming, standard library, mutability, control flow, pattern matching, etc.) without understand every aspect.
As a counterexample, if a language has generics, but only generics for a standard array type, that lacks orthogonality.
1 + 2;
std::add(1, 2);
std::int(1).add(std::int(2));
That's is not orthogonal at all, since we have 3 different ways to do the same thing for no discernible reason.
An orthogonal interface is one where there are fewer reasonable ways to do something, and each function/operation has it's role clearly defined, it doesn't try to do things unrelated to its main role, and the interface doesn't mix different layers of abstraction (like intertwining OOP and arithmetic in my 3rd example, when there already are native arithmetic operations).
That example sticks with me.
- std::unique_ptr and std::shared_ptr (your code should not have any new/delete). It's easy to write code with clearly defined ownership semantics, and I've yet to create a memory leak.
- Proper memory model so that multithreading has defined semantics.
- Lots of syntactic sugar (auto, range-for, lambdas, structured bindings, if constexpr, etc.).
- More metaprogramming tools/fun.
C++11 alone is a very different world compared to what you are probably used to.
I cannot begin to describe how huge a thing this was for me. I came to C++-17 (just about) from C++-98 -- a language I hated so much, I vowed I would never, ever write anything in it on purpose.
It is a massively different beast today from what it was, but the biggest thing of all is this change in how you deal with memory. It was like going from a particularly annoying and verbose aberation of C to a particularly fast and expressive version of Python.
YMMV, of course. In fact, it may vary to the point of becoming absolutely unrecognizable if you're dealing with a massive legacy codebase. But I have to say, I've become a very big fan in the last few years.
More important in my opinion are changes to the language, like lambdas or auto.
I did the latter for a project that started around 2000 and never looked back.
Do you actually find it verbose to read or also verbose to type? I don't consider the latter an issue since 1. you normally spend much more time thinking than typing while programming, and 2. code is read much more often than it is written.
But coding on C++ has become quite fun. Generic lambdas deserve much of the credit.
* Hash maps in the standard library (every so often I have to go and look it up to remind myself that no, I’m not imagining things, C++03 really didn’t have hash maps in the standard library, as ludicrous as that sounds)
* Regexes
* Option and Variant types
* Move semantics
And my personal favorite...
* foo<bar<quux>> is no longer a syntax error
std::unique_ptr<foo> m_foo;
How do you pass that into a function that can receive a pointer to foo, but does not require it? The only way to do so is to declare the function to take a foo* and pass it m_foo.get()In summary, * will never be deprecated until C++ has support for something along the lines of Rust's borrowing and lifetimes.
Many people have pointed out that C++17 has std::optional. While that's true, I don't think it'll deprecate raw pointers. I'll try to explain why.
Here's an example you can paste, compile and run:
#include <iostream>
#include <memory>
struct foo
{
int thingamajig;
};
void borrow_optional_foo(foo * borrowed)
{
if (borrowed != nullptr)
{
borrowed->thingamajig = 42;
}
}
int main()
{
std::unique_ptr<foo> owned = std::make_unique<foo>();
owned->thingamajig = 17;
std::cout << owned->thingamajig << std::endl;
borrow_optional_foo(owned.get());
std::cout << owned->thingamajig << std::endl;
return 0;
}
If you run it, it should print: 17
42
What would it look like if we wanted to use std::optional and get the same behavior? #include <iostream>
#include <functional>
#include <memory>
#include <optional>
struct foo
{
int thingamajig;
};
void borrow_optional_foo(std::optional<std::reference_wrapper<foo>> borrowed)
{
if (borrowed)
{
borrowed->get().thingamajig = 42;
}
}
int main()
{
std::unique_ptr<foo> owned = std::make_unique<foo>();
owned->thingamajig = 17;
std::cout << owned->thingamajig << std::endl;
borrow_optional_foo(std::make_optional(std::ref(*(owned.get()))));
std::cout << owned->thingamajig << std::endl;
return 0;
}
As you can see, there's a tradeoff involved. On the one hand, you get crystal clear, descriptive type: std::optional<std::reference_wrapper<foo>> is clearly an optional reference to foo.On the other hand, using it is absolutely atrocious: you have to write borrowed->get().thingamajig as opposed to borrowed->thingamajig, and std::make_optional(std::ref(*(owned.get()))) as opposed to owned.get()
Does it work? Absolutely. Is it crystal clear? Indisputably so. Will it deprecate raw pointers? I really, really doubt it, but that's just my opinion.
https://herbsutter.com/2013/06/05/gotw-91-solution-smart-poi...
Same as you would have a function that can receive an `int` but does not require it. Or a receive a `std::vector` but does not require it.
I surmise that you mean "how do you pass any value (pointer or otherwise) into a function that can receive a value but does require it". And I surmise you have before used pointer indirection to pass that value because it has a conventional sentinel value of 0/NULL/nullptr.
You are correct in that optional values did not have a standardized solution, until C++17 std::optional.
If you don't have C++17, I recommend one of the equivalent third-party implementations:
* https://www.boost.org/doc/libs/1_52_0/libs/optional/doc/html...
In any case, you can use std::optional<std::reference_wrapper<MyVeryLargeType>>.
I would frankly give a negative code review to anyone who would do that.
You go from e.g.
void my_function(T* foo, T* bar, int baz)
{
// ...
foo->stuff(bar, baz);
}
to void my_function(std::optional<std::reference_wrapper<T>> foo, std::optional<std::reference_wrapper<T>> bar, int baz)
{
// ...
foo->get().call(&bar->get(), z);
}
this also has more overhead (sizeof(std::optional<std::reference_wrapper<T>>) is twice the size of T* ),
and let's not even start talking about calling conventions and the compile time cost of having to include both <functional> and <optional> everywhere.It's easy to tell that in the second function.
Second function makes for a better code review.
The first has no such indication, except hopefully some human-readable comment that the pointer may be NULL.
I don't understand. If the parameter was not meant to be sometimes null it would be a reference; not a pointer. The fact that a pointer is used is the "this may be null" indication.
But C++ is a systems programming language and * is still quite useful.
> I had the luxury of starting from an empty buffer
Let's assume that I'm willing to flush the cache. Are there and C++17 learning resources that you might recommend? My day-to-day languages are Python and scripting languages.The ISO C++ Standard draft (unironically—it's actually quite readable and is actually up-to-date unlike a lot of books)
Tour of C++ (book) by Bjarne Stroustrup (start here, you can read it in one weekend)
* It's gosh darn huge
* While not the hardest standard to read, it is still a standard, so has to be focused on really really low level detains and precise complete explanations instead of being a good learning resource
That said it is a great resource if you're ever confused about some corner of the language -- a lot of articles explain things slightly incorrectly.
Also the books by the person whose blog spawned this comment thread seem to be pretty good.
Is that the case? If so, how would you recommend someone go about learning c++?
As in: have the same binary? You could probably use the same object files, but shared libraries and executables probably won't work (without trickery), just because the file format is different.
Yes you can write cross platform applications though perhaps only the engine, not UI.
Has this changed?
what do you mean by "not C++ compatible" ? Qt is 100% C++ code. The few macros you see (signals:, slots:, emit) are #defined to nothing or public: / private: in a Qt header
(1) Most C++ code I encounter is C++11, and I deal with lots of projects. It's rarely sensible to change the language version of a large code base without a very good reason.
(2) Many developers are well-familiar with C++11.
(3) There's no widespread perception that C++14 or later bring compelling language improvements.
(4) Some (most?) C++11 developers are already uneasy with the complexity of C++11, and aren't eager to incur the learning curve and (perhaps) new set of pitfalls associated with newer C++ versions.
(5) Some C++11 developers look at the language trajectory, especially in terms of complexity, and have decided that their next language(s) will be e.g. Rust or Julia instead of C++14, C++17, etc.
I suspect these factors all contribute to the momentum that C++11 seems to have.
While the number of features and quirks in standard C++ grows with each version, the subset that a developer needs to remember to write correct idiomatic code is shrinking with each new version as capabilities become more general and the expression of those capabilities simpler. There is a new, simpler, and vastly more capable language evolving out of the old one and you can increasingly live almost entirely within this new language as a developer. I hated classic C++ but I actually really like this new C++, and it allows me to write far less code for the same or better result.
There are still many things that C++ can express simply and efficiently that Rust, Julia, etc cannot. If you work on software that can take advantage of that expressiveness (e.g. databases in my case), the cost of changing languages is quite large.
How is that true in practice? I would expect that once you have a codebase big enough, instead of living only with the new language you still have to deal with (or at least read) code written in previous versions of the language. And so instead of having to know only the modern subset you would have to keep in mind all the potential quirks from previous versions.
This does imply some ongoing maintenance work related solely to keeping the code base consistently "modern". This almost always makes the C++ simpler and more maintainable and often reduces the line count, so it is worth the nominal investment. The massive improvement in template code readability is worth the price of admission all by itself.
In the examples I've seen it seems to mostly come down to compiler, toolchain and 3rd party binary availability for the new versions, like with any major language version.
If for example you are using stock gcc from Ubuntu 16.04 for your builds, you can't use C++17.
All 3rd party C++ libs need to be upgraded, all internal projects need to upgrade in an orderly fashion.
On top of that, just the change of the compiler will expose some pre-existing bugs.The bugs might have been there, but are not a problem for the business if they currently don't show.
I've seen projects with a core team of half a dozen people, supported by the other SW engineers working for 6 months just to upgrade to a newer version of windows and the compiler. This was mostly due to unwise SW-design decisions accumulated over 15+ years.
It creates a lot of work, and frequently the nature of the bugs are not material (i.e. in context they may not produce a runtime fault), but I do find it to be good hygiene. I used to do builds with two different compilers for the same purpose, though that found compiler bugs as often as it found issues in the code.
https://bugzilla.mozilla.org/showdependencytree.cgi?id=15606...
https://bugzilla.mozilla.org/buglist.cgi?bug_id=1396098%2C13...
auto_ptr has thankfully be removed, but if you have legacy code that uses it it won't compile under C++17. Of course if you have code that uses auto_ptr you should fix it, but realistically nobody has the time for that.
C++17 does break some code by removing a few things, but they're pretty trivial and nothing sneaky.
[0] https://clang.llvm.org/extra/clang-tidy/checks/modernize-rep...
I don't know what the hold up is on modules. Even a basic "this header uses a C++11 namespace" functionality would fix most of the problems.
But even better would be something like "extern 'Cwrapper'" that would let you expose C++ objects as a C API with a void* object pointer without having to do the pointless wrapper dance every time.
Like make the compiler convert "class Thing { doThing(int a); }" to "typedef void* Thing; Thing_doThing(Thing* this, int a);" automatically so that other languages can call me.
If you switched to the new ABI with c++11 then c++17 as far as I know brings no new breakages.
that is a problem with your distribution (and the reason why using distro packages sucks for development), not with C++.
The ABI only changes if you change the value of _GLIBCXX_USE_CXX11_ABI with libstdc++
Considering that was only 5 years ago, I wouldn't consider this a stable ABI.
This is exacerbated by the lack of usability in compilation toolchains that promotes header-only libraries just for build simplicity and for which the data structures often won't be compatible across .so boundaries.
There are also a number of features not really intended for user code designed to help make libraries faster, and more stable. The downside of course is, well, just look at library source. But really the source to the C standard library has similar issues.
I don't like header-only libraries because they slow down compilation, and slowly the trend is starting to abate.
As somebody who kicks C++'s tires once in a while and is always put off by the incredible complexity of the language, and the complexity and friction and fragility of its workflow, I was sad not to see any explicit mention of pairing an ABI break with removal of the bad parts. (Yeah, I know, that might mean source-level backwards incompatibility...) Makes me wonder if it's ever even discussed at a high level.
This doesn't change what people other than me might be thinking, of course.
I found it way easier to deal with Go, C#, and Ruby documentation for example.
The reason that it is popular in the C++ community is that it cuts down legalese a lot when compared to the language standard.
Try books first. Meyers and Josuttis write excellent books. The template guide by Vandevoorde and Josuttis is my gateway drug to becoming a language lawyer. It is still hard, but at least manageable.
That's pretty interesting. I'm not even sure how to test the effect of
"Hello, world!" = <something>
I imagine the reason this is allowed has something to do with how strings decay into pointers.> I found it way easier to deal with ... Ruby documentation for example.
Maybe in documentation detailing the methods available, but as far as I know, there's no official spec for the syntax nor the details of the semantics of the language.
Not a fan of the spongers that scrape it though.
(2) This used to be the case with C++03 (or C++98), and it changed...
(3) I partially disagree and partially claim you're begging the question. You don't have poll results to suggest what people think of C++14. But - C++14 had a lot less changes compared to C++11 or C++17; this was natural considering how the cycle of debate of potential features progressed.
The thing is, the fact that C++14 wasn't that revolutionary in terms of features also means it is easier to adopt. If you want more novelty - try C++17; or - try a C++14-dependent library like Eric Niebler's ranges-v3.
(4) C++17 allows for writing less complex code in many cases. Specifically, you can significantly reduce the need for template metaprogramming - a particularly hairy part of the language - by using more constexpr functions and if-constexpr in regular functions. And there are other examples. Like Bjarne says: The C++ committee endeavors to "make simple things simple (to write)". So it's a trade-off.
(5) Some people switch languages because of workplace decisions or personal preferences, that's true; but people also adopt C++ because of what becomes possible, or easily-expressible, with newer versions of the language. For example, using the ranges library and with C++14 or later, functional programming with C++ has become quite reasonable, borderline pleasant even.
---
Bottom line: In my opinion, at this point C++11 has more inertia than momentum. And that's ok. People will progress over time.
Also, this is an "easy" breakage, in that it was glaringly obvious. That is, you didn't encounter a strange compilation bug and had to investigate why it occurred, or got different run-time behavior of the same code.
I feel that the template metaprogramming framework deserves an overhaul to clean up historical mistakes and keep only the good part.
I would argue the opposite: C++11 is a fairly different paradigm to C++03, and upgrading an existing code base is a sizable project.
But once you get hooked into the concepts it introduces (auto, lambdas, templates, constexpr), each subsequent revision of the language standard introduces some improvement to these concepts that deals with unpleasant edge cases the earlier revision had, so each of them promises to make your existing use cases simpler, at a moderate rewrite expense.
Constexpr if (introduced in C++17). If you're building a library that uses templated types (and therefore static dispatch instead of virtual functions and dynamic dispatch), then using constexpr if dramatically simplifies the implementation of your code.
C++14 didn't add a whole lot of new things; it mostly improved on existing features.
C++17 adds a number of new things.
I would use it just for std::optional and std::variant though.
- Concepts (Now I can use static polymorphism without all those SFINAE template hacks)
- Designated Initializers (Finally!!! Using POD structs became way more convenient now)
- Coroutines (Would be pretty nice for writing games / interactive applications)
The things I don't have any interest in (but don't care if it's in or not)
- Ranges (Too much complexity for something you could do with plain old for/if loops...)
The things I'm worried about:
- Modules (Theoretically really good, but in practice the whole thing is starting to become a mess, especially when it's interacting with the preprocessor and external libraries, and when trying to preserve backward compatibility.)
Except if your headers include macros or templates, which must be evaluated every time. This is not separate compilation. What C/C++ developers have been doing for 20 years is jumping through hoops to incrementally reuse previously compiled results because C/C++ violate basic modularity principles [1]. They call it "separate compilation", but it's a pale shadow of true separate compilation.
Proper modules would enforce abstraction boundaries so true separate compilation becomes possible. I can't speak to the specific C++ proposal here, but enabling separate compilation is exactly what modules are for.
Since headers can redefine macros, that also means that even including headers in a different order can change the meaning.
Older languages like C++ with headers are simply source files with a different extension, so your individual source file compilation is actually loading in and parsing cross-sectional subsets of your full project. Modern languages typically define the interface inline with the code, and export that interface to other modules which would consume it. The reason that you don't have intra-module distributed build is because the intra-module code dependencies are implicit - there is no developer-defined partial cross section of the code, all of the code needs to be pulled in and semantically evaluated as a unit.
You could hypothetically still do distributed optimization of the code after it is semantically evaluated, but TBH there is a lot of time wasted in evaluating headers for each file. Modern compiled languages usually have compilation time as a restriction on the implementation and design, and elimination of the need to repeatedly parse and semantically evaluate files is part of their design.
And this seriously somehow doesn't seem like a problem to you? My massively distributed (to AWS Lambda) C++ build compiles at the speed of the slowest single translation unit. You seem to be optimizing for serial compile performance, but if you truly want speed you have to go parallel. These explicitly-managed "cross sectional sunsets" are what makes this so easy to do with C++.
This article is possibly out of date--or maybe was wrong to begin with--but it shows how the modules concept from earlier this year simply broke parallel build efforts by enforcing a ton of serial dependencies.
https://vector-of-bool.github.io/2019/01/27/modules-doa.html
than raw loops ? do you have an example ?
// Point2D point2d {.y = 2, .x = 1}; // (1) error
Oh my. The C++ committee keeps their flight altitude rivaling moles and worms. This is another brittle feature, just like the initializer lists that have to mirror the declaration order. In what world is this a better option than allowing any order? Can't compiler implementors be bothered or what is the rationale?? C does this better. struct A { int a, b, c};
you need to do this: A a = { .a = 1, .c = 3};
but you can't do this: A a = { .c = 3, .a = 1};
because... well, because C++. There is no reason to disallow the second form except for .. what exactly? What is the reasoning of the C++ committee to not be able to mirror what one can do in C (where, aside of the missing typedef or "struct" in front of A, both forms are valid)?Just as you're saying, the C++ committee seems to be confused about order-dependent and named initializations, and for some love-of-bean-counting insist that named initialization needs to respect order _as well_.
Which does not means that it couldn't be done, in fact constructor initializer lists have the same issue and you are allowed to list them in any order but this is considered a language mistake and most compilers warn about out of order initializer. This mirror the arbitrary evaluation order of function parameters which is also considered a mistake.
In the end there was no consensus to allowing an arbitrary order, but in the future that can be relaxed without breaking any code if a consensus is reached, while the reverse could not be done.
As usual it is always compromises.
Anyway, at last for me, initializers are more about writing more explicit code and be more robust in the face of changes.
The following:
Point2D pt = {.y = 5, .x = 6};
should not have meant that 'y' is initialized before 'x'. It's only a lexical convenience that exists before parsing. When the code is parsed into an AST, the actual AST would reflect the following code: Point2D pt = {.x = 6, .y = 5};
There is absolutely no reason to enforce declaration order.For what it's worth, I find strictness pedantry like this fairly useless in production. For example, a JSON file has an order to the elements of an associative array, but Javascript doesn't. So most of the time, I treat associative arrays as having no order, and use another array to map indices to keys or whatever.
Contrast that with PHP, which does maintain key index in the order it's added. There have been countless times where it turned out that I needed that order, and it saved me the work of declaring the extra array. Even more importantly, I've never been hit by that surprise (of not having order) in PHP, whereas I have in pretty much every other language.
So maybe PHP is less pure, but truthfully, pureness is the single biggest source of friction in my daily life. Basically the entirety of my day now goes to setting up environments from minimalist environments, working around subtle language errata, or translating what I want to do in my head to the domain-specific language decisions that are so widespread in languages like Ruby.
I prefer the lazy route now in most things. Just give me everything, with the least-friction needed to do what's in my head, and let me prune it/optimize it when I'm done. So in this case, I'd prefer C++ to have a strict ordering of elements, but let me initialize them in any order, so that I can think in abstractions rather than implementation details. Which is why PHP is roughly 100 times more productive for me than C++, even though I grew up with C++ and know it like the back of my hand.
* An opinionated, modern packaging and dependency story (like go modules, Rust crates)
* built-in library support for logging, http, zip, gzip, json, yaml, template rendering, RFC3339 datetime reading/writing
* the dream: compliant compilers must be able to compile down to static binaries, cross compiling built-in.
Features C++20 actually has:
* new fancy spaceship operator that I'll now have to learn and never use...
That's completely out of scope for a programming language pretending to be taken seriously as a general purpose one (including e.g. systems programming).
Also, your list looks like a pretty arbitrary choice of protocols and formats, why don't we also add support for SSH, SMPT, IMAP, PNG?. In the end, everyone would want their niche protocol/format to be included in the standard.
> the dream: compliant compilers must be able to compile down to static binaries, cross compiling built-in.
Cross-compiling to what architectures specifically? The most used ones are proprietary and I think that disqualifies them from being included in a serious international standard, since you'd need to read the spec and implement it in order to be compliant (I'm not versed in ISO inclusion requirements, so this may be already happening, but it would still be wrong. (grepping "x86" in the offical C standard returns no results though.)).
There is a Boost library for each of those things.
> An opinionated, modern packaging and dependency story (like go modules, Rust crates)
> the dream: compliant compilers must be able to compile down to static binaries, cross compiling built-in.
You can use Conan or vcpkg. LLVM basically solves the cross-compiling issue since you can take the IR to any platform that has a LLVM target.
Neither of these are feasible to include in the International Standard because C++ runs on more than amd64 and would make a lot of obscure platforms no longer compliant with the standard. Rust crates are nice, but people building medical devices with C++ shouldn't need to worry about supporting an npm-like monstrosity in their builds.
Rust compiles on far more platforms than amd64, and Cargo works just fine.
Nothing forces you to depend on any packages you don’t want to.
Nothing stops you, either, which is a problem for everyone in the future who uses your code. The first reaction to any problem in JavaScript/webdev is to google for a library to import to solve the problem. This is acceptable behavior because the existence of a web browser implies powerful enough resources to accommodate bloat. But this mentality isn't acceptable on other systems and has even infected some basic functionality:
The random number generator for Rust has 6 dependencies. In C++, it's only lib[std]c++. In Java, it's java.base.
Most other langues with a "simple" built-in are unfit in some of these aspects, so you may end up with a footgun and still have to find a replacement.
In Rust you can choose which algorithms are used and how. And because it's a regular package, it can be improved without breaking the language.
Note that number of dependencies doesn't mean anything. Some crates are split into tiny deps for each feature, so that you can reduce binary size and compile time if you disable optional features.
But stock C++ has many options for the random number generation algorithm:
https://en.cppreference.com/w/cpp/header/random
As does Java and .NET. Without any dependencies and guaranteed to be portable.
> Note that number of dependencies doesn't mean anything. Some crates are split into tiny deps for each feature, so that you can reduce binary size and compile time if you disable optional features.
In theory I agree. It just seems like one of the persistent unsolved problems in software is dependency hell (spawning the whole containerization movement), so I'd like to avoid it if I could.
Most of the industries using the language optimize their code for a target platform. And general non-optimized portable binaries are simply not cost effective, so standardizing installation has not been simple.
If you do care about portability, you functionally need to. - ensure you're not linking to anything but LIBC and the VDSO
- compile with no assembly language
- disable most optimizations, and select a sufficiently generic hardware target. For the most part, they succeed in features.
- directly compile all 3rd party source in your binary.
Then, your binary should last a long time.
That all said, I believe your needs are in the minority in the C++ community, and other languages likely better suit your needs.
There will never be a standard build system, but CMake is the de facto standard build generator and works well enough.
I'm hoping I can map them into GPU space -- looks like the standard has adequate control for this.
It's often overkill for what I do, especially as it can't use the same compiler options as my tree normally does (they have to bend over backwards for back compatibility; I don't). I do use it some places. I certainly don't envy the hard work of library developers!
When I simply need a 2D or 3D resizable array it's often easier, as I mentioned, to simply spin one up. Having that support "native" will be great.
Modules: While it is important to be able to fine-tune linking and compilation in some settings, in most, and especially for beginners, this should be handled by the compiler. Especially when compared to other modern languages, C++ is much more complex to understand the toolchain and what's going on under the covers with the dichotomy of compilation and linking. Header files were a hack that have been around for too long, and there needs to be less separation between the interface and actual binary code when compiling. This is a headache both for novices and the experienced.
Concepts: The missing link for templates. Besides removing some of the roundabout-ism of SFINAE by providing compile-time verifiable interfaces, I think the biggest benefit of concepts will be the error messages. Right now, the state of errors in heavily templated code is abysmal, and this translates to a lot of wasted time for developers. Concepts should allow the errors to indicate exactly what's wrong.
I can't wait to be able to use these in public code. Some compilers support concepts with a flag already (and GCC 10 has it without the special flag), though none support modules yet...
What kind of things can the compiler improve? For instance it can arrange the code in a way that the instructions of the hot path are directly behind each other, whereas the unlikely case jumps further away. Or it can arrange code a bit to help the branch predictor.
It seems conceivable that it might also affect the compiler's inlining choices in each branch (i.e. don't bother to inline as aggressively in the less likely branch, to reduce code size), though I don't know for sure.
But is can already figure that out often especially if it sees exceptions.
Better is to just use the profile guided optimization in the compiler.
Say you are writing a high speed trading program. At some point you get to the "if(wouldMakeMoneyOnTrade())...". However for every time this passes and you trade there are 1000 where it fails and you don't. However time is critical because your competitors are using similar algorithms and so you need to beat them - thus you put in a likely and ensure that the CPU jumps to the trade code the fastest possible. Your competitors that use Java pay a CPU branch prediction penalty here because the hotspot optimizer has noticed that the if almost always fails and optimized for the false case. Thus you can make money by beating your competitor to the trade. (Note, this doesn't mean you can't use Java for your code, but if you do you need to manage that risk somehow)
The trick is to remember that garbage collection is not some sort of werewolf that wakes up and decides to mess with your stuff whenever it feels like it; it (can) happen only at certain, documented points. So you have to write your code in such a way that it does not do memory allocations in the critical path (which you would not be doing in C++ anyway, for a similar reason; malloc/free is expensive).
Generally if you're doing HFT your critical path is not going to being doing much anyway. A few reads, comparisons, maybe some trivial arithmetic and that should be it.
// Compilers can use the information that a certain branch is not likely to be
// taken (for instance, a CHECK failure) to optimize for the common case in
// the absence of better information (ie. compiling gcc with `-fprofile-arcs`).
//
// Recommendation: Modern CPUs dynamically predict branch execution paths,
// typically with accuracy greater than 97%. As a result, annotating every
// branch in a codebase is likely counterproductive; however, annotating
// specific branches that are both hot and consistently mispredicted is likely
// to yield performance improvements.
Today __buitin_expect are mostly used to mark hot/cold branches. The compiler will normally optimize hot code for speed and cold code for size also will attempt to put cold code in separate cachelines and even pages. And other similar code layout optimizations.
I was able to start a project in c++17 in 2016, though at the time I had to use boost::variant with the Xcode compiler and boost::filesystem for all three compilers (only a limited use so I could even have skipped it). Also for some complex third party libraries I had to make a little trampoline file that compiled in C++14, or with special flags, and then call those entry points.
Since I was starting from a blank slate I also compiled everything with pretty much maximal warnings an -Werror which really improved the code a lot, something you can never get away with with legacy code (not to insult working legacy codebases!)
James Dennett and Geoff Romer. P0927R2: Towards A (Lazy) Forwarding Mechanism for C++. ISO/IEC C++ Standards Committee Paper. 2018. url: https://wg21.link/p0927r2.
Would be nice for my threaded logger to know if it can copy the pointer instead of the whole string, knowing it was initialized statically.
Think of `strcmp` which returns an int. The int can be <0, ==0, or >0. That same `strcmp` can then be used to implement all other standard comparison functions. `operator <=>` is the generalized version of that.
Yup. My C++ code would often include a `int T.compare(const T& a)` member function. Then any comparison operator would be custom written too -- but only to delegate to the custom compare and check its result.
This one feature alone will eliminate several lines of boilerplate.
I can't think of any case that would have any appreciable difference in speed.
Is it mostly around eliminating branching logic?
Wouldn't there be a good chance that might be inlined and optimized away anyways?
Along with automatic synthesis of all the operators, c++ 20 also clarified rules about overload-matching and automatic construction of objects for purposes of comparison, so you don’t have to write the friend methods to for example compare int to your type if you have trivial int-taking constructor for your type. Also if your == operator or != is more efficient than a full lexigrapgical comparison the compiler will now do the right thing
https://www.bfilipek.com/2018/08/cpp17indetail.html?m=1
That guy has a great blog where he writes at length about each feature, and the book is a compilation of that.
But C++ would not be C++ if it had no shenanigans coming. Unless they have changed things within some months the new calendar/datetime library is hilarious.
It has features like being able to write a date like: 1/2/2000y. ooh fancy operatoe overloading. Which one is month? The first one ofcourse as it they didn’t standardize on any of the existing ISO standards, or allow you to choose, but selected ”Customary US way” as the Only True C++ Way(tm).
Also do not forget the abuse of operator overloading like in the good 90’s when misusing it was the recommended thing.
I suppose it opens up the possibility of adding std::basic_fixed_string<T> later and then have std::basic_fixed_string just be a specialization?
And no, if it weren't a template, it would not be possible to make it a template in the future as it would be a breaking change.
typedef _string<char> string etc
Or maybe not, I don't know what they had in mind, which is why I'm asking...
template <class T = char> class string { ... };
string s = "a";Try:
#include <cstdio>
template <class T = char> class string
{
public:
string(const T *const c) : c_ptr(c){}
const T *const c_ptr;
};
int main()
{
string s = "qwe";
printf("%s", s.c_ptr);
return 0;
}I do a lot of node.js programming, which uses C++ to write extensions, so here's hoping I rememeber it if/when I need to.
If it's important, it's using C++.
Actually a bit frustrating, since Qt is very opinionated in a way that hasn't aged well with newer C++ standards, but it is probably one of the best cross platform UI libraries.