Hacker News new | past | comments | ask | show | jobs | submit login
Rule of Zero (C++) (rmartinho.github.com)
103 points by AndreyKarpov on Nov 23, 2012 | hide | past | favorite | 69 comments



It's nice to see more and more C++ articles.

Some additions:

- The least you allocate on the heap, the better. Move semantics really help in that aspect;

- Smart pointers are not magic. Although I really think using unique_ptr whenever possible is a good idea, shared_ptr comes with a performance cost (especially in multithreaded environments), can introduce vicious memory leaks and is often used as an excuse to not think your lifetime cycle through.


I also like it.

C++ suffered for a long time due to lack of quality libraries.

This now seems to be finally changing, at least until another language is a good enough replacement for it.

I am keeping an eye how D and Rust evolve, additionally Ada seems to be getting some users, at least here in Europe.


Ada in Europe? Interesting, where did you encounter that?

As for quality libraries, I'd almost say that we had too much of them. For every platform and every different C++ methodology/subset (from "C with Classes, ocassionally Structs" to "Post-Modern Template Abuse") had its own set of supporting libraries. Often good enough, but most of the time not the ideal source for your particular project, and very hard to integrate, if every code paragraph follows a different paradigm, depending on what library your calling.

Still don't see a good solution to that. Quite likely that your GUI library won't quite mesh with your collections, data parsing or network support.


> Ada in Europe? Interesting, where did you encounter that?

I heard it at this year FOSDEM.

Since GNAT availability many universities seem to be making use of it. As for the industry I guess it still constrained to the usual types of systems where Human life is very important.

As for the libraries you're doomed to have that for all libraries that are not part of the standard library. Even the "comes with batteries" languages suffer from this when you need to integrate external libraries.

If Java and .NET didn't happen, maybe there would already be an InteliJ for C++. I am looking forward for the tooling progresses made possible by using the compiler as plugins, as clang shows.


Well, if you've got a pretty big standard library, you've got more on which to base external libraries on, regarding the style of the code. The STL alone didn't really provide a big enough template. Sometimes it even went the opposite way, so when you read something in lower-case letters with underscores, you knew that you were using the "core C++ language", putting things like "vector" and "reverse" closer to keywords than library APIs.

Especially if the rest of the code base was the usual OO class forest.

I knew of Ada use in teaching, it seems to be quite popular for some advanced "software engineering" courses. Then again, I've seen a pretty great amount of Algol-family languages there, out of proportion to the actual practical use. Oberon was quite popular for a while, as it was a very small language to teach and the barriers to running your first programs were basically non-existant (as you can easily call your module function directly, compared to the rather perplexing "public static void main" of introductory Java courses).


Eh? C++ has always had a pretty good selection of libraries, way better than most languages (and of course, a bazillion times better than D/Rust/Ada...).

Many library authors will choose to make a C interface instead, as that basically makes the library usable by almost any language, but C++ seems near the top when it comes to language-specific library support.


In what planet have you been leaving?

What are the C++ libraries for

- Networking

- XML

- Reflection

- ORM

- Database access

- UI

- Asynchronous programming

- Make use of std::string and STL, instead of providing their own types.

- Web Services

- ...

Don't forget that your list should provide libraries that work across multiple OS (UNIX and Windows are not enough) and multiple compilers (besides gcc, clang, msvc).

The libraries also need to be compatible with the possibility that the developers compile with exceptions, RTTI enabled/disabled.

Did you know that STL was actually developed in Ada and later on adopted to C++?


Qt provides most or all of the things in your list -- just to name one.


You forgot my requirement, in addition to the list:

> Don't forget that your list should provide libraries that work across multiple OS (UNIX and Windows are not enough) and multiple compilers (besides gcc, clang, msvc).

EDIT: typo


I didn't forget it, I ignored it b/c it seemed unrelated to your original complaint about a lack of quality libraries. Linux/Windows/Mac support is plenty for many people.

Out of curiosity, what platforms do you need to use which are unsupported by Qt but work well with other languages...?


All the UNIX systems used in Fortune 500 companies, mainframe systems and embedded devices.

The consulting world is much more than just Linux/Windows/Mac.


Who said anything about consulting? Your complaint was the lack of quality libraries, not the lack of quality libraries which can be used in every possible computing environment.

Which languages/libraries work well in all environments?


Java for one, as strange as it might seem.

Most of those environments have JVMs available. Although. a few of them, like i/OS, still only have Java 5 or 6 available.

Don't take me wrong I am not against C++ and do use it in some of my employer projects. Then again we need to fill our code with #ifdefs when the need to support multiple environments with different sets of compilers arises.

For example, even Boost with its exhaustible list of compilers and operating systems does not support:

- HP-UX

- i/OS

- QNX

- VxWorks

- Symbian (yes people still develop for them)

This is just one library, it gets worse if we need to take a few proprietary libraries into the equation.

Now, for doing native code programming, C++ is the best currently available language, and I wish that with C++11 and the upcoming revisions, it gets better.


yeah, and this was a very well-written one. rust is doing some very interesting things with lifecycle management too; it will be interesting to see how d and rust influence the evolution of c++


Abstracting the ownership behind an interface presumably is needed when the ownership model is very complex. From my experience this is nothing but a big red flag. If you need a class to manage an ownership, it means that you can't easily trace its movement, in which case it means that it is a total bitch to debug, leave alone being able to read through the code and understand where that damn ownership is at any given moment.

I know it's beautiful language with a laundry list of features, but keeping things simple is still the best approach:

  #define __no_copying(type)                      \
                                                  \
	private:                                  \
		type(const type &);               \
		type & operator = (const type &); \
	public:

  class foo
  {
  public:
        foo();
        __no_copying(foo);
  ...
  }
In a spaghetti-less code this typically applies in 99% of all cases. There's a remaining 1%, which it's easier and cleaner to handle on a case by case basis.


Even better:

  class foo : boost::noncopyable
  {
  public:
    foo();
  };


Oh, jeez. Don't get me started on that Boost thing, comrade.


boost has many problems, but I think not using any of it just because some parts of it are bad isn't very useful.

In this particular example it is very easy to screw it up. Your way involves many more lines of code and use of the preprocessor which makes searching through code very difficult. I can easily find all the noncopyable classes by searching for "noncopyable". I don't have to worry about someone coming along and screwing up the access modifier at a later date. It is much less likely that someone will misunderstand what the code is trying to do, even if they don't know much about C++ value semantics.

Edit: I guess my criticism regarding searching isn't valid here, but I have come to try and avoid the preprocessor as much as possible. I've just been bitten by it too many times.

  class Noncopyable
  {
  private:
    Noncopyable(const Noncopyable&);
    Noncopyable& operator(const Noncopyable&);
  };
That's pretty much was boost::noncopyable is, and it's the same thing you wrote, but it removes the preprocessor.


> it is very easy to screw it up

No, it's not. This just a made-up argument.

> ... many more lines of code

6 in #define, one of which is blank. Your option however creates an external dependency on the library that somehow needs to be built even though it is supposed to be just a collection of headers.

> ... and use of the preprocessor which makes searching through code very difficult

Ok, considering your Edit, let's skip this one.

> I can easily find all the noncopyable classes by searching for "noncopyable".

Similarly, you can find all noncopyable classes by searching for "__no_copying". Besides, why on Earth you would ever need to do that in real life?

> I don't have to worry about someone coming along and screwing up the access modifier at a later date.

Ah, right. Here we start to really diverge. You are writing code that needs to be protected against incompetent colleagues, and I typically don't.

> It is much less likely that someone will misunderstand what the code is trying to do

No, of course it is not. This is too a made-up argument.

PS. I hate Boost with passion, because it drags C++ in the exact opposite direction from how I am, personally, using it, which is C with classes, uncomplicated inheritance and basic templating. This is beautiful and functional subset of C++ that retains the clarity of C and adds flexibility and convenience that latter is missing. Boost has its place, clearly, but I don't understand programmers who willingly use it, just as I don't understand those who like Bjork.


I have seen this screwed up time and time again. People who don't know how all this works shouldn't be writing C++, but they do and I have to deal with the things they break. Most of my code isn't broken by incompetent colleagues, it's incompetent clients that use the code. Maybe the experience is different when you're writing commercial libraries which is what I've been doing for the past few years.

I guess you and I have had vastly different experiences. boost has improved my code immeasurably. I cannot stand the "C with classes" approach. All the code I've come across the uses that approach is always broken in subtle ways the could be avoided by using the simple utilities in boost. I think in the end it is probably just personal preference.

I'll agree with you about Bjork. But I am a serious musician and the best musician I have every been in the presence of loved Bjork.


Ya, and in my humble opinion / coding style (everyone's got one...) it's not too hard to just add the

foo(const foo &);

foo & operator = (const foo &);

to every class definition that needs it. Small amount of basic, simple C++ boilerplate > magic.


Agree in the main. Even before smart_ptr's were integrated into the standard, bespoke versions that appeared to some to remove scoping confusion had a tendency to propagate like weeds. In the majority of cases, const correctness, mutability if needed and good scoping concepts removed redundant reference counting run time garbage.

On one project I worked on as a contractor, and quite a large one at that, I removed every single one of them and rescoped the code base. It took a while but performance alone improved by about 400%


I'll say this just once: smart pointers and reference counting are not the same thing, and neither of them implies the other.


Didn't need to say it at all. I probably wasn't being clear enough but I was referring to early versions that were called smart precisely because they removed the need for the programmer to count because they counted themselves. Hence 'smart'. Before boost and others cleaned it all up.

As others are pointing out they have advantages and disadvantages. It all depends on the situation. But they're certainly not free because they have a scope cost. Code size and simplifying compiler optimisation are important too. I think it's worth that pointing out.


the lesson there was not that every programmer needs to painstakingly thing through every detail of scope management, it's that the problem needed to be solved comprehensively and integrated into the standard library.


That was one lesson. There were others as well, and they're related. It was a temptation to believe that they were a kind of magic bullet that acted as a scoped resource scavenger. Mostly vtable jump resource cleanups through dtors in the usual way.

Unfortunately it was slow, and as others have mentioned, sometimes difficult to debug and heap errors can propagate out of order, depending on other conditions.

I'm in no doubt about the value of the auto_ptr variants. However I'm often brought in to speed up projects that don't meet performance requirements, and it often entails rethinking structure, simplification, replacing generalised ctor dtor lists, and rediscovering where const references, independently of move semantics, lead to algorithmic questions that need reexamining. This leads back to encompassing scope, and from there it's often the right thing to institute a better model.

The benefit can be a lot less instructions and resultant speedups.

Perhaps I've had too many of these jobs, but hey, it's a living.


I disagree. “unique_ptr” and “shared_ptr” both model very simple ownership policies, yet there is still value in separating them from the objects whose lifetimes they manage. The value is in their composability—a programmer can rely on “unique_ptr” to do the right thing automatically, in a manner enforced by the type system. When writing C++ code, I spend an irritating proportion of my time navigating the minefield—it’s well worthwhile to encapsulate some of those details.


The codebase I'm currently working on has quite a few bugs related to ownership. However, the programmers I'm working with are very capable and all are familiar with different kinds of smart pointers in the STL. Unfortunately they are either, 1) typedef'd to the point where it's hard to tell what kind of ownership is used where, 2) often can't be used because objects need to be passed by reference to libraries that use regular-old-pointers.

I think many bugs could have been avoided by not typedef'ing our pointers, because when you have to type `std::shared_ptr<myclass>`, then you know what kind of pointer it is. On the other hand, if you have to, at least call the typedef `myclassSharedPtr` instead of `myclassPtr`. But for (2), I don't see a work-around. How can you write good code when your critical dependencies don't play nice?


> often can't be used because objects need to be passed by reference to libraries that use regular-old-pointers.

I don't understand what you mean by this. If you have a `smart_ptr<T> p;` nothing prevents you from passing `*p` or `p.get()` to some legacy interface. You only have trouble if that legacy interface wants to claim ownership of the object.


I think the issue is that the typedef is intended to hide the smartptrness so it's not obvious what the API for getting an old-style pointer is, or whether getting an old-style pointer is even part of the intended public interface anymore (beyond p.operator->() anyway ;)


> You only have trouble if that legacy interface wants to claim ownership of the object.

That's exactly what I mean. This happens. My point is just that even the best intentions are easily subverted in a real-world scenario where you didn't write the whole codebase yourself from scratch.


I agree on (1). Typedef obscures more than it helps.

On (2), I think the solution is intrusiveness. intrusive data structures and intrusive_ptr should be used more in C++.

The dislike for intrusiveness is rooted on equating classes with responsibilities. A single responsibility is often spread over several classes, because the classes form a useful cluster. A clear case is items of a collection. If an object already knows it's an item in a collection, there's really no harm in using intrusive lists.


Intrusiveness also isn't so good when you want the class logic to be independent of the memory management scheme in use. We experimented with language support for intrusive data structures in Rust (`@class` for an intrusive `shared_ptr`) and it turned out to be a lot of trouble, so we removed it.


Typedef should really be creating a new type, and not an alias. It's one of few thing I wish C(++) had different.


To me, "Rule of Zero" sounds more like not implementing {con,copy-con,de}structors, i.e. forgoing RAII. I've been doing this quite a lot, and it makes writing low-level and optimized code so easier.

Actually, I find that having fixed ownership makes much more sense than reference counting for the majority of cases. Example: we have a FileSystem, and a bunch of File's belonging to this FileSystem. Whenever a part of a program needs a file, it just initializes its own File object, and when it doesn't need it anymore, it releases it somehow (e.g. file.deinit() or destructor if RAII is used). Even if two users both need the same file, each gets its own File. The FileSystem internal implementation can take care of any resources sharing, invisible to the users.

As far as the memory management of FileSystem is concerned: again, in most cases, you don't need shared_ptr<FileSystem>. Just make sure the FileSystem stays alive as long as there's the possibility that anyone is using it. Like initialize it at the beginning of the program, and deinitialize it during shutdown at the right point.


This "Rule of Zero" construct doesn't forgo RAII, since RAII does not simply mean "write constructors and destructors".

Instead it allows the compiler and language to automatically do the right thing to enable RAII by using appropriate library-provided building blocks. E.g. his std::unique_ptr + custom deleter type enables the compiler to automatically make a RAII-compliant class for the author, so it's still RAII, just automatically implemented (except for the ctor, in this case).


I like that using technique, but why is it using and not typedef? I thought using was meant for exposing names from superclasses.


In C++11 you can write "typedef templates". However, as "typedef" has always been a misleading keyword (it does not define a new type, it declares an alias for an existing type) it was decided to instead overload the using keyword:

    template <typename T>
    using MyVector = std::vector<T, MyAlloc>;
As a side effect, it is now also possible to use using as a 1:1 replacement for typedef:

    // strictly equivalent to typedef int MyInt;
    using MyInt = int;


After reading the C++ FAQ on this I can accept that using tyepdef may have been impossible/confusing, but why overload "using"?

This is one of the things that makes C++ mega-complicated. The same keywords have different meanings based on context, when really a new keyword should just have been used. For example "virtual" functions vs "virtual" inheritance.


The moment you add a keyword to a popular language, you break lots of existing code that happen to have that keyword as an identifier, e.g. as a variable, class or function. This means that when adding features that need a new keyword, you best overload an existing one.

It's for this reason, too, that Java 7 overloaded `try` to implement some sort of poor-man's-RAII-support, instead of adding a new keyword (such as C#'s `using`, which does the same, and was by the way also overloaded for the same reason)


In the OP example, "using" has the same semantics as "typedef". The new syntax emerged from the need for "template typedefs".

http://www.stroustrup.com/C++11FAQ.html#template-alias


Oh. I wish they would have just shoehorned it into typedef somehow. I guess they must have had reasons why they couldn't.


+1 on that; people intuitively expect template'd typedefs to work until they discover they don't. Oh well.

You can read about the reasoning behind the new syntax here:

http://www.emarcus.org/papers/n1449.pdf

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2003/n148...


The alias syntax has the benefit of not requiring typename everywhere because the syntax requires the right parts to be types and doesn't allow for ambiguities as does typedef. This makes code in dependent contexts a lot easier to read and to write.


There's a similar rule of thumb in .Net with respect to managing native resources. Inlining the logic that handles the lifetime is much more bug prone than encapsulating that logic into a wrapper class (e.g. SafeHandle http://msdn.microsoft.com/en-us/library/system.runtime.inter... does this).


I guess I'm lucky enough to never have seen that class in code I've seen. :)

That seems like typical Microsoft object-orientation. It's not really much of encapsulation. Reminds me of MFC :-/


Actually Microsoft started by developing a C++ library with lots of OO in the design.

However not everything went as they expected and people were also demanding something more light over Win16 and MFC was born.

http://computer-programming-forum.com/82-mfc/d13ea80282846f9...


Nice article, but the use of std::wstring threw me in a little loop. Perhaps the author is on Windows where its usage is more common?


Per the article, "As an example, let’s write a class that owns a HMODULE resource from the Windows API."



Do people who write C++ also manually allocate disk blocks instead of using the filesystem? Just use a garbage collector. These problems will all go away.


The problems start when you need to manage resources other than memory.

Examples of things that require explicit clean up: -> Files -> Sockets -> Transactions

The C++ resource model can be applied to all resources, not just memory.

Garbage collection only "solves" memory management for you - often that's the least important resource that your program is managing.


> often that's the least important resource that your program is managing

I think you're right but wrong here : it may be the least important in a technical sense, but the sheer amount of developer productivity lost in manual memory management is very expensive (for both the mental resources of the programmer and the firm), and this makes automated memory management a key positive.

Honestly, my eyes glazed over to avoid traumatic memories of C++ and Windows DOM internals as I read the article. I have a LIFE since I moved to Java.


True, however you have mechanisms like using, with, try, scope, defer that GC enabled languages offer.

C++ destructors benefit is that you don't forget to call such constructs.


Most (all?) of these cannot cope with trees of objects, plus that they leave the responsibility of destructing to the user of the object. So they are just as ugly as external locking. Problem is that very few people understand this, so they implement "using" and feel content. I would give an arm and a leg for RAII on the JVM.


> Most (all?) of these cannot cope with trees of objects

You can if all "destructors" take care of also closing the internal resources, thus initiating a cascade process of the object owned by them.

But you're right, this leaves the responsibility in the user's side.

> I would give an arm and a leg for RAII on the JVM.

At least they finally added the "using" concept.

In a few more versions maybe they get to add values.


Or in other words, garbage collection only covers about 99.999% of all resource allocations/deallocations ;-)


Patient0 said "the most important resources", not "the most frequently allocated resources".


The most important resource is the one that we spend the most time managing and that is clearly memory.


You don't need RAII to manage non-memory resources. All you need are first class functions.


The function trick suffices to handle objects with stack-shaped lifetime (certainly a very common case), but it does not suffice when resources are associated with parts of a data structure. The strength of RAII is that it handles both of these cases with the same machinery.


Could you expand on this a bit? I'd be interested to see an example of what you mean.


it's a ubiquitous pattern in ruby. this blog post uses a file open/close lifecycle manager as an example: http://yehudakatz.com/2012/01/10/javascript-needs-blocks/


That's more about coroutines than first class functions, and it is RAII, using a wrapper methodd for the actor/dtor instead of defining a class.


it is not raii as i understand the term. there is no destructor involved in closing the file, there is just a function that opens the file, passes your code the handle, then closes it when you're done.


Doesn't this amount to the same thing ?


it accomplishes the same purpose, but the filehandle gets closed explicitly, not when it goes out of scope and its destructor gets called.


No, but it is sometimes necessary for them to allocate big files and access them in a particular fashion. Or use an append-only data structure for the file and compact it now and then.

Sometimes exact control is what you need. It is no use trying to stop any discussion on the subject.


Not all resources are memory.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: