Bjarne Stroustrup – The Essence of C++ [video]
channel9.msdn.com
channel9.msdn.com
C++'s C heritage makes it hard to master and also causes countless misconceptions. Even if you never want to use C++, it's worth looking into some of the unique concepts that, sadly, didn't catch on in other languages so far. (My personal favourites are const correctness and RAII.)
Do watch the talk, the first few minutes might already be mind blowing for anyone who thought C and C++ are basically the same language.
Gadget* p = new Gadget(n); // not exception safe
shared_ptr<Gadget> p{new Gadget(n)}; //exception safe
unique_ptr<Gadget> p{new Gadget(n)}; //exception safe and less wasteful than shared_ptr if local
Gadget g{n}; //his preferred solution
My problem with his preferred solution is that I cannot know that Gadget is really a handle to a shared Gadget and not a big fat Gadget value on the stack. The only way of knowing that is to look at the implementation or the documentation.So if g is returned from a function or passed to a function by value, I don't know whether or not a deep copy is made. If I manipulate that Gadget, am I manipulating a shared object affecting others or is this my private copy?
Qt uses that pattern throughout, and because it is used for everything in Qt, you know that you're dealing with handles. But the C++ standard library doesn't do it that way. Almost none of the classes in the standard library are handles.
If I see a pointer, shared or otherwise, I know I'm not dealing with a deep copy. I know someone somewhere else might point to the same object.
I'd say you do not _need_ to know. If Gadget contains tons of data, it's wiser for it to just put that on the heap, internally. Like std::vector does. As long as Gadget handles (or forbids) copying, assigment etc. (rule of three), that's fine.
If you do have a huge performance issue because you're using a class that, unwisely, puts tons of data on the stack, you can use a unique_ptr instead. Kind of an edge case in my experience though.
But agreed, it's opaque when working with third party code. Gotta figure out the conventions.
Exactly. In the STL, if it looks like a value copy it is a value copy, but the handle pattern that Stroustrup prefers gives you no clue whatsoever as to what's going to happen. You have to figure it out one class at a time.
I can't count the number of bugs I've fixed due to programmers (myself, sadly, included) using IDE code completion features as a substitute for reading documentation.
For example, in the code you posted all the pointer-like handles are named 'p'. In my experience, if we consistently pursue minimal scope and minimal lifetime for our objects, then longer-lived objects are oddballs and we use the type of the handle to help describe its ownership strategy.
This is a very big problem in enterprise size code basis, specially with the rotation of external developers.
Well, at this point it's more like C++'s C++ heritage.
> const correctness
It certainly exists in plenty of other languages, but in a non-broken way. C++'s const correctness model is mostly broken, by the way [1]. D's model is a bit more sane [2]. Languages like Haskell or Clojure (AFAIK) make immutability a default. This kind of functional style is a good thing for C++ developers overall, as emphasized by John Carmack [3].
[1] Once again, the C++ FQA has plenty of wisdom to share: http://yosefk.com/c++fqa/const.html [2] http://en.wikipedia.org/wiki/Const-correctness#const_and_imm... [3] http://www.altdevblogaday.com/2012/04/26/functional-programm...
> RAII didn't catch in other languages
That's because most other languages are garbage collected. (Not that RAII is a bad thing, it's just not needed.)
What in particular is broken about const correctness in C++? That things are not const by default? I agree, they should be, but I wouldn't really call that broken. I put const wherever I can, just like Carmack. So I sort of pretend it's the default :)
I don't think it makes sense to compare const correctness in C++ to immutability in Clojure. In Clojure, you don't have objects that encapsulate internal state. const in C++ allows you to say which methods do change internal state and which don't. If you pass a const reference, the receiver can only call const methods. I just learned from you that D has this, wasn't aware of any other OOP language that has it. Not even Scala :(
> That's because most other languages are garbage collected. (Not that RAII is a bad thing, it's just not needed.)
RAII is about more than just memory, it's about any kind of resource. That includes stuff like file handles, Java's try {} finally {} is IMO a weird workaround. In C++, stream's can just close themselves when they go out of scope.
The lack of transitivity for one. Const pointers shouldn't be able to modify the pointed objects (that's the default in the D language). The fact that working with non-const-correct libraries is a nightmare (prepare to const_cast a lot!). There are plenty of other things that make this a real PITA to work with on sizable projects with legacy or outside code.
> RAII is about more than just memory, it's about any kind of resource.
That's true, yeah, though most of the time it's about memory (with smart pointers). But you're absolutely right on that point.
As far as I know, Common Lisp users have some kind of RAII with macros, but I'm not familiar enough to compare it to C++. Of course, D also has support for this idiom.
Are they? I either misunderstand or disagree. The syntax gets a bit silly with pointers, because you can have const pointers and pointers to const (and const pointers to const of course):
const Foo* foo; // Cannot change *foo, but can change the pointer (e.g. foo = 0)
Foo* const foo; // Can change *foo, but cannot change the pointer
const Foo* const foo; // Cannot change *foo or the pointer
> The fact that working with non-const-correct libraries is a nightmare (prepare to const_cast a lot!).Agreed!
> As far as I know, Common Lisp users have some kind of RAII with macros, but I'm not familiar enough to compare it to C++.
In my experience, the kind of problem RAII is useful for is far less common in Lisps (can only really speak for Clojure). One tends to not encapsulate resource management in any way, so handling it explicitly in the caller is fine. And you can use macros to do something when the scope is excited.
So in Clojure e.g., you can use with-open to do something with a file:
(with-open [writer (io/writer "foo.txt")]
(write writer "Hello")))
with-open ensures that the reader is properly closed when the scope is exited or when an exception occurs.In C++ (or any OOP language I guess), you'd want something like this:
File foo("foo.txt");
foo.write("Hello");
So I suppose it depends on the philosophy. For OOP languages, RAII makes a lot of sense IMO.[1] http://channel9.msdn.com/Events/GoingNative/GoingNative-2012...
That is to say if you have a constructor:
MyClass::MyClass(const std::string& s) : m_s(s) {}
That you call like: std::string s = "Some string";
MyClass c(s);
You're hamstringing the compiler into always copying that string instead of being able to use the new move semantics, because it can't mess with the guts of a const reference. Instead, do the previously unspeakable evil of passing by value and then moving, e.g. MyClass::MyClass(std::string s) : m_s(std::move(s)) {}
This lets the compiler know that if string has a move constructor, and is an rvalue, it can just move the guts into place instead of performing the copy, since the variable is 'sunk' into the new location. Huge wins all around.move calls strike me as ugly and kinda dangerous. What if you do wind up wanting to refer to s in the body later on? It'd be nice if the compiler just handled it.
Then copy s, don't move it.
Do you allow, but not require, the compiler to do this substitution whenever it can prove a value is never used again? This is unreliable across build options and compilers so it would be unwise to depend on it. There is an opportunity here for a tool that could identify places where you could insert move, perhaps even a -W option for the trivial cases such as above, but I am not convinced the language should allow the compiler to do this.
Do you make it mandatory and add more special cases for the compiler to have to implement? This would require the compiler to track references to make sure they don't get passed to some other function. It would need someone to codify the special cases in the standard and this might be very difficult. It would also be fragile, there would be cases where implicit move used to kick in but some added function call inhibits it even though a move is still the right thing to do.
Also, your example of escape analysis is precisely because Java doesn't allow you to express what you are wanting to do, whereas C++ does allow you to express moves.
edit: I would compare this to C++'s copy elision, but that has a very high value and is much less intrusive.
void f(int n, int x)
{
Gadget* p = new Gadget(n); // look I'm a java programmer! :)
// ...
if(x<100) throw std::runtime_error("Weird!"); // leak
if(x<200) return; // leak
// ...
delete p; // and I want my garbage collector! :(
} void f(int n, int x) {
Gadget p = new Gadget(n);
// ...
if (x < 100) throw new Exception("Weird!); // no leak
if (x < 200) return;
// ...
}
Yes, good night's sleep tonight after writing that...There are two better options:
void f(int n, int x) {
Gadget p(n); // Stack allocated
// ...
if (x < 100) throw new Exception("Weird!); // no leak
if (x < 200) return;
// ...
}
Or, if it really has to be a pointer: void f(int n, int x) {
std::unique_ptr<Gadget> p = new Gadget(n); // Smart pointer
// ...
if (x < 100) throw new Exception("Weird!); // no leak
if (x < 200) return;
// ...
}
Both will be automatically freed as soon as the scope is exited.This is fine until I want to do this, at which point C++ becomes a memory management bastard:
void f(int n, int x) {
Gadget g = new Gadget(n);
// ...
if (x < 100) throw new GadgetException("Gadget broke", x);
if (x < 200) return;
// ...
} void f(int n, int x) {
Gadget g = new Gadget(n);
std::shared_ptr<void> defer(nullptr, [g](void *) {
delete g;
});
...
But honestly ... I'd just use a stack allocated object - or a smart pointer. In the last year I didn't write one delete. (I'm a full time C++ dev). Gadget g = new Gadget(n);
That code won't compile unless Gadget has an assignment operator that accepts a pointer, which is, well, not a very common scenario.Note: I'm not trying to be nitpicky. I really don't understand what you mean when you write that you "want to do this".
Now, I don't know anything about you, but based off your comments in this thread I'm not sure you have a working knowledge of C++.
In C++, you have various ways of referencing and passing objects, so you need to be aware of the lifetime and ownership of objects. It's arguably harder, and unfortunately, the C heritage makes it a lot harder than it needs to be :(
void f(int n, int x) {
Reader fr = new FileReader("foo.txt");
// ...
if (x < 100) throw new Exception("Weird!); // resource leak
if (x < 200) return; // resource leak
// ...
}
A garbage collector that gives a false sense of security is much worse than no garbage collector at all.C++ makes it possible for the library writer to take care of freeing the resources automatically.
Anyway, what if you need to share non-memory resources? Suddenly you cannot depend on the garbage collector, you cannot use try/finally, you cannot use using or try-with-resource - you need to handle the situation just like in C++, except you're given fewer tools to do it - and a poorer understanding of the situation if you've learned that you don't need to do manual resource management due to the garbage collector.
if (x < 100) throw new Exception("Weird!); // LEAK!
You shouldn't use `new` either when throwing exceptions. Just: if (x < 100) throw Exception("Weird!); // no leak.I've done a limited amount of C++ many many years ago before I even knew what garbage collection was and I keep thinking of revisiting it, but honestly in my line of work ("Enterprise") I don't need the mental overhead of dealing with things such as pointers and memory allocation.
Perhaps, my view is outdated, but I get the impression that everything in C/C++ is just a little thorny when compared to other slightly more high-level languages, such as namespaces, package management, list comprehensions, library compatibilities, type strictness etc.
I would like to be wrong about that though... I wish I had a little more motivation to spend some real time with C++ (or perhaps even C).
Of course, you may prove me wrong.
- partial construction: having to deal with already allocated pointers in case of errors during the construction - copy construction: who owns the resources? (Alternatively, you'll need to remember to disable the copy constructor explicitly.) - copy assignment: likewise
Doing the Right Thing is just so much easier when you wrap those member objects into e.g. `std::unique_ptr<T>`. That'll disable copy construction and assignment, which means that you'll need to think of that separately if it's needed.
Can you expand upon what you mean by "safe" here?
If you mean "unable to fail", I vehemently disagree. Constructors should validate what is passed into them and fail if that is invalid. The alternative is to construct a zombie object that can't actually be used. Objects like this subvert the type system and lead to lots of unnecessary "if object.is_valid()" checks all over code that uses them.
If you have a pointer in a class that is exactly what you want to happen in most cases. Then if you want copies you "have to" define your own copy constructor and assignment operator that copy the object pointed to rather than just copying the pointer.
//C++ RAII goodness.
void foo()
{
conn.open();
file.open();
printer.activate();
//do stuff
}
//Java's lack of RAII is disturbing
void foo()
{
try{
conn.open();
file.open();
printer.activate();
//do stuff
}
catch(Exception e) { }
finally{
try {
conn.close();
}
catch(Exception ee) {}
finally{
try {
file.close();
}
catch(Exception eee) {}
finally {
try{
printer.close();
}
catch(Exceptoin eeee) {}
}
}
}
}On the other side, some programming techniques are hard or downright impossible without GC - e.g. heavy functional programming with immutable persistent structures.
Even with the new block syntax, you have painful nesting if the operation requires several disparate resources.
Blocks are an improvement, but not the same as RAII. A block is literally converted to a try-finally by the compiler. It is the same thing with a cleaner syntax. There is no safety added to the old Java way. A programmer can forget to put his stuff in a block and leak the resource the same as he forgets to use a try-finally. The onus is on the consumer, not the library writer.
With RAII the onus is removed from the consumer. The consumer writes safe code automatically, no onus to remember any special clean up idioms.
void foo() {
try (open file or resource here) {
// do stuff
}
// after try block, resource will close automatically
}
This works with non-memory resources such as files, streams, sockets, and database connections.Blocks are an improvement, but not the same as RAII. A block is literally converted to a try-finally by the compiler. It is the same thing with a cleaner syntax. There is no safety added to the old Java way. A programmer can forget to put his stuff in a block and leak the resource the same as he forgets to use a try-finally. The onus is on the consumer, not the library writer.
With C++ RAII the onus is removed from the consumer. The consumer writes safe code automatically, no onus to remember any special clean up idioms.
Hopefully we can have an interesting discussions on those topics now that at least 1 news about them has reached the main page.
but hey, i'm not a system programmer so i can afford it :)
And then there's tons of things that creep me out a bit. Being a multi paradigm language sounds good on paper, but it seems to cause a lot of accidental complexity. The best way to stay sane is probably to pick a certain subset of C++ for your project and stick to that, that's what I tend to do.
Then again, that's not uncommon in simpler languages either. "JavaScript, The Good Parts", lint and all that. Code accessibility matters IMO.