I love the safety path Bjarne, Herb and others are driving the language, but I wonder how much of this work will ever be adopted by the masses, given the amount of "C with C++ compiler" that I usually see.
Or code guidelines like the ones from Google, where one is even refrained from using Modern C++ best practices.
What Modern C++ practices specifically have you found that Google discourages? I'm curious. I'm reading through Scott Meyers' Effective Modern C++ and am finding so much easy and fantastic advice, I wonder if Google is at odds with some it?
It's at odds with almost all of it. Google's standards are C-with-classes based on compiler bugs circa 1999.
Despite their (misleading) marketing, Google is still just another enterprisey megacorp employing 30000 blub programmers. Expect the usual 'enterprise' failures.
> Google is still just another enterprisey megacorp employing 30000 blub programmers. Expect the usual 'enterprise' failures.
Ouch! That must have hurt 'em. Of course the original meaning of "Blub" is "anything which isn't Lisp", especially C++ (as well as say Java.) But what the heck, it has a good sound to it.
Anyway, I think their C++ style guide isn't bad. As to "modern C++ style", let's wait for a few years, shall we? When the STL came out, auto_ptr and for_each(v.begin(), v.end(), FunctorFromHell()) were marketed as the way to go for a long while; then the usual suspects became a bit more silent. Now again, despite their being a smart for loop, for_each with a lambda (with its crazy capture lists and all) is supposedly considered at least as good as a for loop, and typically better. And you're supposed to use unique_ptr and what-not. Maybe a few years down the road things will look different again.
C++11/14 has a ton of weird shit you ought to keep in your mind on top of the weird shit in C++98/03 and many "enterprisey megacorps" seem to apply a restricted subset of C++ rather successfully.
It's hard to deny that there are some really useful new features in modern c++, though. Standardized multithreading primitives is a big one. Smaller ones like scoped enums are useful too. I'm a big fan of lambdas and the ability to use them for things that used to be very awkward to do without them, like std::bind and functors. In all, I think the modern additions are a net positive for sure.
I would agree with this. I am writing C++03 at work due to lame compilers I am forced to use on Windows, and I really miss little things like std::atomic.
Lambdas would mean I could bin all those structs with bool operator()() for find_if that I have to litter my code with.
"Boost.Atomic is a library that provides atomic data types and operations on these data types, as well as memory ordering constraints required for coordinating multiple threads through atomic variables. It implements the interface as defined by the C++11 standard, but makes this feature available for platforms lacking system/compiler support for this particular C++11 feature."
I don't deny that there are useful features in C++11/14 (though I find "modern C++" an irksome marketing trick by people peddling a disgusting programming language; it was called "modern" since at least the early 2000s and it was disgusting then.) Of course I'd rather iterate over std::map with a range for loop than use the C++98 way, and as to lambdas - while I find them way too awkward and limited compared to closures without the sharp edges (and with garbage collection), and a ton of "modern C++ code" went batshit insane in its extent of unnecessary reliance on lambdas, lambdas remain much more practical than the alternatives when doing things like some sort of a parallel_for. But then it's hard to find a language whose version X+1 adds nothing useful to version X.
Is C++14 a better language than C++98? It's hard to tell (I think it is - I think C++ is worse than C, but having gotten to C++98, perhaps it helps more than hurts to add all that other shit), but it's not an interesting question anyway because the upgrade is easy enough and it's worth it for most people for their specific reasons, so C++98 will die out.
But then it's not like Google aren't upgrading to the latest C++ standard, they're just doing it slowly AFAIK.
It's interesting to consistently find that people who hate C++ just really really hate it a lot, to the point of using profanity a lot of the time.
I don't see what the big deal is. C++ is probably the most gigantic and complex language I am likely to ever use... but you don't have to use the vast majority of it if you don't want to. If you want to stick with C-style coding, you pretty much can, if you don't want OO or STL or ++11/14 features -- nothing is forcing you to write those features into your code. But when you want access to them, there they are; and if suddenly you find yourself needing a particular library that only exists in C++ (as so many graphics and physics engines do), you can get access to that as well.
As soon as you need multithreading, or sorting of hash maps, or even basic string operations, knowing you can reach for them in pinch rather than find the C alternatives or build your own, seems like a big bonus to me (assuming a comparison to C).
True, but I think a lot of the encouragement is to move away from writing C++ like it is C because we really don't want to encourage people to write with dangerous approaches anymore (like you can if you write C++ like it is C).
I work with a few who write their C++ like it is C (pointers thrown around, no const-correctness, memcpy instead of copy constructors etc. etc.) and it is horrible to read. There is no indication of lifetimes of objects, nor any indication of ownership. No RAII, no exceptions, nothing. It is dangerous.
You are rewarded for writing C++ safely because you won't have to fix any bugs and you can offload all the work onto the compiler for checking types and safety for you.
Like you, though, I don't understand people who get red in the face about C++ and go into a profane rage. It is just a language after all.
Guilty. Of writing C++ as if it were C. Sometimes even with classes. Yes, I use memcpy instead of copy constructors. I find "modern C++" horrible to read. Indication of object lifetime? Ownership?
Um... why? If the development support does not include a garbage collector, I have to be very careful about these things. And, I am. Very careful indeed. I just use the approach I do in C, and assume NOTHING from C++. Honestly, I do NOT assume that the language system, compiler or run-time can do it properly. I have been bitten too many times.
I do presume a garbage collector at the very next level up.
Yes, you may find my code "horrible to read". And, yes, I have gone into profane rages over C++ code. It happens when the code bullies me -- with hopeless error messages for simple typos. When simple changes cause the application to no longer link. When simple changes cause massive explosions in object size. In fact C++ is so complex, it encourages cargo cult programming. Not just use of libraries, but enforced cargo-cult when USING the libraries. Because, raisons.
Not just a language -- hell-spawn. Sorry, "modern hell-spawn".
The problem is that if you were hit by a bus, the next guy maintaining the code has to know about where everything is cleaned up and has to know all of the secrets that you know and that you decided you knew better than the compiler in areas. This makes for unmaintainable code.
Ownership is very important because if you don't know who owns it, who will clean it up??
If you don't know how long an object will be around because you've got a pointer, it is now a dangling pointer and attempts to use it will cause chaos. So object lifetime is important too.
The error messages are usually quite helpful. clang's messages are more helpful for gcc, I have found.
C++ is complex, but you can still write safe code without using any of the complexities.
No, what I "hate" is the idea that C++ promotes "automatic" management of "ownership and lifetime" without a garbage collector.
Which works, except when it doesn't. Which results in stuff like reference counting in the codebase. Which, of course, changes on a whim.
Example: the CEF3 library (chromium embedded framework).
Reference counted objects. Which requires source code changes depending on the version of CEF 3 being used. Requires VERY deep analysis to determine what to change in the using code. Really -- because the objects participate in a multi-threaded library.
So, most code ends up as "cargo cult".
Since my code doesn't fit into the CEF3 object model, I do the contortions needed -- building another layer.
Reference counting is code smell that you don't have a clear ownership model.
Take a look a Rust for something more along the lines of what I'm talking about. There's been quite a few people who've re-written their C/C++ code to Rust only to find out that they didn't have a clear ownership model and found architectural improvements because of it.
RAII solves this. Allocate all you need in the constructor, tidy up in the destructor. So when the parent item gets cleaned up, the child items get cleaned up too (so lifetime is sorted for you, and ownership is done too as the parent that owns it is getting cleaned up). You will eventually only have one parent item but if you tidy everything up for that in its destructor, the lifetimes of everything is sorted.
If your acquisition of a resource fails or initialisation fails, throw an exception in the constructor.
C++11's threading support library in the STL should help your threaded problems.
If you need containers of items, put them into an STL container. Don't allocate them with new - write a move constructor for your item and it'll be moved. Or a copy constructor; follow the "rule of 5" if you have pointer-held items in your object (probably best not to), or "rule of 0" and let the compiler do it for you. std::move your items into the container.
Use const references if you want to let something else use it, ordinary references if you want them to modify it. Or if you must use naked pointers, put them in a unique_ptr so it gets cleaned up eventually (http://en.cppreference.com/w/cpp/memory/unique_ptr). Or shared_ptr in other areas (see Stroutrup 5.2.1 in C++ 4th edition).
If you need naked pointers, allocate them on the heap but wrap them in std::unique_ptr or have a vector of unique_ptr. If your objects are child items in a class hierarchy (e.g.. ChildClass : public BassClass) or the base class is an abstract class and you want to have a generic STL container (like std::vector<BaseClass* >) to store all base and child items in one neat place, you will have to use pointers (as you can't have std::vector<MyAbstractBaseClass> but you can have std::vector<MyAbstractBaseClass* >), you will need to take care in the containing object's destructor to clean up this container, eg. while(!vector.empty()) { delete vector.back(); vector.pop_back(); }
You shouldn't need reference counting. A read of Stroustrup's C++ Fourth Edition book and the "tour of C++" section toward the beginning will help you immensely.
> You are rewarded for writing C++ safely because you won't have to fix any bugs and you can offload all the work onto the compiler for checking types and safety for you.
But that's not the case. No shipping version of C++ is memory safe. In fact, this is exactly what the article is about.
Here's the thing, there is a nice way (well, more than one) to blend highly tuned low-level C code with clean, modern C++. I have to do it: I work on high performance algorithmic trading systems.
You want to keep raw pointers, etc. confined to private class members, initialize memory only within the constructor (if possible), and free memory at the destructor. The user/public API should not expose unsafe stuff. With all this, RAII will give you a nice expectation of lifetime, etc. Plus you're able to use something like tcmalloc.
I often find a difficult issue arises when you have a large data structure that you want to free before it goes out of scope (to reclaim some memory). In this case, adding a "clear" method that frees the internals is handy. The destructor will call clear() but you can call it at any time. No harm done if it's called twice.
The next thing is const-correctness: if you follow it then any C-like code underneath should be relatively safe.
Not using copy constructors is just awful. Memcpy has a time and a place: at the low level (eg; I use it for serializing to a buffer). If you're using memcpy for classes you're probably doing something wrong.
Exceptions are a place where you see a wide gamut of safety. Some people don't use them at all: which isn't great. Others add hundreds of lines of code by wrapping everything in a try/catch. Not sure where I stand here, honestly. There's a point where you're catching exceptions that just aren't going to happen...
He's not just any guy, but the guy that created a whole website, C++ FQA, dedicated to trying to explain how C++ is terrible. Or maybe he just shares the same name with him.
Anyway, it's difficult to have a reasonable discussion over a topic where said topic has religious dimensions for one of the involved parties.
That's me alright, but I think my comment in this instance didn't warrant the claim about "religious dimensions." Having a firm opinion and using (OMG) the word "shit" when stating it does not mean it's a religion, and I'm very secularly using much of C++11 as I said, and I was among the people pushing for the upgrade where I work.
Maybe it was too strong of a word, but... I have the impression that C++ topics are more emotional than they need to be. In the end it's just a programming language.
I tried being a C++ programmer back along time ago (1992-1995) and I failed. The biggest mistake was trying to learn the whole language and apply the whole language to the problem. The utility of the language doesn't compensate for the cognitive burden. I would have been better served by using ML (with some C), or Rexx (with some C). Good C++ has an abstract top level that isn't much different than a scripting language in expressivity. Now something like a Lua +Rust can be more expressive, correct and fast.
I find this a lot more encouraging to read. Most gamedevs I see talking about C++ mention to avoid boost & templating, how STL is bad, etc. How they have to write their own more efficient version of vectorm or their own allocator. There are things I like things about C. I find malloc & free fairly understandable, where as a lot of the newer pointer arithmetic in C++11 can be a bit overwhelming.
That said as someone who hasnt done a lot of C or C++, i'd like to think learning the new way is best, otherwise it's hard to know where to start.
The thing with game devs is that they only change tooling when forced to do.
In the 80's Assembly was the only way to go, if you were targeting consoles and home systems.
Higher level languages were the managed languages of the day.
When the SDK started to push for C, like on first PS, they were forced to move. Still the code was pretty much inline Assembly wrapped in C, until they were confident to write more and more in C.
Similarly when around PS 3 time, the SDKs were moving into C++, the majority took advantage of the C compatibility and used only as much C++ as required by the SDKs.
Only recently some started to do the transition to embrace C++.
Likewise if in the future language X becomes the only way to target games for platform Y, and they a market that they need to be in, they will eventually adopt it. Of course writting big chunks in C++ until they can be convinced it is ok to just use X.
> C++ is probably the most gigantic and complex language I am likely to ever use... but you don't have to use the vast majority of it if you don't want to. If you want to stick with C-style coding, you pretty much can, if you don't want OO or STL or ++11/14 features -- nothing is forcing you to write those features into your code. But when you want access to them, there they are
The features are dependent on each other though. The flipside of my example elsewhere in the thread: if you want to use templates to eliminate repeated code, you more-or-less need to make any classes you want to template over use RAII (or else do very error-prone partial template specialization to ensure your things are initialized and deininitialized correctly). So then you have to use exceptions, because if you're doing complex logic in your constructors then you need a way to report failure. So then you have to make your code exception-safe, which means making everything else RAII.
Well, if you are going to choose just one feature to reach for in C++, template metaprogramming is probably not going to be it. Even a lot of devs who are very comfortable with other C++ tools and idioms don't write, if any, template code. I wouldn't think that someone would avoid all the other C++ features, but then willingly go down the road of writing templates.
> many "enterprisey megacorps" seem to apply a restricted subset of C++ rather successfully
Your definition of 'successfully' is vastly different from the Megacorp definition of 'successfully'. I interview C++ programmers _all the time_, and let me tell you: practically the only reason those restricted subsets of C++ exist is to allow bad programmers to work at Megacorp.
There are a lot of really unqualified C++ programmers in the world, and by far the biggest problem faced by Megacorp is employee churn, not code quality and not product deadlines.
The use of 'C++ subsets' solves the employee churn problem at the cost of code quality.
The functional approach is great in a language with typeclasses or modules. But it's not a good fit for RAII - how would a C++ constructor report an error using Result?
IMO constructors should be setting sane defaults and nothing more. If you need to do work that could fail put that into an "init()" function with a return type that can signal an error.
I worked for quite a while on platforms that didn't have exceptions and this was the standard pattern for keeping RAII without needing them. If you wrap the return value correctly(like with Result<T,E>) you can also guarantee that your clients need to deal with the error case(or fail to handle it with unwrap()) as opposed to getting caught by an exception that wasn't declared or documented.
> IMO constructors should be setting sane defaults and nothing more. If you need to do work that could fail put that into an "init()" function with a return type that can signal an error.
That allows you to construct the object in an invalid state. The whole point of RAII is to avoid that. The acronym literally means not having a separate init() function.
Sorry, I should have clarified that the init function is usually static so you can invoke with it returning an object + possible error code if you never want an object in an invalid state.
FWIW some quick googling shows that having a constructor acquire a resource is not a requirement(https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Resource_A...). The important part about RAII is that the automatic releasing of resources when out of scope(through a no-throw destructor).
You can solve this problem the Rust way too: don't use a constructor. Create a static `new` method that returns the object to you, with optional error flags.
This is still RAII, you're just not using a constructor to acquire. Rust doesn't have constructors and is still RAII.
While I also don't like the exception handlers in constructors syntax, not putting any work in the constructor doesn't mean that data members or base classes cannot fail to initialize.
In Rust, we don't have special constructors, just functions, and to construct a struct, all of its members must be valid, and assigned at the same time. So a Rust-style "constructor" in this vein would return a Result<MyStruct, Error>, basically.
or similar, you get the point. At the moment I'm using a class like llvm's ErroOr<T> [1] and I'm loving it so far. The aformentioned function would be written as
ErrorOr<File> OpenFile( const std::string& path )
and contain an std::error_code if opening failed, or else a File instance.
That's actually std::optional, minus the nothing/just notation? Indeed more people should use things like this. Reminds me of a similar concept I also have wrappers for: instead of manually having to compare the results of std::find etc against range.end() all over the place, the result is basically in a small container which has both the iterator returned and the end of the range. So I write
const auto result = alg::find( range, pred );
if( result )
doStuff( *result ); //*result is reference to found element
instead of typcial
const auto end = range.cend();
const auto result = std::find( range.cbegin(), end, pred );
if( result != end )
doStuff( *result );
Not that much of a gain, but once used to it I could never go back. And I realize now this could actually just be rewritten using your maybe class or std::optional.
Yeah it's pretty much std::optional but unfortunately that's in the library fundamentals TS and still in the experimental namespace last I checked. But I have to say that I prefer the name "maybe", it does convey the notion that you have to check whether it's valid much better than "optional" in my opinion.
I don't think monadic notation here is the panacea that it is sold as. The harder part would still be modifying your function's return type to thread the result up the stack.
In any case, .unwrap() is perfectly fine in applications, it's only in libraries that they're discouraged.
Well, your definition of bad C++ devs may differ from mine or Google's. Or it might not, but for all I know, you might require knowledge of arcana that I don't care about, even when I happen to know it.
To me success includes being able to find (not frequently replace - I like working with the same people for many years - but find new ones in order to grow) developers that can contribute to the code base, and some of the best ones I know can't be bothered to learn much of C++. Go figure. Maybe you and I legitimately disagree because we work on different things, and maybe one of us is wrong.
When I read the Google coding standard I realized I never want to work there. Microsoft on the other hand seems to get cpp. Maybe because herb Sutter has managed to change the culture from the inside. Anyone remember how awful vc6 was back in the day? MS has come a long way.
Edit: It seems the standards have changed. It doesn't seem so bad now. They cant use exceptions but they can use some of boost cpp11 and templates. So maybe working at google ain't that bad.
I was recently reading the docs for a physics engine in c++ and they said that its initial implementation used std::vector in many places, but they instead implemented their own in later versions to accommodate maximum "portability and compatibility." I wonder what that means? Since the STL is included in C++ compilers and vector is always going to be there, why would they need to roll their own to make the engine either more portable or more compatible?
>use of templates is frowned upon
I wonder if this means the writing of templates, or using existing templates. If the latter, maybe like the STL their primary concern is compile times?
std::vector spec leaves quite a bit of performance-critical details unspecified. I imagine the allocator behaviour differs enough between implementations that they implemented their own to keep performance in different platforms more predictable. The performance requirements for a physics engine can be quite different from the general case that the std::vector spec is aiming for.
I imagine templates are frowned upon for the same reason -- too many moving parts. Otherwise they're perfectly fine to use in projects that have goals other than portably predictable performance.
Yup, the spec is also really vague on if clear() actually releases memory. AFAIK the only "correct" way to actually clear a vector is "vec = std::vector<T>()" with the STL.
Actually you'd have to try harder: even this might leave the current capacity unchanged. The blessed idiom is vec.swap(std::vector<T>());
c++11 added shrink_to_fit which is a non binding request to reduce capacity.
Use of templates isn't frowned upon. TMP is frowned upon because usually the performance gains don't justify the loss of readability. It has been said many times in many ways: don't write code for yourself, write it for others.
Basically, Google has an enormous code base. Stylistic decisions should have semantic weight, but since everybody has their own style, it becomes challenging to understand the reasoning behind certain choices. When you have a standard, it means that you can read into some of the stylistic decisions sprinkled throughout the code.
Take for example, the use of pointer arguments for passing the addresses of results. The justification is that using a pointer is explicit and unambiguous. Non-const references have value syntax but pointer semantics, which can be confusing. Yes, this is contrary to some of the core values behind C++ but Google felt that the improved readability was worth that dissonance.
I would have thought that non-const references was better? It is clear that a non-const reference can be modified. If I just wanted to let them read a value, a const reference would be clear.
Do Google enforce const pointers for arguments that define where to store results? If not, you could delete the pointer they pass you. A pointer does not always indicate who owns the data clearly.
Far better would be to return the values from the function (or std::move them out if you must) instead of using a parameter as a "dump your results here" flag.
Stroustrup encourages this (see 5.3.3 of C++ 4th edition where he says "I don’t consider returning results through arguments particularly elegant"). IMHO, it would be better to return a std::pair<bool, std::vector<myWonderfulType>> where bool indicates the success/failure of the function, with pair.second item being the results.
It probably means that it was written before we had a complete and correct implementation of the STL on all major compilers.
Google around for Visual Studio 6 STL, and take into account the fact that most organizations don't upgrade right away, and you're looking at at least 2005 before you could rely on the STL cross-platform. Maybe much later.
They avoid using exceptions, which means avoiding doing work in constructors, which compromises your ability to use RAII, which in turn makes template-style programming much less effective.
On one hand it's good things like this are written, on the other hand it's a bit of a shame it still gets written. We should have moved beyond that by now, but adoption of modern C++ goes so slow.. Not exactly sure what the reasons are - sometimes it just seems like stubborness to me - I hear 'legacy' often, and yes if your codebase doesn't even compile with the latest compilers than it's a valid argument, but otherwise: it's not exactly hard to interface modern C++ with C-with-classes. Anyway I've worked in multiple teams and after the initial stage of getting overit and doing the right thing none of useever writes code like the 'bad' examples anymore since smart pointers and collections were made commonly available years ago and they solve most of the problems in this pdf.
Safe by convention is a ridiculous notion. I think (and I know this isn't a popular opinion here, but I'll say it anyway) that C and C++ are dying horses; these 'modern' versions and 'best practices' are not adequate solutions, evidenced partly by the fact that we can't even agree on what they should be. The next decade is going to see the rise of systems programming languages with static safety guarantees. Personally, I think Rust has a lot of potential, but we will see.
C++ has had to be backwards compatible for the billions of lines of code written in it. It has managed to introduce new features and safety quite well in view of this. Perhaps the rise of systems programming languages with static safety guarantees is C++ already? You don't have to write it with "old school" methodology just because you can write it with old-school dangerous approaches.
I think that C++11 and beyond will do quite well. Sadly here in the UK at least, it has lost vast swathes of the job market to C# (likely because of C++11 taking so long to appear) but there are still a few C++ jobs (for maintaining crusty MFC codebases) or a few that specifically mention C++11.
I myself think C++11 is a great language and daily write C++03 at work through gritted teeth.
As you say though, we'll see what comes about. It is only a language after all.
EDIT: And I will add that C++ does have the benefit of having loads of libraries available to it, including GUI libraries and bindings which makes development a breeze.
> C++ has had to be backwards compatible for the billions of lines of code written in it. It has managed to introduce new features and safety quite well in view of this. Perhaps the rise of systems programming languages with static safety guarantees is C++ already? You don't have to write it with "old school" methodology just because you can write it with old-school dangerous approaches.
The very definition of static safety guarantees is that the compiler enforces them, and that's what makes them useful. C++ has great safety guarantees if written correctly - but if you can reliably write code correctly, you don't need safety guarantees in the first place!
> EDIT: And I will add that C++ does have the benefit of having loads of libraries available to it, including GUI libraries and bindings which makes development a breeze.
There are many libraries, but the important ones have mature bindings from other languages, or at least tools for generating them (e.g. Swig). That said, using them exposes you to the safety risks of C++, so you're often better off rewriting them (e.g. I worked on a Java project that initially used ffmpeg to handle some transcoding tasks - but as a result had to deal with bugs in ffmpeg that took down the whole JVM for particular inputs).
The safety guarantees being enforced by the compiler does mean that you can (and really should) offload all the checking onto the compiler and not think you know better by casting everywhere etc.
Good point though!
Sad about your JVM - did you not monitor the state of the process you spawned to handle transcoding? Did you end up fixing the bugs in ffmpeg?
> The safety guarantees being enforced by the compiler does mean that you can (and really should) offload all the checking onto the compiler and not think you know better by casting everywhere etc.
When the set of actions that might violate the guarantees is small, circumscribed and easy to spot in code review and/or with automated tools this kind of approach works. But in C++ it feels more like today's best practices are tomorrow's unsafe constructs - it's like perl's "there is more than one way to do it", but additionally all but one of the ways are unsafe.
> did you not monitor the state of the process you spawned to handle transcoding?
That was my point - we originally tried to use JNI so that we could use libffmpeg as a library (and e.g. handle parameters and input/output as structured data rather than having to flatten them to a string command line and temporary file or pipe). But unfortunately if you do that you give up the nice guarantees of the JVM platform - e.g. a Java-native library will only ever affect its own thread, but a bug in a JNI library can take down the whole JVM. (We did ultimately move to a separate-process-for-transcoding model for exactly this reason)
> Did you end up fixing the bugs in ffmpeg?
We fixed specific bugs as we found them. It didn't feel like we were making a dent in the number there were though.
It would be neat though if GCC and Clang supported some compiler flag like Wsafe to warning on all raw pointer manipulation and raw memory addressing. Would make maintaining a C++ project and avoiding unsafe habits easier.
I am actually re-writing Rust's documentation on ownership right now. I'm interested to compare and contrast my stuff with this. (First draft of the first chapter is https://github.com/rust-lang/book/pull/58, I have PRs up for the other two, but they're not quite as polished yet)
I wanted to write a similar comment, but then noticed that the paper claims 100% safety by using an analysis tool, not just by convention.
Agreed about the potential of Rust, it sidesteps most of these problems. Though it might introduce other problems of its own, only time will tell. I expect that Rust's future sore spots will include destructor leaks, the reliance on macros, and the tremendous complexity of traits. (Though of course I don't claim to have better solutions to those.)
Rust doesn't leak destructors any more than C++ (including the version of C++ presented in the OP). The only way to leak a destructor accidentally is to create a cycle (which is quite difficult to do, essentially requiring you to stuff a RefCell inside an Rc) or to have some data owned by a thread that's eternally stalled.
> reliance on macros
Individual macros are generally regarded as convenient workarounds rather than ideal long-term solutions, and anywhere that you see pervasive macro use tends to be ripe for an RFC for a dedicated feature to subsume it.
> the tremendous complexity of traits
No denying that traits are Rust's "deepest" feature, but that's because of a general philosophy to leverage traits to provide newly desired bits of functionality rather than introducing all-new language concepts. Removing functionality from traits wouldn't result in making the language less complex; that functionality would just end up going elsewhere.
> Removing functionality from traits wouldn't result in making the language less complex; that functionality would just end up going elsewhere.
Yes, the Waterbed Theory of Complexity[1]. It can be useful to funnel complexity into specific areas where it's at least expected, and there may exist better mechanisms for dealing with it.
I think the broader context of "the leakpocalypse" involved a host of difficult decisions, all of them suboptimal in their own ways. Of all the options on the table, it's my opinion that Rust took the most pragmatic one, especially given that the proposals that sought to ensure "never-leak" behavior all felt like they had undiscovered holes of their own, leading to a lot of ceremony for no actual benefit.
> I'm referring specifically to try!
I'm also referring to `try!`. :) It's a personal goal of mine this year to advocate a language-level replacement (and enhancement) for improved error-handling ergonomics.
Note that whilst the leakpocalypse made it safe to leak, it's not a common case. Leaking safely involves an explicit call to mem::forget or extremely convoluted code involving Rc cycles. Its pretty much going to happen in languages which provide refcounted pointers.
None of the proposed solutions mitigated this either. You will always be able to leak cycles. The difference was that some proposals made it possible to mark types as non-leakable, in which case they can't be placed in things which may potentially leak.
The fact that things will leak was never under question there. The decision to be made was on how safety should interact with it -- should there be a way for a type to rely on its destructor being run for the purposes of safety? The decision was that there shouldn't, and we ended up fixing two APIs which relied on destructors being run for safety, instead of adding a major language change.
For what it's worth, I feel that marking types is also a mistake, and the whole machinery of OIBITs exists due to some unfortunate decisions made early on.
Maybe the cleanest solution to the leakpocalypse would be to mark RefCell.borrow_mut as unsafe, because it's problematic in two separate ways (it can both panic and create cycles). That way the safe fragment of Rust would guarantee that memory leaks can't happen and destructors always run. That would fit with the spirit of Rust, which strongly prefers 100% guarantees to 99% guarantees. Though I guess that idea was judged too inconvenient for users, and I don't claim to know what users want.
Concurrency/parallelism is sometimes described as "abstraction piercing", i.e. you can't tell if something is safe for use in a concurrent code by just looking at its interface, the internals of the type and the implementation details of its functionality matter.
One way to handle this is to have each type explicitly opt-in to saying they're concurrent-safe, but this is fairly annoying, as it requires a lot of boilerplate. Boilerplate that is mostly pointless in Rust, because its rules mean most types people define will be safe, and most types that aren't safe will be built out of existing types that aren't safe. OIBITs are proxies for "safe", and are designed to strict a balance, by basically following that rule (types made out of types implementing an OIBIT automatically impl that OIBIT too).
Moving on to your specific suggestion: RefCell isn't the only way to not run destructors, for instance, one can create ownership cycles with Mutex and RWLock in a similar way, and, more broadly, threads that dead lock (or live lock) will leak their resources, as would calling std::process::exit. There's also weirder/wilder things like the following: suppose Queue<T> is a multiple-producer multiple-consumer queue type, where any instance can act as both a producer and a consumer (typical for such types), then the following code creates an ownership cycle within the queue, and so won't run the destructor of X:
struct Msg {
x: X,
y: Queue<Msg>
}
let q: Queue<Msg> = ...;
let q2 = q.clone(); // another handle to the same queue
q.send(Msg { x: ..., y: q2 });
drop(q);
Now, sure you could mark all these things unsafe, but I think that starts to make things very inconvenient, and, somewhat, weaken what `unsafe` means (it normalise its use since those things are typically totally fine). (Also, those examples are almost surely not exhaustive.)
Interesting! Do you mind if I ask you a few newbie questions?
1) After some soul searching, it seems like the really important invariant to me is that all alive objects should be reachable from the stack(s) at any given moment. So I'm kind of okay with failing to run destructors due to deadlock or process exit, but not okay with leaking a cycle. Is that a sensible distinction, or is the situation even more subtle?
2) How do I make a cycle with Mutex and RWLock?
3) I don't completely understand the queue example, what exactly is the magic that allows self-reference?
> 1) After some soul searching, it seems like the really important invariant to me is that all alive objects should be reachable from the stack(s) at any given moment. So I'm kind of okay with failing to run destructors due to deadlock or process exit, but not okay with leaking a cycle. Is that a sensible distinction, or is the situation even more subtle?
I think that's a reasonable distinction, but, at least for deadlocks, I'm not sure it is enough to have all the guarantees one might want (particularly around multithreading/scoping). Then again, it might be.
> 2) How do I make a cycle with Mutex and RWLock?
The same as a RefCell, just call `.lock().unwrap()` and `.write().unwrap()` instead of `.borrow_mut()`. These two types are essentially the same as a RefCell, just with more synchronisation, and fewer panics. They can block and wait for another thread to finish, at the risk of deadlocks of course, while a RefCell is restricted to a single thread, so blocking would just dead lock a thread with itself, hence a panic resolves the situation better. In fact, other than this blocking/panicking distinction, a RwLock and RefCell are almost identical: .read == .borrow and .write == .borrow_mut.
> 3) I don't completely understand the queue example, what exactly is the magic that allows self-reference?
This is also essentially the same as the core RefCell/Mutex example, replacing .borrow_mut()/.lock() with .send. An mpmc Queue<T> will be isomorphic to Arc<Mutex<VecDeque<T>>> (although unlikely to be implemented as such in practice). The key is that they will reference-count the book-keeping that needs to be shared among all handles (like the start/tail of the queue), which allows creating a reference cycle.
Actually, I'm increasingly convinced we made the right choice there. In some other areas (exception safety) we're discovering that adding new traits to the language that infect everything has dramatically negative effects on usability.
I think of things like the leakpocalypse the same way I think of security advisories like the Go one from a few days ago. Yeah, it looks bad on paper, but the odds of being affected by it in practice are so incredibly low that it's not worth worrying about.
There is this cppcon presentation by Stroustrup where he is talking about the GSL annotations https://m.youtube.com/watch?v=1OEu9C51K2A but too bad that it all depends on this unreleased big black box proprietary code analysis tool.
It describes a (sketch of) a statically provable memory safe dialect of c++. A more detailed description is in one of the referenced papers by Herb Sutter.
Unfortunately the promised static checker is, as far as I know, yet to be released, and we will have to see how well it will work in practice and whether it is feasible to piecemeal add the safety annotations to an existing codebase.
In the long run, I agree with you, where new projects are concerned. At the same time, we have these mountains of C/C++ around that aren't going to go anywhere anytime too soon.
Remember the Y2K problem and how those programmers in the 60s and 70s thought that by the year 2000, surely their code would have been either fixed or rewritten.
At most, C and C++ are "dying" in the same way COBOL is - for new projects, other languages might prevail, but there are still mountains of legacy code written in it that nobody dares touch...
1. Are still whitepapers, the tool has not been released.
2. Does not protect you from everything Rust does.
3. Incrementally improves on C++, rather than being a whole new language.
These points have a number of implications. On the first point, all Rust code that exists today is already written in this style. As the tool still isn't released yet, essentially none (the tool is built based on some Microsoft work that they've been using internally, but other than that) of existing C++ code is following this. This means that even if _you_ use the tool, it doesn't mean your dependencies use the tool. Furthermore, how many people lament that they are still writing C++98 and not C++11, let alone C++14? Once the tool exists, how much code will actually use it?
On #2, Herb has said in the past that "data races are off the table" and "dangling pointers in some circumstances are important." I _think_ that in the latter case, he means something akin to what we call "non-lexical borrows" in Rust, so it's not a big deal, but data races are. Rust's version of this concept also provides a foundation for compile-time concurrency errors, and this paper states "Our rule set for concurrency is not yet fully developed." We'll see what they come up with.
On #3, this is both an advantage and a disadvantage, and ties into #1. If you have a large C++ codebase, it's not like you're just going to instantly port everything to Rust. Even Mozilla, with Firefox, is taking an incremental approach. This means this tool could have a lot of value for organizations which are using C++ today, in a way that Rust can't. However, as mentioned elsewhere in this thread, I'm not sure how much C++ code will actually pass the tool; rules like this can really change your design, so we'll see.
EDIT: One more difference, we use "memory safety" in different ways. They describe their definition on the end of 3.0:
> We say that a program is memory safe if every allocated object
> is deallocated (once only) and no access is done through a pointer
> (or reference, iterator, or other non-owning indirection) to an
> object that has been deleted or gone out of scope (and thus
> technically isn’t an object anymore – just a bag of bits).
We usually describe memory safety in terms of "no data races". Theirs assumes single threading (see above about how concurrency is under-developed here) and we take it into context. Terminology is hard!
Finally, I'm glad to see this project develop. I'm glad that the C++ committee is taking safety even more seriously than they have in the past. We all want better software with less bugs.
All valid points. For point 1, we should assume the tool is near release. Once it is, that point evaporates.
> Once the tool exists, how much code will actually use it?
Any number of the 10M+ C++ developers and probably a large number of Open Source projects. I remember something like 20% of C++ (projects|programmers) are using C++1/4. If half of those use the tool, that is still 1M programmers using a much safer system. Not the same as Rust, but better than the old way. In the near term, the total quantity of safer code will come from C++ adopting better static analysis than from the adoption of Rust, even if Rust is the better way.
The biggest impediment to Rust adoption with C++ programmers is the non-ability to incrementally include Rust in a project. But this is true of C++ in general.
Rust has the advantage of mind share, witnessing the mistakes of C++ and the ever increasing torrent of new programmers who will choose Rust over C and C++. Every problem that a C++ programmer has, a Rust programmer has. A Rust programmer just solves it slightly differently.
Yeah. I _thought_ that I remembered the tool was supposed to come out two months ago, but I don't have any citation for that. I want to play with it!
> the non-ability to incrementally include Rust in a project.
Well, it just depends on how you do it. The approach Firefox is taking is to peel off chunks, re-write them in Rust, and then integrate that way. That said, better integration would be nice, and some people have some ideas about it.
For the isocpp guidelines to be truly safe on a C++ module similar work is required, really. Without the C-interface boilerplate, but plenty of old-C++ idioms will probably be incompatible with isocpp and you need to carefully work at abstraction boundaries.
For something of this magnitude I wouldn't consider the project late until mid summer.
Stroustrup's keynote at CppCon is a great overview of the semantics they want to support and the errors that the tooling with will prevent. https://www.youtube.com/watch?v=1OEu9C51K2A
- Simpler lifetime system: Both a pro and a con, because some functions will not be expressible using "c++ borrowing"
- No `Alias XOR mutability`, which in rust protects from memory issues and things like iterator invalidation. Not sure if isocpp addresses the segfault that can be caused by
vector<int> v;
// fill it up with elements
int *ptr = v[3];
v.push_back(0); // may reallocate
cout<<*ptr; // may segfault
its been a while since I looked at the guidelines and I'm not sure of either of these points, take them with a grain of salt.
I'm amazed at how well-written and thoughtful these papers are given the product is a language I oppose. Well, it does have some good ideas. :)
However, what made my jaw drop was section 3.2: "a dynamic model" for making the system safe and easily implemented in hardware. The section said Stroustrup deciced not to publish it because it was impractical. One of few times see his name and go "what an idiot..." What wasn't practical in general case or that time period doesn't mean it won't be in future in other circumstances. Good ideas, esp simple solutions, have a way of coming back to us.
Specifically, efforts like Cambridge's CHERI processor are looking at minimal modifications to CPU's to greatly increase assurance w/ legacy compatibility. This concept should've been published as there's use in embedded and appliance field where a lot uses subsets of C++. He should've published it. I'm saving the paper for my collection just in case that idea comes in handy.
> The section said Stroustrup deciced not to publish it because it was impractical. One of few times see his name and go "what an idiot..."
He's right though. Such a system would be slower than a good tracing GC, which completely eliminates these problems and is much easier for the programmer to use.
In "hardware" as a minimal processor extension as I said? Pointer checks, etc take almost nothing in HW. Garbage collection schemes have a bigger hit and more complex integration. Hence, having his published report for details to add to collection of lightweight schemes out there for C++ or even another language that might benefit later.
That sort of thing happens more than you know in CompSci. Example: One method for Oberon and Java appliances I pulled right out of an old Scheme CPU paper that was also impractical then. Also found a correct-by-construction HW method in same one. ;)
I should've clarified that my statement assumes his model works as advertised, then notes that the pointer checks are efficient in hardware. I couldn't be sure if he was detecting or preventing use-after-free with what would be in compiler and what in runtime. Here's the things I was looking at:
"supports the general thesis that garbage collection is neither necessary nor sufficient for quality software. This paper describes the techniques used to eliminate dangling pointers and to ensure resource safety."
"Every object on the free store (heap, dynamic store) must have exactly one owner [pointer/reference]"
"set the ownership bit when a value returned from new is assigned"
"use delete whenever a pointer with the ownership bit is set goes out of scope"
Sounds similar to what I read in Rust guide, esp one-per-location and scope part. Those and other rules appeared to make it easy for checks to prevent or catch use-after-free. If so, then my keep it around for hardware suggestion could lead to acceleration work. If I misread the paper or goals of ownership model, then my suggestion would've only led to accelerating... whatever safety it was trying to enforce.
Did that clear it up for you? Or did you spot where I went wrong? Could go either way given it was a quick read for me.
Here's my biggest question posed yet again, because the paper does not provide an answer to it: The analysis requires that all mutable references be statically proven to not alias on function entry. So how do you call a function foo(T& a, T& b) as foo(x, y) where x and y are instances of shared_ptr<T>? (The problem persists even if a and b are of different types, because a might own b or vice versa.)
This is not a contrived example. It is precisely what kept hitting us in Rust with things like @mut and we took years to figure out the best solution (RefCell/Mutex). It comes up all the time because you can't get away from RC--large programs use reference counting a lot, and even if they don't one of the major use cases of systems languages is to interoperate with GC'd or RC'd systems (dynamic languages, COM, Core Foundation) where you don't statically know anything about aliasing. Forbidding these calls statically bifurcates functions into "those callable on shared_ptr" and "those callable on owned or stack-local objects", which is unusable in practice. The only solution we found is dynamic enforcement of the aliasing rules, scoped to reference counted pointers. This indeed works, but it requires overhead. You want this overhead to be opt-in in a low-level systems language, but existing code was not written to opt into this. So C++ will need to backwards incompatibly introduce RefCell/Mutex to support this.
The bottom line is that I see a dilemma. Either C++ makes it impossible to use shared_ptr with mutable references, severely damaging usability, or it breaks backwards compatibility by making shared_ptr immutable and requiring dynamic enforcement of the borrow rules for mutable shared_ptr. Either option has huge consequences for existing C++ code.
111 comments
[ 351 ms ] story [ 3726 ms ] threadOr code guidelines like the ones from Google, where one is even refrained from using Modern C++ best practices.
It's at odds with almost all of it. Google's standards are C-with-classes based on compiler bugs circa 1999.
Despite their (misleading) marketing, Google is still just another enterprisey megacorp employing 30000 blub programmers. Expect the usual 'enterprise' failures.
What do you mean by this?
Ouch! That must have hurt 'em. Of course the original meaning of "Blub" is "anything which isn't Lisp", especially C++ (as well as say Java.) But what the heck, it has a good sound to it.
Anyway, I think their C++ style guide isn't bad. As to "modern C++ style", let's wait for a few years, shall we? When the STL came out, auto_ptr and for_each(v.begin(), v.end(), FunctorFromHell()) were marketed as the way to go for a long while; then the usual suspects became a bit more silent. Now again, despite their being a smart for loop, for_each with a lambda (with its crazy capture lists and all) is supposedly considered at least as good as a for loop, and typically better. And you're supposed to use unique_ptr and what-not. Maybe a few years down the road things will look different again.
C++11/14 has a ton of weird shit you ought to keep in your mind on top of the weird shit in C++98/03 and many "enterprisey megacorps" seem to apply a restricted subset of C++ rather successfully.
Lambdas would mean I could bin all those structs with bool operator()() for find_if that I have to litter my code with.
"Boost.Atomic is a library that provides atomic data types and operations on these data types, as well as memory ordering constraints required for coordinating multiple threads through atomic variables. It implements the interface as defined by the C++11 standard, but makes this feature available for platforms lacking system/compiler support for this particular C++11 feature."
Is C++14 a better language than C++98? It's hard to tell (I think it is - I think C++ is worse than C, but having gotten to C++98, perhaps it helps more than hurts to add all that other shit), but it's not an interesting question anyway because the upgrade is easy enough and it's worth it for most people for their specific reasons, so C++98 will die out.
But then it's not like Google aren't upgrading to the latest C++ standard, they're just doing it slowly AFAIK.
I don't see what the big deal is. C++ is probably the most gigantic and complex language I am likely to ever use... but you don't have to use the vast majority of it if you don't want to. If you want to stick with C-style coding, you pretty much can, if you don't want OO or STL or ++11/14 features -- nothing is forcing you to write those features into your code. But when you want access to them, there they are; and if suddenly you find yourself needing a particular library that only exists in C++ (as so many graphics and physics engines do), you can get access to that as well.
As soon as you need multithreading, or sorting of hash maps, or even basic string operations, knowing you can reach for them in pinch rather than find the C alternatives or build your own, seems like a big bonus to me (assuming a comparison to C).
I work with a few who write their C++ like it is C (pointers thrown around, no const-correctness, memcpy instead of copy constructors etc. etc.) and it is horrible to read. There is no indication of lifetimes of objects, nor any indication of ownership. No RAII, no exceptions, nothing. It is dangerous.
You are rewarded for writing C++ safely because you won't have to fix any bugs and you can offload all the work onto the compiler for checking types and safety for you.
Like you, though, I don't understand people who get red in the face about C++ and go into a profane rage. It is just a language after all.
Um... why? If the development support does not include a garbage collector, I have to be very careful about these things. And, I am. Very careful indeed. I just use the approach I do in C, and assume NOTHING from C++. Honestly, I do NOT assume that the language system, compiler or run-time can do it properly. I have been bitten too many times.
I do presume a garbage collector at the very next level up.
Yes, you may find my code "horrible to read". And, yes, I have gone into profane rages over C++ code. It happens when the code bullies me -- with hopeless error messages for simple typos. When simple changes cause the application to no longer link. When simple changes cause massive explosions in object size. In fact C++ is so complex, it encourages cargo cult programming. Not just use of libraries, but enforced cargo-cult when USING the libraries. Because, raisons.
Not just a language -- hell-spawn. Sorry, "modern hell-spawn".
Ownership is very important because if you don't know who owns it, who will clean it up??
If you don't know how long an object will be around because you've got a pointer, it is now a dangling pointer and attempts to use it will cause chaos. So object lifetime is important too.
The error messages are usually quite helpful. clang's messages are more helpful for gcc, I have found.
C++ is complex, but you can still write safe code without using any of the complexities.
Which works, except when it doesn't. Which results in stuff like reference counting in the codebase. Which, of course, changes on a whim.
Example: the CEF3 library (chromium embedded framework).
Reference counted objects. Which requires source code changes depending on the version of CEF 3 being used. Requires VERY deep analysis to determine what to change in the using code. Really -- because the objects participate in a multi-threaded library.
So, most code ends up as "cargo cult".
Since my code doesn't fit into the CEF3 object model, I do the contortions needed -- building another layer.
Reference counting is code smell that you don't have a clear ownership model.
Take a look a Rust for something more along the lines of what I'm talking about. There's been quite a few people who've re-written their C/C++ code to Rust only to find out that they didn't have a clear ownership model and found architectural improvements because of it.
If your acquisition of a resource fails or initialisation fails, throw an exception in the constructor.
C++11's threading support library in the STL should help your threaded problems.
If you need containers of items, put them into an STL container. Don't allocate them with new - write a move constructor for your item and it'll be moved. Or a copy constructor; follow the "rule of 5" if you have pointer-held items in your object (probably best not to), or "rule of 0" and let the compiler do it for you. std::move your items into the container.
Use const references if you want to let something else use it, ordinary references if you want them to modify it. Or if you must use naked pointers, put them in a unique_ptr so it gets cleaned up eventually (http://en.cppreference.com/w/cpp/memory/unique_ptr). Or shared_ptr in other areas (see Stroutrup 5.2.1 in C++ 4th edition).
If you need naked pointers, allocate them on the heap but wrap them in std::unique_ptr or have a vector of unique_ptr. If your objects are child items in a class hierarchy (e.g.. ChildClass : public BassClass) or the base class is an abstract class and you want to have a generic STL container (like std::vector<BaseClass* >) to store all base and child items in one neat place, you will have to use pointers (as you can't have std::vector<MyAbstractBaseClass> but you can have std::vector<MyAbstractBaseClass* >), you will need to take care in the containing object's destructor to clean up this container, eg. while(!vector.empty()) { delete vector.back(); vector.pop_back(); }
You shouldn't need reference counting. A read of Stroustrup's C++ Fourth Edition book and the "tour of C++" section toward the beginning will help you immensely.
From what I know, having the chance to ever find a job on my area where one can use proper C++ is very thin.
But that's not the case. No shipping version of C++ is memory safe. In fact, this is exactly what the article is about.
You want to keep raw pointers, etc. confined to private class members, initialize memory only within the constructor (if possible), and free memory at the destructor. The user/public API should not expose unsafe stuff. With all this, RAII will give you a nice expectation of lifetime, etc. Plus you're able to use something like tcmalloc.
I often find a difficult issue arises when you have a large data structure that you want to free before it goes out of scope (to reclaim some memory). In this case, adding a "clear" method that frees the internals is handy. The destructor will call clear() but you can call it at any time. No harm done if it's called twice.
The next thing is const-correctness: if you follow it then any C-like code underneath should be relatively safe.
Not using copy constructors is just awful. Memcpy has a time and a place: at the low level (eg; I use it for serializing to a buffer). If you're using memcpy for classes you're probably doing something wrong.
Exceptions are a place where you see a wide gamut of safety. Some people don't use them at all: which isn't great. Others add hundreds of lines of code by wrapping everything in a try/catch. Not sure where I stand here, honestly. There's a point where you're catching exceptions that just aren't going to happen...
Anyway, it's difficult to have a reasonable discussion over a topic where said topic has religious dimensions for one of the involved parties.
C++ biggest problem are all the design decisions to fit into C's tooling and its compatibility.
That said as someone who hasnt done a lot of C or C++, i'd like to think learning the new way is best, otherwise it's hard to know where to start.
In the 80's Assembly was the only way to go, if you were targeting consoles and home systems.
Higher level languages were the managed languages of the day.
When the SDK started to push for C, like on first PS, they were forced to move. Still the code was pretty much inline Assembly wrapped in C, until they were confident to write more and more in C.
Similarly when around PS 3 time, the SDKs were moving into C++, the majority took advantage of the C compatibility and used only as much C++ as required by the SDKs.
Only recently some started to do the transition to embrace C++.
Likewise if in the future language X becomes the only way to target games for platform Y, and they a market that they need to be in, they will eventually adopt it. Of course writting big chunks in C++ until they can be convinced it is ok to just use X.
The features are dependent on each other though. The flipside of my example elsewhere in the thread: if you want to use templates to eliminate repeated code, you more-or-less need to make any classes you want to template over use RAII (or else do very error-prone partial template specialization to ensure your things are initialized and deininitialized correctly). So then you have to use exceptions, because if you're doing complex logic in your constructors then you need a way to report failure. So then you have to make your code exception-safe, which means making everything else RAII.
Your definition of 'successfully' is vastly different from the Megacorp definition of 'successfully'. I interview C++ programmers _all the time_, and let me tell you: practically the only reason those restricted subsets of C++ exist is to allow bad programmers to work at Megacorp.
There are a lot of really unqualified C++ programmers in the world, and by far the biggest problem faced by Megacorp is employee churn, not code quality and not product deadlines.
The use of 'C++ subsets' solves the employee churn problem at the cost of code quality.
IMO constructors should be setting sane defaults and nothing more. If you need to do work that could fail put that into an "init()" function with a return type that can signal an error.
I worked for quite a while on platforms that didn't have exceptions and this was the standard pattern for keeping RAII without needing them. If you wrap the return value correctly(like with Result<T,E>) you can also guarantee that your clients need to deal with the error case(or fail to handle it with unwrap()) as opposed to getting caught by an exception that wasn't declared or documented.
That allows you to construct the object in an invalid state. The whole point of RAII is to avoid that. The acronym literally means not having a separate init() function.
FWIW some quick googling shows that having a constructor acquire a resource is not a requirement(https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Resource_A...). The important part about RAII is that the automatic releasing of resources when out of scope(through a no-throw destructor).
This is still RAII, you're just not using a constructor to acquire. Rust doesn't have constructors and is still RAII.
[1] http://www.llvm.org/docs/doxygen/html/classllvm_1_1ErrorOr.h...
In any case, .unwrap() is perfectly fine in applications, it's only in libraries that they're discouraged.
To me success includes being able to find (not frequently replace - I like working with the same people for many years - but find new ones in order to grow) developers that can contribute to the code base, and some of the best ones I know can't be bothered to learn much of C++. Go figure. Maybe you and I legitimately disagree because we work on different things, and maybe one of us is wrong.
Edit: It seems the standards have changed. It doesn't seem so bad now. They cant use exceptions but they can use some of boost cpp11 and templates. So maybe working at google ain't that bad.
- C style error codes
- minimal use of STL
- use of templates is frowned upon
- C style enums
This just of the top of my head, I would need to read it again to see if they changed anything.
I was recently reading the docs for a physics engine in c++ and they said that its initial implementation used std::vector in many places, but they instead implemented their own in later versions to accommodate maximum "portability and compatibility." I wonder what that means? Since the STL is included in C++ compilers and vector is always going to be there, why would they need to roll their own to make the engine either more portable or more compatible?
>use of templates is frowned upon
I wonder if this means the writing of templates, or using existing templates. If the latter, maybe like the STL their primary concern is compile times?
I imagine templates are frowned upon for the same reason -- too many moving parts. Otherwise they're perfectly fine to use in projects that have goals other than portably predictable performance.
Basically, Google has an enormous code base. Stylistic decisions should have semantic weight, but since everybody has their own style, it becomes challenging to understand the reasoning behind certain choices. When you have a standard, it means that you can read into some of the stylistic decisions sprinkled throughout the code.
Take for example, the use of pointer arguments for passing the addresses of results. The justification is that using a pointer is explicit and unambiguous. Non-const references have value syntax but pointer semantics, which can be confusing. Yes, this is contrary to some of the core values behind C++ but Google felt that the improved readability was worth that dissonance.
Do Google enforce const pointers for arguments that define where to store results? If not, you could delete the pointer they pass you. A pointer does not always indicate who owns the data clearly.
Far better would be to return the values from the function (or std::move them out if you must) instead of using a parameter as a "dump your results here" flag.
Stroustrup encourages this (see 5.3.3 of C++ 4th edition where he says "I don’t consider returning results through arguments particularly elegant"). IMHO, it would be better to return a std::pair<bool, std::vector<myWonderfulType>> where bool indicates the success/failure of the function, with pair.second item being the results.
You cannot ever known what a pointer is used for.
If the prototype has const, maybe just maybe, assuming the function doesn't cast it away, it is a read only.
So I never bought into this argument.
Google around for Visual Studio 6 STL, and take into account the fact that most organizations don't upgrade right away, and you're looking at at least 2005 before you could rely on the STL cross-platform. Maybe much later.
I think that C++11 and beyond will do quite well. Sadly here in the UK at least, it has lost vast swathes of the job market to C# (likely because of C++11 taking so long to appear) but there are still a few C++ jobs (for maintaining crusty MFC codebases) or a few that specifically mention C++11.
I myself think C++11 is a great language and daily write C++03 at work through gritted teeth.
As you say though, we'll see what comes about. It is only a language after all.
EDIT: And I will add that C++ does have the benefit of having loads of libraries available to it, including GUI libraries and bindings which makes development a breeze.
The very definition of static safety guarantees is that the compiler enforces them, and that's what makes them useful. C++ has great safety guarantees if written correctly - but if you can reliably write code correctly, you don't need safety guarantees in the first place!
> EDIT: And I will add that C++ does have the benefit of having loads of libraries available to it, including GUI libraries and bindings which makes development a breeze.
There are many libraries, but the important ones have mature bindings from other languages, or at least tools for generating them (e.g. Swig). That said, using them exposes you to the safety risks of C++, so you're often better off rewriting them (e.g. I worked on a Java project that initially used ffmpeg to handle some transcoding tasks - but as a result had to deal with bugs in ffmpeg that took down the whole JVM for particular inputs).
Good point though!
Sad about your JVM - did you not monitor the state of the process you spawned to handle transcoding? Did you end up fixing the bugs in ffmpeg?
When the set of actions that might violate the guarantees is small, circumscribed and easy to spot in code review and/or with automated tools this kind of approach works. But in C++ it feels more like today's best practices are tomorrow's unsafe constructs - it's like perl's "there is more than one way to do it", but additionally all but one of the ways are unsafe.
> did you not monitor the state of the process you spawned to handle transcoding?
That was my point - we originally tried to use JNI so that we could use libffmpeg as a library (and e.g. handle parameters and input/output as structured data rather than having to flatten them to a string command line and temporary file or pipe). But unfortunately if you do that you give up the nice guarantees of the JVM platform - e.g. a Java-native library will only ever affect its own thread, but a bug in a JNI library can take down the whole JVM. (We did ultimately move to a separate-process-for-transcoding model for exactly this reason)
> Did you end up fixing the bugs in ffmpeg?
We fixed specific bugs as we found them. It didn't feel like we were making a dent in the number there were though.
Agreed about the potential of Rust, it sidesteps most of these problems. Though it might introduce other problems of its own, only time will tell. I expect that Rust's future sore spots will include destructor leaks, the reliance on macros, and the tremendous complexity of traits. (Though of course I don't claim to have better solutions to those.)
Yes, the Waterbed Theory of Complexity[1]. It can be useful to funnel complexity into specific areas where it's at least expected, and there may exist better mechanisms for dealing with it.
1: https://en.wikipedia.org/wiki/Waterbed_theory
Right, I'm not saying C++ is perfect either. I feel like Rust made a wrong turn here: https://www.reddit.com/r/rust/comments/3404ml/prepooping_you...
> reliance on macros
I'm referring specifically to try!.
None of the proposed solutions mitigated this either. You will always be able to leak cycles. The difference was that some proposals made it possible to mark types as non-leakable, in which case they can't be placed in things which may potentially leak.
The fact that things will leak was never under question there. The decision to be made was on how safety should interact with it -- should there be a way for a type to rely on its destructor being run for the purposes of safety? The decision was that there shouldn't, and we ended up fixing two APIs which relied on destructors being run for safety, instead of adding a major language change.
Maybe the cleanest solution to the leakpocalypse would be to mark RefCell.borrow_mut as unsafe, because it's problematic in two separate ways (it can both panic and create cycles). That way the safe fragment of Rust would guarantee that memory leaks can't happen and destructors always run. That would fit with the spirit of Rust, which strongly prefers 100% guarantees to 99% guarantees. Though I guess that idea was judged too inconvenient for users, and I don't claim to know what users want.
One way to handle this is to have each type explicitly opt-in to saying they're concurrent-safe, but this is fairly annoying, as it requires a lot of boilerplate. Boilerplate that is mostly pointless in Rust, because its rules mean most types people define will be safe, and most types that aren't safe will be built out of existing types that aren't safe. OIBITs are proxies for "safe", and are designed to strict a balance, by basically following that rule (types made out of types implementing an OIBIT automatically impl that OIBIT too).
Moving on to your specific suggestion: RefCell isn't the only way to not run destructors, for instance, one can create ownership cycles with Mutex and RWLock in a similar way, and, more broadly, threads that dead lock (or live lock) will leak their resources, as would calling std::process::exit. There's also weirder/wilder things like the following: suppose Queue<T> is a multiple-producer multiple-consumer queue type, where any instance can act as both a producer and a consumer (typical for such types), then the following code creates an ownership cycle within the queue, and so won't run the destructor of X:
Now, sure you could mark all these things unsafe, but I think that starts to make things very inconvenient, and, somewhat, weaken what `unsafe` means (it normalise its use since those things are typically totally fine). (Also, those examples are almost surely not exhaustive.)1) After some soul searching, it seems like the really important invariant to me is that all alive objects should be reachable from the stack(s) at any given moment. So I'm kind of okay with failing to run destructors due to deadlock or process exit, but not okay with leaking a cycle. Is that a sensible distinction, or is the situation even more subtle?
2) How do I make a cycle with Mutex and RWLock?
3) I don't completely understand the queue example, what exactly is the magic that allows self-reference?
I think that's a reasonable distinction, but, at least for deadlocks, I'm not sure it is enough to have all the guarantees one might want (particularly around multithreading/scoping). Then again, it might be.
> 2) How do I make a cycle with Mutex and RWLock?
The same as a RefCell, just call `.lock().unwrap()` and `.write().unwrap()` instead of `.borrow_mut()`. These two types are essentially the same as a RefCell, just with more synchronisation, and fewer panics. They can block and wait for another thread to finish, at the risk of deadlocks of course, while a RefCell is restricted to a single thread, so blocking would just dead lock a thread with itself, hence a panic resolves the situation better. In fact, other than this blocking/panicking distinction, a RwLock and RefCell are almost identical: .read == .borrow and .write == .borrow_mut.
Here's a demo which creates cycles that contain a type that prints in the destructor (demonstrating that they don't run): https://play.rust-lang.org/?gist=6ea16b859e22f57fee14&versio...
> 3) I don't completely understand the queue example, what exactly is the magic that allows self-reference?
This is also essentially the same as the core RefCell/Mutex example, replacing .borrow_mut()/.lock() with .send. An mpmc Queue<T> will be isomorphic to Arc<Mutex<VecDeque<T>>> (although unlikely to be implemented as such in practice). The key is that they will reference-count the book-keeping that needs to be shared among all handles (like the start/tail of the queue), which allows creating a reference cycle.
I think of things like the leakpocalypse the same way I think of security advisories like the Go one from a few days ago. Yeah, it looks bad on paper, but the odds of being affected by it in practice are so incredibly low that it's not worth worrying about.
It describes a (sketch of) a statically provable memory safe dialect of c++. A more detailed description is in one of the referenced papers by Herb Sutter.
Unfortunately the promised static checker is, as far as I know, yet to be released, and we will have to see how well it will work in practice and whether it is feasible to piecemeal add the safety annotations to an existing codebase.
Remember the Y2K problem and how those programmers in the 60s and 70s thought that by the year 2000, surely their code would have been either fixed or rewritten.
At most, C and C++ are "dying" in the same way COBOL is - for new projects, other languages might prevail, but there are still mountains of legacy code written in it that nobody dares touch...
Compared to Rust, the C++ Core Guidelines:
1. Are still whitepapers, the tool has not been released. 2. Does not protect you from everything Rust does. 3. Incrementally improves on C++, rather than being a whole new language.
These points have a number of implications. On the first point, all Rust code that exists today is already written in this style. As the tool still isn't released yet, essentially none (the tool is built based on some Microsoft work that they've been using internally, but other than that) of existing C++ code is following this. This means that even if _you_ use the tool, it doesn't mean your dependencies use the tool. Furthermore, how many people lament that they are still writing C++98 and not C++11, let alone C++14? Once the tool exists, how much code will actually use it?
On #2, Herb has said in the past that "data races are off the table" and "dangling pointers in some circumstances are important." I _think_ that in the latter case, he means something akin to what we call "non-lexical borrows" in Rust, so it's not a big deal, but data races are. Rust's version of this concept also provides a foundation for compile-time concurrency errors, and this paper states "Our rule set for concurrency is not yet fully developed." We'll see what they come up with.
On #3, this is both an advantage and a disadvantage, and ties into #1. If you have a large C++ codebase, it's not like you're just going to instantly port everything to Rust. Even Mozilla, with Firefox, is taking an incremental approach. This means this tool could have a lot of value for organizations which are using C++ today, in a way that Rust can't. However, as mentioned elsewhere in this thread, I'm not sure how much C++ code will actually pass the tool; rules like this can really change your design, so we'll see.
EDIT: One more difference, we use "memory safety" in different ways. They describe their definition on the end of 3.0:
We usually describe memory safety in terms of "no data races". Theirs assumes single threading (see above about how concurrency is under-developed here) and we take it into context. Terminology is hard!Finally, I'm glad to see this project develop. I'm glad that the C++ committee is taking safety even more seriously than they have in the past. We all want better software with less bugs.
> Once the tool exists, how much code will actually use it?
Any number of the 10M+ C++ developers and probably a large number of Open Source projects. I remember something like 20% of C++ (projects|programmers) are using C++1/4. If half of those use the tool, that is still 1M programmers using a much safer system. Not the same as Rust, but better than the old way. In the near term, the total quantity of safer code will come from C++ adopting better static analysis than from the adoption of Rust, even if Rust is the better way.
The biggest impediment to Rust adoption with C++ programmers is the non-ability to incrementally include Rust in a project. But this is true of C++ in general.
Rust has the advantage of mind share, witnessing the mistakes of C++ and the ever increasing torrent of new programmers who will choose Rust over C and C++. Every problem that a C++ programmer has, a Rust programmer has. A Rust programmer just solves it slightly differently.
Less work, but nontrivial and of a similar kind.
CppCon 2015: Neil MacIntosh “Static Analysis and C++: More Than Lint" https://www.youtube.com/watch?v=rKlHvAw1z50
CppCon 2015: Neil MacIntosh “Evolving array_view and string_view for safe C++ code"https://www.youtube.com/watch?v=C4Z3c4Sv52U
For something of this magnitude I wouldn't consider the project late until mid summer.
Stroustrup's keynote at CppCon is a great overview of the semantics they want to support and the errors that the tooling with will prevent. https://www.youtube.com/watch?v=1OEu9C51K2A
- Simpler lifetime system: Both a pro and a con, because some functions will not be expressible using "c++ borrowing"
- No `Alias XOR mutability`, which in rust protects from memory issues and things like iterator invalidation. Not sure if isocpp addresses the segfault that can be caused by
its been a while since I looked at the guidelines and I'm not sure of either of these points, take them with a grain of salt.However, what made my jaw drop was section 3.2: "a dynamic model" for making the system safe and easily implemented in hardware. The section said Stroustrup deciced not to publish it because it was impractical. One of few times see his name and go "what an idiot..." What wasn't practical in general case or that time period doesn't mean it won't be in future in other circumstances. Good ideas, esp simple solutions, have a way of coming back to us.
Specifically, efforts like Cambridge's CHERI processor are looking at minimal modifications to CPU's to greatly increase assurance w/ legacy compatibility. This concept should've been published as there's use in embedded and appliance field where a lot uses subsets of C++. He should've published it. I'm saving the paper for my collection just in case that idea comes in handy.
He's right though. Such a system would be slower than a good tracing GC, which completely eliminates these problems and is much easier for the programmer to use.
That sort of thing happens more than you know in CompSci. Example: One method for Oberon and Java appliances I pulled right out of an old Scheme CPU paper that was also impractical then. Also found a correct-by-construction HW method in same one. ;)
"supports the general thesis that garbage collection is neither necessary nor sufficient for quality software. This paper describes the techniques used to eliminate dangling pointers and to ensure resource safety."
"Every object on the free store (heap, dynamic store) must have exactly one owner [pointer/reference]"
"set the ownership bit when a value returned from new is assigned"
"use delete whenever a pointer with the ownership bit is set goes out of scope"
Sounds similar to what I read in Rust guide, esp one-per-location and scope part. Those and other rules appeared to make it easy for checks to prevent or catch use-after-free. If so, then my keep it around for hardware suggestion could lead to acceleration work. If I misread the paper or goals of ownership model, then my suggestion would've only led to accelerating... whatever safety it was trying to enforce.
Did that clear it up for you? Or did you spot where I went wrong? Could go either way given it was a quick read for me.
This is not a contrived example. It is precisely what kept hitting us in Rust with things like @mut and we took years to figure out the best solution (RefCell/Mutex). It comes up all the time because you can't get away from RC--large programs use reference counting a lot, and even if they don't one of the major use cases of systems languages is to interoperate with GC'd or RC'd systems (dynamic languages, COM, Core Foundation) where you don't statically know anything about aliasing. Forbidding these calls statically bifurcates functions into "those callable on shared_ptr" and "those callable on owned or stack-local objects", which is unusable in practice. The only solution we found is dynamic enforcement of the aliasing rules, scoped to reference counted pointers. This indeed works, but it requires overhead. You want this overhead to be opt-in in a low-level systems language, but existing code was not written to opt into this. So C++ will need to backwards incompatibly introduce RefCell/Mutex to support this.
The bottom line is that I see a dilemma. Either C++ makes it impossible to use shared_ptr with mutable references, severely damaging usability, or it breaks backwards compatibility by making shared_ptr immutable and requiring dynamic enforcement of the borrow rules for mutable shared_ptr. Either option has huge consequences for existing C++ code.