Joel Spolsky characterized C++ as “enough rope to hang yourself”. Actually, he was summarizing “Effective C++” by Scott Meyers:
It’s a book that basically says, C++ is enough rope to hang yourself, and then a couple of extra miles of rope, and then a couple of suicide pills that are disguised as M&Ms…
I don’t have a copy of the book, but there are indications that much of the book relates to pitfalls of managing memory which seem like would be rendered moot in C# because the runtime manages those issues for you.
Here are my questions:
- Does C# avoid pitfalls that are avoided in C++ only by careful programming? If so, to what degree and how are they avoided?
- Are there new, different pitfalls in C# that a new C# programmer should be aware of? If so, why couldn’t they be avoided by the design of C#?
7
The fundamental difference between C++ and C# stems from undefined behavior.
It has nothing to do with doing manual memory management. In both cases, that is a solved problem.
C/C++:
In C++, when you make a mistake, the result is undefined.
Or, if you try to make certain kinds of assumptions about the system (e.g. signed integer overflow), chances are that your program will be undefined.
Maybe read this 3-part series on undefined behavior.
This is what makes C++ so fast — the compiler doesn’t have to worry about what happens when things go wrong, so it can avoid checking for correctness.
C#, Java, etc.
In C#, you’re guaranteed that many mistakes will blow up in your face as exceptions, and you’re guaranteed a lot more about the underlying system.
That is a fundamental barrier to making C# as fast as C++, but it’s also a fundamental barrier to making C++ safe, and it makes C# easier to work with and debug.
Everything else is just gravy.
9
Does C# avoid pitfalls that are avoided in C++ only by careful programming? If so, to what degree and how are they avoided?
Most it does, some it doesn’t. And of course, it makes some new ones.
-
Undefined behavior – The biggest pitfall with C++ is that there’s a whole lot of the language that is undefined. The compiler can literally blow up the universe when you do these things, and it will be okay. Naturally, this is uncommon, but it is pretty common for your program to work fine on one machine and for really no good reason not work on another. Or worse, subtly act different. C# has a few cases of undefined behavior in its specification, but they’re rare, and in areas of the language that are infrequently traveled. C++ has the possibility of running into undefined behavior every time you make a statement.
-
Memory Leaks – This is less of a concern for modern C++, but for beginners and during about half of its lifetime, C++ made it super easy to leak memory. Effective C++ came right around the evolution of practices to eliminate this concern. That said, C# can still leak memory. The most common case people run into is event capture. If you have an object, and put one of its methods as a handler to an event, the owner of that event needs to be GC’d for the object to die. Most beginners don’t realize that event handler counts as a reference. There’s also issues with not disposing disposable resources that can leak memory, but these are not nearly as common as pointers in pre-Effective C++.
-
Compilation – C++ has a retarded compilation model. This leads to a number of tricks to play nice with it, and keep compile times down.
-
Strings – Modern C++ makes this a little better, but
char*
is responsible for ~95% of all security breaches before the year 2000. For experienced programmers, they’ll focus onstd::string
, but it’s still something to avoid and a problem in older/worse libraries. And that’s praying that you don’t need unicode support.
And really, that’s the tip of the iceberg. The main issue is that C++ is a very poor language for beginners. It is fairly inconsistent, and many of the old really, really bad pitfalls have been dealt with by changing the idioms. The problem is that beginners then need to learn the idioms from something like Effective C++. C# eliminates a lot of these problems altogether, and makes the rest less of a concern until you get further along the learning path.
Are there new, different pitfalls in C# that a new C# programmer should be aware of? If so, why couldn’t theybe avoided by the design of C#?
I mentioned the event “memory leak” issue. This isn’t a language problem so much as the programmer expecting something that the language can’t do.
Another is that the finalizer for a C# object is not technically guaranteed to be run by the runtime. This doesn’t usually matter, but it does cause some things to be designed differently than you might expect.
Another semi-pitfall I’ve seen programmers run into is the capture semantics of anonymous functions. When you capture a variable, you capture the variable. Example:
List<Action> actions = new List<Action>();
for(int x = 0; x < 10; ++x ){
actions.Add(() => Console.WriteLine(x));
}
foreach(var action in actions){
action();
}
Doesn’t do what naively is thought. This prints 10
10 times.
I’m sure there’s a number of others I am forgetting, but the main issue is that they’re less pervasive.
15
In my opinion, the dangers of C++ are somewhat exaggerated.
The essential danger is this: While C# lets you perform “unsafe” pointer operations using the unsafe
keyword, C++ (being mostly a superset of C) will let you use pointers whenever you feel like it. Besides the usual dangers inherent with using pointers (which are the same with C), like memory-leaks, buffer overflows, dangling pointers, etc., C++ introduces new ways for you to seriously screw things up.
This “extra rope”, so to speak, which Joel Spolsky was talking about, basically comes down to one thing: writing classes which internally manage their own memory, also known as the “Rule of 3” (which can now be called the Rule of 4 or Rule of 5 in C++11). This means, if you ever want to write a class that manages its own memory allocations internally, you have to know what you’re doing or else your program will likely crash. You have to carefully create a constructor, copy constructor, destructor, and assignment operator, which is surprisingly easy to get wrong, often resulting in bizarre crashes at runtime.
HOWEVER, in actual every-day C++ programming, it’s very rare indeed to write a class that manages its own memory, so it’s misleading to say that C++ programmers always need to be “careful” to avoid these pitfalls. Usually, you’ll just be doing something more like:
class Foo
{
public:
Foo(const std::string& s)
: m_first_name(s)
{ }
private:
std::string m_first_name;
};
This class looks pretty close to what you’d do in Java or C# – it requires no explicit memory management (because the library class std::string
takes care of all that automatically), and no “Rule of 3” stuff is required at all since the default copy constructor and assignment operator is fine.
It’s only when you try to do something like:
class Foo
{
public:
Foo(const char* s)
{
std::size_t len = std::strlen(s);
m_name = new char[len + 1];
std::strcpy(m_name, s);
}
Foo(const Foo& f); // must implement proper copy constructor
Foo& operator = (const Foo& f); // must implement proper assignment operator
~Foo(); // must free resource in destructor
private:
char* m_name;
};
In this case, it can be tricky for novices to get the assignment, destructor and copy constructor correct. But for most cases, there’s no reason to ever do this. C++ makes it very easy to avoid manual memory management 99% of the time by using library classes like std::string
and std::vector
.
Another related issue is manually managing memory in a way that doesn’t take into account the possibility of an exception being thrown. Like:
char* s = new char[100];
some_function_which_may_throw();
/* ... */
delete[] s;
If some_function_which_may_throw()
actually does throw an exception, you’re left with a memory leak because the memory allocated for s
will never be reclaimed. But again, in practice this is hardly an issue any more for the same reason that the “Rule of 3” isn’t really much of a problem anymore. It’s very rare (and usually unnecessary) to actually manage your own memory with raw pointers. To avoid the above problem, all you’d need to do is use an std::string
or std::vector
, and the destructor would automatically get invoked during stack unwinding after the exception was thrown.
So, a general theme here is that many C++ features which were not inherited from C, such as automatic initialization/destruction, copy constructors, and exceptions, force a programmer to be extra careful when doing manual memory management in C++. But again, this is only a problem if you intend to do manual memory management in the first place, which is hardly ever necessary anymore when you have standard containers and smart pointers.
So, in my opinion, while C++ gives you a lot of extra rope, it’s hardly ever necessary to use it to hang yourself, and the pitfalls which Joel was talking about are trivially easy to avoid in modern C++.
8
I wouldn’t really agree. Maybe less pitfalls than C++ as it existed in 1985.
Does C# avoid pitfalls that are avoided in C++ only by careful
programming? If so, to what degree and how are they avoided?
Not really. Rules like the Rule of Three have lost massive significance in C++11 thanks to unique_ptr
and shared_ptr
being Standardised. Using the Standard classes in a vaguely sensible fashion isn’t “careful coding”, it’s “basic coding”. Plus, the proportion of the C++ population who are still sufficiently stupid, uninformed, or both to do things like manual memory management is a lot lower than before. The reality is that lecturers who wish to demonstrate rules like that have to spend weeks trying to find examples where they still apply, because the Standard classes cover virtually every use case imaginable. Many Effective C++ techniques have gone the same way- the way of the dodo. Lots of the others aren’t really that C++ specific. Let me see. Skipping the first item, the next ten are:
- Don’t code C++ like it’s C. This is really just common sense.
- Restrict your interfaces and use encapsulation. OOP.
- Two-phase initialization code writers should be burned at the stake. OOP.
- Know what value semantics are. Is this really C++ specific?
- Restrict your interfaces again, this time in a slightly different way. OOP.
- Virtual destructors. Yeah. This one is probably still valid- somewhat.
final
andoverride
have helped change this particular game for the better. Make your destructoroverride
and you guarantee a nice compiler error if you inherit from someone who didn’t make their destructorvirtual
. Make your classfinal
and no poor scrub can come along and inherit from it accidentally without a virtual destructor. - Bad things happen if cleanup functions fail. This isn’t really specific to C++- you can see the same advice for both Java and C#- and, well, pretty much every language. Having cleanup functions that can fail is just plain bad and there’s nothing C++ or even OOP about this item.
- Be aware of how constructor order influences virtual functions. Hilariously, in Java (either current or past) it would simply incorrectly call the Derived class’s function, which is even worse than C++’s behaviour. Regardless, this issue is not specific to C++.
- Operator overloads should behave as people expect. Not really specific. Hell, it’s hardly even operator overloading specific, the same could be applied to any function- don’t give it one name and then make it do something completely unintuitive.
- This is actually now considered bad practice. All strongly-exception-safe assignment operators deal with self-assignment just fine, and self-assignment is effectively a logical program error, and checking for self-assignment just isn’t worth the performance cost.
Obviously I’m not going to go through every single Effective C++ item, but most of them are simply applying basic concepts to C++. You would find the same advice in any value-typed object-orientated overloadable-operator language. Virtual destructors is about the only one that’s a C++ pitfall and is still valid- although, arguably, with the final
class of C++11, it’s not as valid as it was. Remember that Effective C++ was written when the idea of applying OOP, and C++’s specific features, was still very new. These items are hardly about C++’s pitfalls and more about how to cope with the change from C and how to use OOP correctly.
Edit: The pitfalls of C++ do not include things like the pitfalls of malloc
. I mean, for one, every single pitfall you can find in C code you can equally find in unsafe C# code, so that’s not particularly relevant, and secondly, just because the Standard defines it for interoperation does not mean that using it is considered C++ code. The Standard defines goto
as well, but if you were to write a giant pile of spaghetti mess using it, I’d consider that your problem, not the language’s. There’s a big difference between “careful coding” and “Following basic idioms of the language”.
Are there new, different pitfalls in C# that a new C# programmer
should be aware of? If so, why couldn’t theybe avoided by the design
of C#?
using
sucks. It really does. And I have no idea why something better wasn’t done. Also, Base[] = Derived[]
and pretty much every use of Object, which exists because the original designers failed to notice the massive success that templates were in C++, and decided that “Let’s just have everything inherit from everything and lose all our type safety” was the smarter choice. I also believe that you can find some nasty surprises in things like race conditions with delegates, and other such fun. Then there’s other general stuff, like how generics suck horrifically in comparison to templates, the really really unnecessary enforced placing of everything in a class
, and such things.
5
Does C# avoid pitfalls that are avoided in C++ only by careful
programming? If so, to what degree and how are they avoided?
C# has the advantages of:
- Not being backwards-compatible with C, thus avoiding having a long list of “evil” language features (e.g., raw pointers) that are syntactically convenient but now considered bad style.
- Having reference semantics instead of value semantics, which makes at least 10 of the Effective C++ items moot (but introduces new pitfalls).
- Having less implementation-defined behavior than C++.
- In particular, in C++ the character encoding of
char
,string
, etc. is implementation-defined. The schism between the Windows approach to Unicode (wchar_t
for UTF-16,char
for obsolete “code pages”) and the *nix approach (UTF-8) causes great difficulties in cross-platform code. C#, OTOH, guarantees that astring
is UTF-16.
- In particular, in C++ the character encoding of
Are there new, different pitfalls in C# that a new C# programmer
should be aware of?
Yes: IDisposable
Is there an equivalent book to “Effective C++” for C#?
There’s a book called Effective C# which is similar in structure to Effective C++.
No, C# (and Java) are less safe than C++
C++ is locally verifiable. I can inspect a single class in C++ and determine that the class does not leak memory or other resources, assuming that all the referenced classes are correct. In Java or C#, it is necessary to check every referenced class to determine if it requires finalization of some sort.
C++:
{
some_resource r(...); // resource initialized
...
} // resource destructor called, no leaks here
C#:
{
SomeResource r = new SomeResource(...); // resource initialized
...
} // did I need to finalize that? May I should have used 'using'
// (or in Java, a grotesque try/finally construct)? No way to tell
// without checking the documentation for SomeResource
C++:
{
auto_ptr<SomeInterface> i = SomeFactory.create(...);
i->f(...);
} // automatic finalization and memory release. A new implementation of
// SomeInterface can allocate and free resources with no impact
// on existing code
C#:
{
SomeInterface i = SomeFactory.create(...);
i.f(...);
...
} // Sure hope someone didn't create an implementation of SomeInterface
// that requires finalization. In C# and Java it is necessary to decide whether
// any implementation could require finalization when the interface is defined.
// If the initial decision is 'no finalization', then no future implementation
// can acquire any resource without creating potential leaks in existing code.
10
Yes 100% yes as i think its impossible to free memory and use it in C# (assuming its managed and you don’t go into unsafe mode).
But if you know how to program in C++ which an unbelievable number of people don’t. You’re pretty much fine. Like Charles Salvia classes don’t really manage their memories as it all is handled in preexisting STL classes. I rarely use pointers. In fact i went projects without using a single pointer. (C++11 makes this easier).
As for making typos, silly mistakes and etc (ex: if (i=0)
bc the key got stuck when you hit == really quickly) the compiler complains which is nice as it improves quality of code. Other example are forgetting break
in switch statements and not allowing you to declare static variables in a function (which i dislike sometimes but is a good idea imo).
4