How to Adopt Modern C++17 into Your C++ Code [video]
youtube.com
youtube.com
We are using C++ for embedded devices and recognize a steady code bloat with every release since C++11 (especially with C++17) without using any of the new features (with gcc/clang). This is a trust-killer and actually the reason we stay on C++11 for embedded development.
1. Do you see the bloat even if you don't use post C++11 features but compile using the C++17 standard?
2. Do you think it is mostly the compiler that is causing the bloat alone? Or is it stuff from the standard library header files that some how gets linked in (and are not used or needed by your software)?
Yes, actually I tried using various C++ snippets and even reported that to the GCC compiler team. It happens with simple stuff like std::string and std::vector. The response was something like, that there really seems to be a bloat, but no performance impact and I guess most users outside of embedded don't care too much about the size of the compiled binary.
2. Do you think it is mostly the compiler that is causing the bloat alone? Or is it stuff from the standard library header files that some how gets linked in (and are not used or needed by your software)?
That's actually a very good question I cant give an answer to - meaning I haven't looked specifically into that.
As C++17 came to GCC I played with the compiler explorer and observed this by just switching gcc/clang version and -std flag. Actually, you can try it yourself: https://godbolt.org/
I would love to post my code snippets from back then, but Im not home currently and the time Im back home I guess nobody will care about this anymore :D. Maybe I put it into an article.
The intermediate inline stages greatly expand the code size, until the point level where all the abstraction can be compiled away. I guess that -Os simply settles for a local optimum and gives up inlining early.
Also, static size does not mean anything. The only thing that matter is the dynamic size (i.e. the instructions that are actually fetched at runtime: code that isn't run or run rarely doesn't matter (then again, such code is a prime candidate to be compiled with -Os).
Just turning on C++11 in the compiler brings move to all standard library containers. The header files were not just "somehow" linked in and not used, the additional code was used in many places behind the scene. I haven't done benchmarks on my code, but the general report of those who have is that in exchange for the extra binary size the code runtime was 5% faster. For most people these days that expense is worth it.
The performance of embedded MCU's are continuously rising over the years and that little overhead is buying development speed.
Not to mention smart pointers, templates and constexpr making my life easier.
The only real issue with C++ is, that as soon as you get into serious embedded applications, you have restrictions when it comes to heap usage e.g. in medical devices using the heap is forbidden. So you cant use the STL.
There is a promising embedded STL project, but it's not there yet: https://www.etlcpp.com
https://github.com/electronicarts/EASTL
(In case it wasn't obvious, disclaimer: I work at EA, and frequently do work on EASTL, though I'm not the primary maintainer.)
std::array is in there since C++ 11 and it doesn’t use heap.
And/or you can use STL with custom allocators that work without heap. We did something similar developing for Nintendo Wii console. There was a heap but we didn’t want to use it to avoid memory fragmentation. AFAIR we used two arenas (essentially stacks), one very small for temporary data cleaned up at the start of each frame, and a large one cleaned up when a level is unloaded.
However, I don’t have hands on experience developing firmware for medical devices, so I’m not sure it’ll work for them.
So you can do things many different ways. If you do say "we do this like X, which is industry standard, just like T, U, and V do" it's a simpler argument than "we do this like Y. Lots of people do X, but here is how we have demonstrated Y is better for us...". But this can be fine too, just possibly more work.
Also worth noting (a) there is no industry-wide best practices agreement (b) there is no FDA wide agreement on what should be done (different device types are reviewed by different panels (c) the FDA doesn't understand software development deeply across it's panels, but it is catching up.
It's especially true if you're saying "This new device is just like our previous device, with these few small changes". (I forget what that's called, but you can do a lot less paperwork if that's true.) But if you start doing memory allocations where you never did before, they're probably going to want to apply higher scrutiny to your entire software. That's... painful.
But it is worth pointing out, FDA (or other national body) isn’t doing code reviews. They are mostly interested in process , and how you follow it. The place where this really interacts s is hazard analysis, and then it’s just up to you to demonstrate your checks and balances.
With static memory techniques you can "prove" the system has enough memory to work in all modes, i.e. device consumes 20 readings per second, keeps them in a ring buffer backed by a static fixed array, that buffer is large enough to satisfy processing rate.
Out of curiosity, what is the rationale for not using the heap with medical devices?
Avoiding heap allocation is not at all a general constraint for medical devices. For certain types of components (think safety-critical real-time sub systems, for example) they are going to be very interested in your hazard analysis and the mitigating approaches to possible issues.So if there is a way to say: we don't have to worry about [class of error X] because we don't ever do Y, that's a straightforward way to sort out those components. If you have a compelling tech reason to do Y, better start thinking about all the controls you'll put on it.
Think about it this way: What's the worse thing that can happen if your code causes an OOM error? If the answer includes things like "somebody dies if it happens at the wrong time", you'll want to be really careful to prove (prove, not just test out) that can't happen.
- Fragmentation.
- Non-deterministic runtime (in the real-time cases).
- Insufficient analysis of worst-case conditions (i.e. you haven't worked out what your worst case RAM usage is, otherwise you would have statically allocated it).
IMO, the worst is the final case as it shows a lack of thoroughness in the design as a whole and brings the rest of the code into suspicion. Fragmentation can be worked around, but not the others.
Is it still possible today to pare down the compiler output like that? I imagine a lot of modern C++ just doesn't work unless everything is enabled.
Check this talk about fitting C++17 on a C64.
CppCon 2016: Jason Turner “Rich Code for Tiny Computers: A Simple Commodore 64 Game in C++17”
https://www.youtube.com/watch?v=zBkNBP00wJE
Also be aware that embedded devices like Arduino and Cortex-M (with Mbed OS) do use C++ toolchains.
CppCon 2016: Jason Turner “Rich Code for Tiny Computers: A Simple Commodore 64 Game in C++17”
https://www.youtube.com/watch?v=zBkNBP00wJE
Most of the time is either religion against C++ or lack of modern tooling, given that most embedded toolchains are stuck with C90 and C++98.
possibly. but given the how often i've seen c++ users treat c users like idiot savages or heathens that need conversion ("have you heard the good word of our lord and savior, c++?"), i could understand a negative sentiment.
"Although the first edition of K&R described most of the rules that brought C's type structure to its present form, many programs written in the older, more relaxed style persisted, and so did compilers that tolerated it. To encourage people to pay more attention to the official language rules, to detect legal but suspicious constructions, and to help find interface mismatches undetectable with simple mechanisms for separate compilation, Steve Johnson adapted his pcc compiler to produce lint [Johnson 79b], which scanned a set of files and remarked on dubious constructions."
Dennis M. Ritchie -- https://www.bell-labs.com/usr/dmr/www/chist.html
I would like the stack underlying my computing needs not to look like a Swiss cheese.
And yes, C++ is also not the ultimate solution for that as it is tainted by its C compatibility.
Now, I'm aware that you can restrict yourself to "almost C" in C++, but no one ever seems to be doing that. A litte str::string here, a tiny std::vector there, so exceptions are already in, so why not go all the way.
Avoiding bloat and performance hits are trivial in comparison to structural and architectural benefits in the modern language.
Rewriting something that already works would be silly of course.
> and size is an important factor you may be reluctant to trade up. Bloat avoidance is not trivial, in my opinion
I don't agree with this in comparison to C. C is still there, but you have destructors and ownership semantics on top of it. Even templates can be used for things like type checking in debug builds.
Nested namespace definitions and structured binding declarations are also nice.
I just finished my third semester of using C++ (with Data structures 1) and we use NO modern stuff in class [1]. Raw pointers. New and delete. No Lambdas (although we learned about function pointers, though not function objects). I completely understand why (we're learning low level stuff and it's worked for the last 30 years so whatever), but now I'm trying to ramp up and learn modern C++ for a personal project I'd like to start working on (basically an audio plugin using JUCE).
Here's what I've been doing to try and catch up (I have two weeks off before my internship starts). I try to watch Bjarne and Herb's keynotes but they often are speaking to an audience already familiar to modern C++, so this posts video is great! I've discovered the brilliance of Sean Parent, so I'm watching all of his talks. I've ordered Scott Meyers Effective Modern C++ (and hopefully he uses the money for a new haircut /s). I've browsed through the C++ Core Guidelines [2]. I'm reading the JUCE tutorials (and source code) which is beautifully written.
What are other good resources? I'm particularly interested in developing GUI's. I'd like to see some tutorials written like Herbs talk here: for people new to C++ and new to modern C++.
[1] This is our textbook: https://www.amazon.com/Programming-Program-Design-Including-...
[2] http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#...
Effective Modern C++ is part language tutorial, part series of tips, part philosophical discussion on how one should use various features. It's not appropriate as an intro to modern C++.
See, I take the opposite stance. There's enough moving parts here that it's nice to get some background into what's actually happening, and then read more cookbook style stuff when you understand the underlying context.
Is it a useful book if you plan on keeping a C++03/C++98 codebase? Probably not, Effective C++ is the better book for that world.
Is it a useful book for migrating from premodern to modern C++, even iteratively? I'd say yes, that's one of the book's explicit design goals.
Modern day C++ has two ways to do most of the things you want to do: The C way, and the C++ way. eg, c array[] vs std::array<>, or char* vs std::string and the like. However, original C++ without C is still there and not updated. Eg, inheritance is a C++98 feature, not a C feature, so there is only one way to do inheritance for the most part.
It takes little time to identify what features are C features and what features are C++ features. From there you'll know when to look up alternatives and when not to, giving you an easy upgrade map.
Also, some C features are more powerful, so it is a matter of choosing the right tool for the job. For example, looping through a data structure and adding elements. Doing so invalidates iterators, but works will with an index, so it might be ideal to use the old C way in situations like that. This also adds explicitness to code. If you're working in a modern code base and in the rare spots where the C way is done, you can get an idea why and from that what the code is doing without having to read all of it. This makes reading code easier once both C and modern C++ have been learned.
Don't forget the other information that you can get from speakers who are not as good. New and shiny is often easy to abuse simply because nobody is sure if it is a good idea or not. Not everything stands the test of time. Other things are great and critical in their niche but create an awful mess when abused to apply elsewhere.
Josuttis' The C++ Standard Library is the classic reference.
Are you sure it's not just because your teachers are not (enough) aware of those new features ? Because I work in a University and know way too many colleagues that still use new/delete on a daily basis.
The professor who taught my Data Structures class (who is an exceptionally great teacher BTW) actually uses C in most of his own work, so we even learned a little bit about how to simulate recursive calls using a void* stack and goto. I hope my peers don't go out and write code like that in the wild, but that's another story..
And that void*/goto hack sounds bad, even in C land. That's basically only acceptable in a byte code interpreter.
1. I didn't see what the smart pointer stuff had to do with C++17. Aren't these all available from C++11? Are these something new that I missed, or are they just comments on how to avoid bugs, based on Herb Sutter's personal experience?
2. Should I std::string_view everywhere I used to use std::string now? The examples given seemed to lean heavily towards using std::string_view as parameters instead of std::string, but should I use them as a drop-in replacement everywhere?
3. The std::optional example looked really clunky, relying on checking an exception. I see there are a bunch of nice operators and utility functions on std::optional that cover some use cases, like coalescing, but will we get something that allows for optional chaining?
Regarding memory and performance improvements std::string_view should better than using std::string, but don't go overboard replacing all code. It depends what kind of application you are writing, the age of existing code, or sometimes a plain old const reference might be enough.
There are some ongoing efforts for such chaining
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p079...
https://github.com/TartanLlama/optional
EDIT: corrected the make_unique/make_shared misinformation.
You might have a typo here. make_shared was available in C++11. make_unique was left out from C++11 as an oversight and added in C++14.
2. Use std::string for when you need to own a string. Use std::string_view when you need to reference a string [a]. Generally you want std::string_view for parameters and std::string for member variables. Return values and local variables can be either, but if you're not sure, std::string is safer though slower.
3. You chain optional, but only with nested functions, and you code starts looking more and more like lisp. If you are a fan of a chained syntax, you might be interested in libraries that support "ranges". boost already has ranges and there are proposals to add them to C++20.
[a] If you want more specifics and detail, the CppCoreGuidelines have a section on when to use which string. https://github.com/isocpp/CppCoreGuidelines/blob/master/CppC...
If you own the actual string, use std::string — there is no other string for you to view.
If somebody else owns it and you're working on the whole string `const std::string&` is probably your best bet — you're holding a reference to the string as a whole — but std::string_view also works and might make more sense in context.
If you're working with a substring of somebody else's string, std::string_view is the natural choice.
The key idea here is that you want to reference, rather than copy, some underlying string. If you're working on something where your code needs to own a deep copy of the original string for some reason, by all means continue using std::string!
Actually string_view is more general. You can build one out of a string literal, a vector<char>, some memory from a memmap call, and an unbounded list of other things. If an interface uses a `const std::string&` and you don't have one, you have to, implicitly or explicitly, copy the data into a `std::string` to be able to use that interface.
You are correct, there is no safety --- you can call the constructor std::string_view(const char, size_t) and stuff anything you want in there. The main reason string_view is nice is the implicit conversions you get from the other string types. Pass in a "const char*", it finds the terminating null for you. Pass in a std::string, no problem. No boilerplate.
But the real benefit is in providing it as a "vocabulary type". Once C++ code starts using string_view everywhere, you don't need to massage your char sequences this way and that [1] to pass it from the top of your application all the way down into the low-level libraries the application relies on. And you don't incur the overhead (both in lines of code and in copies) of all that massaging.
[1] Maybe your request handler has to convert from char* and size to a string to call your business logic, then your string has to convert from a string to a char pointer and and int length to call saveName. saveName might actually use std::copy under the hood to put the data into a chunk of memory, so it turns the pointer and length into two pointers.
- It encodes the invariant in a type (char * points to at least size bytes, no expectation of null termination, no ambiguous definition of an empty range)
- The size always travel with the pointer so no chance that it gets separated.
- syntactically simple conversion from string (or other string-like entities) to string view reduces clutter in the code.
I consider lack of null termination a plus.
Encodings in general a bit of a headache. I work on Windows and it wants UTF-16 internally and everything external wants UTF-8.
Nope. It must be understood that std::optional is not an Option type, its purpose is not to have a way to express "missing" items in a type-safe manner but rather to have pointer-type semantics without needing to heap-allocate. So you can just deref' an std::optional, and it's UB if they're empty. Don't think of it as Haskell's Maybe, think of it as a stack-allocated version of std::unique_ptr.
I also don't see why you would ever use it in the way you're describing. Even if you had template code, either you manipulate objects/references, or pointers. And worst case, you usually try to handle references, rather than trying to risk UB.
Operative word: type-safe. std::optional is not that.
> I also don't see why you would ever use it in the way you're describing.
I'm literally just describing the semantics of std::optional.
Except that's exactly why you would take an `optional<int>` as a parameter. To avoid the ambiguity and possible bugs of taking an `int*`.
Except for the type-safe part which is the important bit (hence the emphasis on it), and which std::optional does not provide.
> To avoid the ambiguity and possible bugs of taking an `int*`.
The only ambiguity and possible bug (singular) you're avoiding using std::optional is the question of ownership. Because as emphasised in the original comment std::optional is no more type-safe than a raw pointer.
Even if we consider just ambiguous ownership, there are a whole host of behaviors that could result from that single design mistake, including undefined behavior, which could include trashing your production database, firing the missiles, or behaving as expected until an unrelated piece of code changes a year later.
I don't understand what you mean here. I don't have to heap-allocate to get a pointer to something; I can just take the address of something on the stack…
3: Hopefully someday we'll get a proper monadic interface in C++. Until that day, I'll keep using exceptions.
std::optional<int> a = foo(); if(!a.has_value()) { return; } bar(a);
is pretty standard.
Though, with exception handling you get function chaining which can be rather nice.
std::optional<std::string> a = foo();
auto size = a?.size() // size is a std::optional<int>Having used Rust for a year or so, now I look back at these features and they seem quite natural. I have to say the documentations and tutorials from Rust community is great. It might be a detour, but now I feel much more comfortable reading modern C++ blogs and watch this video!
(also has download links)
This guy saved me so much hassle this year and it's causing a complete sea change in the way that we work. I really really want to live in a world of value-typedness and it's finally possible to do it. The simple and natural idioms are now MORE efficient than doing it the complicated way: the average function I'm writing in 2018 is significantly less convoluted and allocates on the heap far less than the same function I'd be writing five or six years ago.
You can the summary of the gcc c++ standard support here: https://gcc.gnu.org/projects/cxx-status.html#cxx17. Full support for all features in the c++17 standard, but the support is declared "experimental".
Similarly here for clang: https://clang.llvm.org/cxx_status.html
And Visual studio: https://blogs.msdn.microsoft.com/vcblog/2018/04/26/announcin... (link to latest blog post, perhaps there is a status page that gets updated, but could not find it).
For example, TI still has some compilers stuck on C++98 and C++03.
http://processors.wiki.ti.com/index.php/TI_Compilers_and_Ind...
I can't make heads or tails out of half of the code in that example. C++ has become extremely hard to grok.
I mean, take these two lines:
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
What's even going on there? Inheriting from a variadic list of classes? `using` a variadic list of parent functions, I guess? The second line looks like it's defining a constructor function for the overloaded() struct, but it's not implemented anywhere?In the end there's magic code that looks like pattern matching, but I have no idea how they got there.
The first line defines 'struct overloaded' derived from all of its template parameters. Template parameters are supposed to be functors (lambdas), so we use their operator()'s in 'overloaded'.
The second line defines a template deduction guide for the 'overloaded' constructor, which says: take types from the constructor arguments and use them as the class template parameters.
The idea is to be able to construct 'overloaded' like this:
overloaded{[](ArgT1 arg){/*lambda1*/},
[](ArgT2 arg){/*lambda2*/}, ...}; template<class ...Args>
void g(Args... args) {
f(const_cast<const Args*>(&args)...);
// const_cast<const Args*>(&args) is the pattern, it expands two packs
// (Args and args) simultaneously
f(h(args...) + args...); // Nested pack expansion:
// inner pack expansion is "args...", it is expanded first
// outer pack expansion is h(E1, E2, E3) + args..., it is expanded
// second (as h(E1,E2,E3) + E1, h(E1,E2,E3) + E2, h(E1,E2,E3) + E3)
}
Taken from: http://en.cppreference.com/w/cpp/language/parameter_packSo for
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; }
if you used this like overloaded<X, Y, Z> foo;
it would expand to struct overloaded : X, Y, Z {
using X::operator(), Y::operator(), Z::operator();
} foo;
EDIT: Removed stuff about the function since I was only guessing and Alexeiz answered it. The only thing I’ll add is that you can construct structs like this: struct Foo { int x; float y; };
Foo foo {1, 2.3};
overloaded is constructed from the three lambdas in the same way. I guess the deduction guide is needed because the compiler cannot deduce the template parameters from that initializer form, so the deduction guide tells it to use the types of the lambdas for the types of the template parameters.More information on deduction guides: http://en.cppreference.com/w/cpp/language/class_template_arg... (specifically, look for “Class template argument deduction of aggregates typically requires deduction guide” in the notes section for an example very like the overloaded one)
First of all, cargo does not understand binary dependencies, compiles everything from source.
Second, my only use case for C++ is for native code when integrating system code with Java, .NET, UWP, Swift, Objective-C.
Third, my C++ code can also be used on the GPGPU, with nice debugging tools from all vendors.
There Rust misses the integrated workflow and IDE debugging experience that I have with C++ on those environments.
No doubt Rust will get there, by then I would gladly use it as well, but it still needs to mature a bit more.
Today's Rust release also had many goodies.
But if you're already willing to use C or C++, you've got no reason to avoid unsafe.
Worth noting that it's doubly linked lists specifically that are hard. So cyclic structures.
Variadic templates?