Advertisement

How many of you use C for game programming?

Started by January 24, 2011 04:33 AM
107 comments, last by Washu 13 years, 6 months ago

I've used both C and C++, but prefer to restrict myself to the C-like subset of C++ with maybe a few C++ specific features here and there. I don't subscribe to the thinking that "if you're not using STL/new and delete/etc you're not using C++" and I've seen enough C++ code that focusses more on designing pretty class structures than on actually getting the job done.

One reason I can think of to prefer C is that it compiles faster.


So what do you use for collections? How do you manage memory? If you're focused on "getting the job done", why not use a proven set of tested tools?

While C++ compile times are a pain, I would argue that they are less painful than the alternative. That said, I wouldn't use C or C++ if I had the choice.
if you think programming is like sex, you probably haven't done much of either.-------------- - capn_midnight
[quote name='mhagain' timestamp='1300050170' post='4785347']I've used both C and C++, but prefer to restrict myself to the C-like subset of C++ with maybe a few C++ specific features here and there. I don't subscribe to the thinking that "if you're not using STL/new and delete/etc you're not using C++" and I've seen enough C++ code that focusses more on designing pretty class structures than on actually getting the job done.
So what do you use for collections? How do you manage memory? If you're focused on "getting the job done", why not use a proven set of tested tools?[/quote]I'm trying to "get a job done" at the moment, which involves the very strict requirement that no globals be used.
C++'s new/delete (and C's malloc/free) unfortunately are (usually) globals - they provide access to a global memory allocator object. This makes them unusable to me - it also makes large parts of the STL unusable.

The easiest way to get the job done is to simply get on with it and KISS (if passing strings around as char* turns out to be the simplest option in a certain situation, then so be it). Massaging the STL to work in this environment would be possible, but it's a whole bag of complexity I don't need. I am using C++, just very differently than it "should" be used.

The result of this may be "ugly" code, but it's often very shallow in depth (easy to read, explore and understand), doesn't require chasing of abstractions, is more easily documented and has extremely minimal dependencies on other parts of the code base. It's still well engineered, it's just different.

As usual, there's no absolute answers, just circumstances.

I should make a video on youtube that starts with the quote 'Trust Me, I can manage my own memory!", followed my a non stop montage of memory leaks, blue screens, general protection faults, games that were released in a state that they could only ever crash to desktop, buffer overflow exploits (the entire C standard library needed to be deprecated and replaced with safer alternative functions to remedy this), air traffic control failures, road sign failures, etc, etc, to this music.
To provide a totally balanced counter-point, you should also make a video on youtube that starts with the quote 'Trust Me, I can manage your memory!", followed by the Java loading icon animating for 10 minutes ;)
Advertisement

[quote name='mhagain' timestamp='1300050170' post='4785347']
I've used both C and C++, but prefer to restrict myself to the C-like subset of C++ with maybe a few C++ specific features here and there. I don't subscribe to the thinking that "if you're not using STL/new and delete/etc you're not using C++" and I've seen enough C++ code that focusses more on designing pretty class structures than on actually getting the job done.

One reason I can think of to prefer C is that it compiles faster.


So what do you use for collections? How do you manage memory? If you're focused on "getting the job done", why not use a proven set of tested tools?

While C++ compile times are a pain, I would argue that they are less painful than the alternative. That said, I wouldn't use C or C++ if I had the choice.
[/quote]


If you find that your biggest time sink is memory management and collections, you have bigger problems. Besides, in my experience, STL has it's own issues.


1. Say you are trying to optimize some critical execution paths. You find some areas where an std::vector is created, populated, and passed around to a few methods for manipulation before handing it off to a library. For whatever reason, the vector would be more efficiently allocated on the stack, as certain characteristics are known. Great, you just need to write a custom allocator and you're all set, right? Not quite...unfortunately, the newly defined std::vector type that makes use of the custom allocator will no longer be compatible with the type required by the library call.


2. STL collections are not thread safe. Usually this is acceptable, as in the worst case scenario, collections can be made thread safe via some sort of synchronization. However, this may not be optimal. Consider a linked list. In the optimal case, the mutex only needs to be acquired for a very brief moment -- when assigning the next/prev/head pointers. The allocation and initialization of the node/type can be done outside of the lock. Using an std::list, this isn't possible. The mutex needs to be wrapped around the call to list.push_back (or whatever method you use), which means you are locking needlessly over the duration of the memory allocation and initialization of the type. In some cases, this is acceptable, in others, it's not.


3. The rules for iterators are a pain. For example, when are iterators invalidated for a linked list? What about a deque? What about a map? What about a vector? Yea, it's very difficult to keep track of, and have encountered way too many bugs as a result. Also, this makes it very tricky to just 'change' the container type.


4. Using the STL adds lots and lots of line noise to your source code. You either need to riddle your code with typedefs, or live with never-ending std::map<std::string, std::pair<std::vector<foo>, int> > lines. Yea, I hear C++0x introduces 'auto' to 'fix' that, but that's just a whole other can of worms. C++0x has its own problems. And still, 'auto' won't save you from the 20 line template compiler errors.


5. While not necessarily a flaw in the STL itself, I've found that using the STL encourages a 'lazy' atmosphere. I can't count the number of times I've seen code such as:

std::map<> m;

if(m.find(foo) != m.end())
use(m[foo]);


Or looping and doing collection.push_back() without reserving the length (again, sometimes acceptable, sometimes not.)


6. STL (or maybe this belongs more in the C++ critiques) has spawned an entire cult of syntax optimizers. With C++, very very subtle syntax style can negatively effect performance. Take this rather basic example:


for(std::vector<>::iterator it= v.begin(); it != v.end(); it++){} // Where's the performance problem?





C++ *is* a terrible language, and the STL doesn't save it. C++ requires loads and loads of context just to fully understand simple snippets of code. You need to examine methods, determine if those methods are being overloaded, make sure those '+' signs aren't overloaded, decide how member variables are shared between class hierarchies (multiple virtual inheritance anyone?), figure out if any exceptions might be thrown that yank you out of the code path you think is being executed, etc, etc.

in my experience, STL has it's own issues.
Those are some pretty weak issues.

1. No, you refactor the consumers to take begin/end pointers instead of a container. KISS.

2. A thread-safe container would be an even worse choice for a general-purpose container. If you're using a general-purpose container as an inter-thread communication tool, then you obviously don't care about performance.

3. How often do you want to just change a map to a list to a vector without expecting to have to refactor your design? That's like renaming a PNG to WAV and expecting to hear a picture.
If I have a pointer to an item in a custom array or a custom linked list, what's the invalidation rules for that pointer? This is the exact same problem (iterators/pointers into any container have the invalidation problem)!
If every container API has this problem, then at least the STL is kind enough to make the rules very clear-cut and easy to remember.

4. The occasional 1 line typedef isn't "lots and lots" of noise in my book. That's a pretty subjective / style-based call.

5. You've seen retarded STL usage, therefore the STL causes retardation? I've seen bad programmers write retarded code with pretty much every API I've had to use.

6. Again, this is something that separates a junior C++ programmer from an experienced one. Not an STL issue. Plus an optimising compiler can fix this mistake in simple cases.

C++ *is* a terrible language, and the STL doesn't save it. C++ requires loads and loads of context just to fully understand simple snippets of code. You need to examine methods, determine if those methods are being overloaded, make sure those '+' signs aren't overloaded, decide how member variables are shared between class hierarchies (multiple virtual inheritance anyone?), figure out if any exceptions might be thrown that yank you out of the code path you think is being executed, etc, etc.[/quote]That's why every C++ project usually decides on a sub-set of the language that they're going to use. I've never seen a design incorporating multiple virtual inheritance ever actually get chosen over something simpler. A lot of projects outright ban virtual and multiple inheritance.
Also, I've never seen exceptions used in a game project. Ever. You can simply choose not to take that level of complexity on board, and it's actually recommended practice (by the console compiler authors) that you do not enable exception handling.

As for the context argument - you can make the same case against any language... If I have a C function called [font="Lucida Console"]add[/font], do I have to go and make sure that it actually adds things? Overloading operators to do non-obvious things is simply bad code - I can write bad code in any language (e.g. a Java class [font="Lucida Console"]FileOpener[/font] than actually deletes files).This isn't an anti-C++ argument; it's an anti-bad-code argument.

C++ is just like democracy. It's the worst systems engineering language, except all the others that have been tried.
Also, yes, C/C++ are systems languages. If you've got junior programs breaking your game with their bad code, then you should've seen it coming and written your gameplay in Lua/etc while leaving C/C++ at the bottom of the system stack ;)

1. No, you refactor the consumers to take begin/end pointers instead of a container. KISS.


You've assumed you have control over the library, and you *can* just refactor. If you're the sole developer, this may a reasonable solution. Often times it's not.




2. A thread-safe container would be an even worse choice for a general-purpose container. If you're using a general-purpose container as an inter-thread communication tool, then you obviously don't care about performance. [/quote]

I never claimed this wasn't the case. I was interested in the cases where you do need a thread safe container. If you've ever dealt with bottlenecks due to contention, you'd understand that there are times when holding the lock while initializing / allocating memory is completely unacceptable. You'd end up needing to roll-your-own container anyways just to fix the contention problems.




3. How often do you want to just change a map to a list to a vector without expecting to have to refactor your design? That's like renaming a PNG to WAV and expecting to hear a picture.
If I have a pointer to an item in a custom array or a custom linked list, what's the invalidation rules for that pointer? This is the exact same problem (iterators/pointers into any container have the invalidation problem)!
If every container API has this problem, then at least the STL is kind enough to make the rules very clear-cut and easy to remember.[/quote]

I agree it's not often you just want to change your container type. I only brought it up because I've heard people claim it to be an advantage of C++. My intention with this point was to highlight that the rules for iterators and containers are vast. It's very easy to make assumptions and make mistakes. Just look at the rules for iterators with regards to std::vector: Insertion will invalidate all iterators, unless those iterators are prior to the point of insertion. However! If the std::vector::capacity() isn't sufficient, they are all invalidated. Then you move on to std::deque, and discover there's an entirely new set of rules wrt iterators. It's a lot of rules to remember, and very easy to make mistakes. I've seen it, even from good programmers.



4. The occasional 1 line typedef isn't "lots and lots" of noise in my book. That's a pretty subjective / style-based call.[/quote]

It doesn't sound like you've spent a lot of time working with large code bases. Have you ever had the pleasure of chasing down a trail of typedefs just to determine the type used to define a map? Sure, with C you might have to chase down a few types once in a while, but with C++...the chase is much longer.



5. You've seen retarded STL usage, therefore the STL causes retardation? I've seen bad programmers write retarded code with pretty much every API I've had to use.[/quote]

I thought I was clear when I said: "While not necessarily a flaw in the STL itself". My primary claim was that STL encourages a 'lazy atmosphere'. Again, it's not just bad programmers who do it. When modifying code, it's very easy to jump into a file...and find an already existing conditional that reads:




if(m.find(foo) != m.end())

{

/* big chunk of code */

}


A developer tracks down a bug in the big chunk of code, and makes a change similar to what I described. Obviously you can point out the error, but often it goes unnoticed simply by accident. Again, have you worked in large teams on large projects with unfamiliar code bases before?

6. Again, this is something that separates a junior C++ programmer from an experienced one. Not an STL issue. Plus an optimising compiler can fix this mistake in simple cases.[/quote]


A compiler can absolutely NOT fix that mistake. Not even the best compiler can say 'i++ should be ++i'. These are two different function calls. So now, with C++, you are stuck paying attention to insane details that no developer should have to care about.


That's why every C++ project usually decides on a sub-set of the language that they're going to use. I've never seen a design incorporating multiple virtual inheritance ever actually get chosen over something simpler. A lot of projects outright ban virtual and multiple inheritance.
Also, I've never seen exceptions used in a game project. Ever. You can simply choose not to take that level of complexity on board, and it's actually recommended practice (by the console compiler authors) that you do not enable exception handling.[/quote]

Yes, you can agree on a subset...but as the project continues, team members quit / new people come on board...it becomes harder and harder to enforce the specific subset you agree with. And I think you missed my point. My point wasn't that virtual multiple inheritance should be avoided, it's that you need extra context to determine multiple virtual inheritance wasn't used. With C, you can often take a look at a single snippet of code (say a patch file) and can give a pretty good estimate of what it's doing. You can easily find the function calls, and can likely give a good estimate of it's runtime. With C++, this is absolutely not the case. Often times significantly more context is needed (header files to determine what's virtual, what's overloaded, where you're inheriting from, etc, etc). Actually, I believe this is accepted common knowledge.

As for the context argument - you can make the same case against any language... If I have a C function called [font="Lucida Console"]add[/font], do I have to go and make sure that it actually adds things? Overloading operators to do non-obvious things is simply bad code - I can write bad code in any language (e.g. a Java class [font="Lucida Console"]FileOpener[/font] than actually deletes files).This isn't an anti-C++ argument; it's an anti-bad-code argument.[/quote]

Yes, I agree...any language can have problems. I think you're crazy to deny that C++ tends to require more context than C. I feel there's already a good thread on this subject, involving Linus Torvalds and a few other prominent developers who can discuss the finer points better than me: http://www.realworld...110549&roomid=2

One simple example Linus gives:

when you communicate in fragments (think "patches"), it's always better to see "sctp_connect()" than to see just "connect()" where some unseen context is what makes the compiler know that it is in the sctp module.


[/quote]

C++ is hard for a compiler to parse, and even harder for a human being.





C++ is just like democracy. It's the worst systems engineering language, except all the others that have been tried.
Also, yes, C/C++ are systems languages. [/quote]

Funny, I've worked on device drivers for embedded devices. Many device drivers, actually. They were all in C. No one had any desire to transition to C++. C was wonderful because it is absolutely straight forward and simple. The simplicity of C is a strength of the language, not a weakness.

A compiler can absolutely NOT fix that mistake. Not even the best compiler can say 'i++ should be ++i'.[/quote]
They don't need to change the function call. These are templates, the compiler will probably inline either function and figure out what to do from there.

Sample program:

#include <vector>
#include <iostream>

int main()
{
std::vector<int> v;
int i;
while(std::cin >> i) {
v.push_back(i);
}


// Print post
for(std::vector<int>::iterator it = v.begin(); it != v.end(); it++) {
printf("%d\n", *it);
}

// Print pre
for(std::vector<int>::iterator it= v.begin(); it != v.end(); ++it) {
printf("%d\n", *it);
}
}

Lets examine the compiler asm output (MSVC 2010, Release mode):

; 13 : // Print post
; 14 : for(std::vector<int>::iterator it = v.begin(); it != v.end(); it++) {

mov ebx, edi
cmp edi, esi
je SHORT $LN4@main
npad 4
$LL148@main:

; 15 : printf("%d\n", *it);

mov ecx, DWORD PTR [ebx]
push ecx
push OFFSET $SG-31
call _printf
add ebx, 4
add esp, 8
cmp ebx, esi
jne SHORT $LL148@main
$LN4@main:

; 16 : }
; 17 :
; 18 : // Print pre
; 19 : for(std::vector<int>::iterator it= v.begin(); it != v.end(); ++it) {

mov ebx, edi
cmp edi, esi
je SHORT $LN1@main
npad 3
$LL178@main:

; 20 : printf("%d\n", *it);

mov edx, DWORD PTR [ebx]
push edx
push OFFSET $SG-32
call _printf
add ebx, 4
add esp, 8
cmp ebx, esi
jne SHORT $LL178@main
$LN1@main:

; 21 : }

I'm not spotting this performance problem you're talking about...
Advertisement
Note that I'm not attacking C here - I was just pointing out that your issues with the STL were all pretty weak. Only the first one was actually a problem, and it was demonstrated though a spurious example.

You've assumed you have control over the library, and you *can* just refactor. If you're the sole developer, this may a reasonable solution. Often times it's not.
Sorry, I misread your statement as if you were already refactoring the library to take a new type of vector.
The fact that containers shouldn't be used like this is a valid issue with the STL, but because of this, a library that requires a specific kind of container to be passed in is possibly badly designed (note how STL algorithms all operate on iterators/pointers, not containers). This means your example would never happen in the real world, as the library wouldn't take a vector as an argument.
If you've ever dealt with bottlenecks due to contention, you'd understand that there are times when holding the lock while initializing / allocating memory is completely unacceptable. You'd end up needing to roll-your-own container anyways just to fix the contention problems.[/quote]Yeah I have dealt with this problem, and the solution was to use a wait-free structure instead of one that requires mutual exclusion whatsoever. If you're using a mutex in performance critical code, you're doing it wrong.
It doesn't sound like you've spent a lot of time working with large code bases. Have you ever had the pleasure of chasing down a trail of typedefs just to determine the type used to define a map? Sure, with C you might have to chase down a few types once in a while, but with C++...the chase is much longer.[/quote]I've worked on engines like Unreal and Gamebryo, if they count as "large code bases", and no I don't find the over-abuse of typedefs to be an everyday problem caused by C++.
Sounds like you've never worked on a large, well-run C++ project using decent tools?
A developer tracks down a bug in the big chunk of code, and makes a change similar to what I described. Obviously you can point out the error, but often it goes unnoticed simply by accident. Again, have you worked in large teams on large projects with unfamiliar code bases before?[/quote]Yes, and bad code like this occurred in C projects, C++ projects, Lua projects, etc... This isn't specific to C++, or to the STL.
A compiler can absolutely NOT fix that mistake.[/quote]I was going to post an assembly dump to demonstrate this, but I've been beaten to it ;) Sounds like you need to upgrade your compiler.
My point wasn't that virtual multiple inheritance should be avoided, it's that you need extra context to determine multiple virtual inheritance wasn't used.[/quote]Well as I said before, on a specific project with specific guidelines, you can assume it wasn't used, because it would've failed a code review if it was used.
With C, you can often take a look at a single snippet of code (say a patch file) and can give a pretty good estimate of what it's doing. You can easily find the function calls, and can likely give a good estimate of it's runtime. With C++, this is absolutely not the case.[/quote]Get a modern IDE, then it is the case.
I think you're crazy to deny that C++ tends to require more context than C.[/quote]I'm not denying it, I'm denying that it poses any kind of problem or hindrance (unless you're coding in the 80's).
C++ is just like democracy. It's the worst systems engineering language, except all the others that have been tried.[/quote]Funny, I've worked on device drivers for embedded devices. Many device drivers, actually. They were all in C. No one had any desire to transition to C++. C was wonderful because it is absolutely straight forward and simple. The simplicity of C is a strength of the language, not a weakness.[/quote]Your C code would likely be accepted by a C++ compiler though ;D So it's a C++ project with a restrictive coding guideline (yes, j/k).
Again I'm not attacking C - see my post above where I'm advocating the use of [font="Lucida Console"]char*[/font] instead of [font="Lucida Console"]std::string[/font] etc...(in some circumstances)
Yes, simplicity can be a very good thing.
They don't need to change the function call. These are templates, the compiler will probably inline either function and figure out what to do from there.


[quote name='Hodgman']I was going to post an assembly dump to demonstrate this, but I've been beaten to it ;) Sounds like you need to upgrade your compiler.[/quote]


I think you're both confused. A compiler is never allowed to change 'i++ to ++i'. It may be that when the definition of the post-increment / pre-increment operators are available, the compiler can more aggressively optimize the functions through inlining and unused variable elimination. However, this doesn't change the fact that in one case, the compiler is optimizing the call to ++i, and in the other case, the compiler is optimizing the call to i++. In the case of a vector, the methods for either implementation are simple enough that the compiler can optimize them both to the point of identical assembly.

As the iterators become more complicated for the compiler to analyze, or, if the definition is not available to the compiler...you may see less optimal code due using post/pre increment inappropriately.

My point remains, the compiler can never substitute ++i for i++. C++ forces you to pay attention to silly, subtle syntax.
in one case, the compiler is optimizing the call to ++i, and in the other case, the compiler is optimizing the call to i++. In the case of a vector, the methods for either implementation are simple enough that the compiler can optimize them both to the point of identical assembly.
Exactly
My point remains, the compiler can never substitute ++i for i++. C++ forces you to pay attention to silly, subtle syntax.[/quote]But it just did! It took code that was requesting "i++" to be performed (increment and return previous), but the compiler decided that a simple increment (with no return) would work just as well. In other words "an optimising compiler can fix this mistake in simple cases".

It's not silly syntax - they're two completely different operations. If you were to implement STL's vectors/iterators in C, you'd get something like:struct Iterator {...};
struct Vector {...};
Vector vec;
for( Iterator it = Begin(vec), end = End(vec); !IteratorsEqual(it,end); IncrementIterator_ReturnNewValue(&it) )
{
}
for( Iterator it = Begin(vec), end = End(vec); !IteratorsEqual(it,end); IncrementIterator_ReturnPreviousValue(&it) )
{
}
Of course in reality you'd just use a raw-pointer instead of some abstract iterator data type, in which case p++ or ++p will optimise to the same assembly in both C and C++.... Often in C++ you'd use a pointer instead of an actual iterator class too.

I should make a video on youtube that starts with the quote 'Trust Me, I can manage my own memory!", followed my a non stop montage of memory leaks, blue screens, general protection faults, games that were released in a state that they could only ever crash to desktop, buffer overflow exploits (the entire C standard library needed to be deprecated and replaced with safer alternative functions to remedy this), air traffic control failures, road sign failures, etc, etc, to this music.


I think that should be the theme song to this thread. rolleyes.gif

Though, not because I think it lacks valuable information or valid arguments.

This topic is closed to new replies.

Advertisement