14

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:

  1. Does C# avoid pitfalls that are avoided in C++ only by careful programming? If so, to what degree and how are they avoided?
  2. 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
  • 10
    From the FAQ: Your questions should be reasonably scoped. If you can imagine an entire book that answers your question, you’re asking too much.. I believe this qualifies as such a question...
    – Oded
    Aug 25, 2012 at 20:27
  • @Oded Are you referring to the character-limited headline question? Or my 3+ more precise questions in the body of my post?
    – alx9r
    Aug 25, 2012 at 20:34
  • 3
    Frankly - both the title and each of the "more precise questions".
    – Oded
    Aug 25, 2012 at 20:38
  • 3
    I have started a Meta discussion about this question.
    – Oded
    Aug 30, 2012 at 20:06
  • 1
    Regarding your now deleted 3rd question, Bill Wagner's Effective C# series (now 3 books) taught me more about programming C# well than anything else I read on the subject. Martins review of EC# is right in that it can never be a direct replacement for Effective C++, but he is wrong thinking that it should be. Once you no longer have to worry about the easy mistakes, you have to move on to harder mistakes.
    – Mark Booth
    Sep 3, 2012 at 13:03

7 Answers 7

33

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
  • All undefined stuff is really defined by the implementation, so if you overflow an unsigned integer in Visual Studio, you'll get an exception if you've turned on the right compiler flags. Now I know this is what you're talking about, but it is not undefined behaviour, its just that people don't usually check for it. (same with truly undefined behaviour like operator++, it is well defined by each compiler). You could say the same with C#, only there is just 1 implementation - plenty of 'undefined behaviour' if you run in Mono - eg. bugzilla.xamarin.com/show_bug.cgi?id=310
    – gbjbaanb
    Aug 26, 2012 at 14:30
  • 1
    Is it really defined or only defined by whatever the current .net implementation on the current version of Windows does? Even c++ undefined behaviour is fully defined if you define it as whatever g++ does. Aug 26, 2012 at 14:46
  • 6
    Overflowing unsigned integers isn't UB at all. It's overflowing signed integers that's UB.
    – DeadMG
    Aug 26, 2012 at 15:47
  • 6
    @gbjbaanb: Like DeadMG said -- signed integer overflow is undefined. It's not implementation-defined. Those phrases have specific meanings in the C++ standard, and they aren't the same thing. Don't make that mistake.
    – user541686
    Aug 26, 2012 at 19:10
  • 1
    @CharlesSalvia: Uh, how exactly does "C++ make it easier to take advantage of the CPU cache" than C#? And what kind of control does C++ give you over memory that you cannot have in C#?
    – user541686
    Aug 27, 2012 at 22:09
12

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.

  1. 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.

  2. 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++.

  3. Compilation - C++ has a retarded compilation model. This leads to a number of tricks to play nice with it, and keep compile times down.

  4. 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 on std::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
  • 4
    Memory leaks are a thing of the past, and so is char*. Not to mention that you can still leak memory in C# just fine.
    – DeadMG
    Aug 25, 2012 at 23:34
  • 2
    Calling templates "glorified string pasting" is a bit much. Templates are really one of the best features of C++. Aug 25, 2012 at 23:36
  • 2
    @CharlesSalvia Sure, they are the really distinguishing feature of C++. And yes, that's perhaps an oversimplification for compilation impact. But they do disproportionally impact compile times and output size, especially if you're not careful.
    – Telastyn
    Aug 25, 2012 at 23:47
  • 2
    @deadMG certainly, though I would argue that many of the template meta-programming tricks used/needed in C++ are... better implemented via a different mechanism.
    – Telastyn
    Aug 26, 2012 at 0:05
  • 2
    @Telastyn the whole point of type_traits is to get type information at compile time so you can use this information to do things like specialize templates or overload functions in specific ways using enable_if Aug 26, 2012 at 0:12
10

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
  • It was the rule of three in C++03 and is rule of four in C++11 now.
    – DeadMG
    Aug 25, 2012 at 23:40
  • 1
    You could call it "Rule of 5" for copy constructor, move constructor, copy assignment, move assignment, and destructor. But move semantics aren't always necessary just for proper resource management. Aug 25, 2012 at 23:56
  • You don't need separate move and copy assignment. Copy-and-swap idiom can do both operators in one.
    – DeadMG
    Aug 26, 2012 at 0:02
  • 2
    It answers the question Does C# avoid pitfalls that are avoided in C++ only by careful programming?. The answer is "not really, because it's trivially easy to avoid the pitfalls Joel was talking about in modern C++" Aug 26, 2012 at 4:34
  • 1
    IMO, while high-level languages like C# or Java provide you with memory management and other stuff that is supposed to help you, it doesn't always do as supposed. You still end up looking after your code design so you leave no memory leak (which is not exactly what you would call in C++). From my experience, I find it EVEN easier to manage memory in C++ because you know that destructors will be called and in most cases they do the clean up. After all, C++ has smart pointers for cases when design does not allow for efficient memory management. C++ is great but not for dummies.
    – Pijusn
    Aug 31, 2012 at 5:11
3

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:

  1. Don't code C++ like it's C. This is really just common sense.
  2. Restrict your interfaces and use encapsulation. OOP.
  3. Two-phase initialization code writers should be burned at the stake. OOP.
  4. Know what value semantics are. Is this really C++ specific?
  5. Restrict your interfaces again, this time in a slightly different way. OOP.
  6. Virtual destructors. Yeah. This one is probably still valid- somewhat. final and override have helped change this particular game for the better. Make your destructor override and you guarantee a nice compiler error if you inherit from someone who didn't make their destructor virtual. Make your class final and no poor scrub can come along and inherit from it accidentally without a virtual destructor.
  7. 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.
  8. 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++.
  9. 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.
  10. 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
  • 5
    An educated userbase or new constructs aren't really diminishing the rope though. They're just work-arounds so less people end up hung. Though this is all good commentary on Effective C++ and its context in the language's evolution.
    – Telastyn
    Aug 26, 2012 at 0:47
  • 3
    No. It's about how a bunch of the items in Effective C++ are concepts that could equally apply to any value-typed object-orientated language. And educating the userbase to code actual C++ instead of C is definitely diminishing the rope that C++ gives you. Also, I would expect that new language constructs are diminishing the rope. It's about how just because the C++ Standard defines malloc does not mean that you should do it, any more than just because you can whore goto like a bitch means that it's rope you can hang yourself with.
    – DeadMG
    Aug 26, 2012 at 9:42
  • 3
    Using the C parts of C++ is no different to writing all your code unsafe in C#, which is just as bad. I could list every pitfall of coding C# like C as well, if you'd like.
    – DeadMG
    Aug 26, 2012 at 9:43
  • @DeadMG : so really the question should be "a C++ programmer has enough rope to hang himself as long as he is a C programmer"
    – gbjbaanb
    Aug 26, 2012 at 14:08
  • "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." Citation needed.
    – dan04
    Aug 27, 2012 at 6:24
3

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 a string is UTF-16.

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++.

1

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
  • 3
    ... it is pretty trivial in modern IDEs to determine if something inherits from IDisposable. The main problem is that you need to know to use auto_ptr (or a few of its kin). That is the proverbial rope.
    – Telastyn
    Aug 26, 2012 at 14:05
  • 3
    @Telastyn no, the point is you always use a smart pointer, unless you really know that you don't need one. In C# the using statement is just like the rope you're referring to. (ie in C++ you have to remember to use a smart pointer, why then is C# not as bad even though you have to remember to always use a using statement)
    – gbjbaanb
    Aug 26, 2012 at 14:13
  • 1
    @gbjbaanb Because the what? 5% at most of C# classes are disposable? And you know you need to dispose them if they're disposable. In C++, every single object is disposable. And you don't know if your particular instance needs to be dealt with. What happens for pointers being returned that aren't from a factory? Is it your responsibility to clean them up? It shouldn't be, but sometimes it is. And again, just because you should always use a smart pointer doesn't mean that the option not to ceases to exist. For beginners especially, this is a significant pitfall.
    – Telastyn
    Aug 26, 2012 at 14:51
  • 3
    @Telastyn: Knowing to use auto_ptr is as simple as knowing to use IEnumerable or knowing to use interfaces, or don't use floating-point for currency or such. It's a basic application of DRY. Nobody who knows the basics of how to program would make that mistake. Unlike using. The problem with using is that you have to know for every class whether or not it's Disposable (and I hope that never, ever changes), and if it's not Disposable, you automatically ban all derived classes that might have to be Disposable.
    – DeadMG
    Aug 26, 2012 at 15:46
  • 2
    kevin: Uh, your answer makes no sense. It's not C#'s fault that you're doing it wrong. You do NOT depend on finalizers in properly written C# code. If you have a field that has a Dispose method, you must implement IDisposable (the 'proper' way). If your class does that (which is the equivalent of implementing RAII for your class in C++), and you use using (which is like the smart pointers in C++), it all works perfectly. The finalizer is mostly meant to prevent accidents -- Dispose is responsible for correctness, and if you're not using it, well, that's your fault, not C#'s.
    – user541686
    Aug 26, 2012 at 20:51
0

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
  • 4
    Java and C# made the =/== problem even worse by using == for reference equality and introducing .equals for value equality. The poor programmer now has to keep track of whether an variable is 'double' or 'Double' and be sure to call the right variant. Aug 26, 2012 at 14:08
  • @kevincline +1 but in C# struct you can do == which works incredibly well since the majority of the time one would only have strings, ints and floats (ie only struct members). In my own code i never get that problem except when i want to compare arrays. I don't think i ever compare list or non struct types (string,int,float,DateTime,KeyValuePair and many others)
    – user2528
    Aug 26, 2012 at 17:37
  • 2
    Python got it right by using == for value equality and is for reference equality.
    – dan04
    Aug 27, 2012 at 2:30
  • @dan04 - How many types of equality do you think C# has? See the excellent ACCU lightning talk: Some objects are more equal than others
    – Mark Booth
    Sep 3, 2012 at 13:12

Not the answer you're looking for? Browse other questions tagged or ask your own question.