Writing a Roguelike in D, C# and C++

279 views
Skip to first unread message

Walter Landry

unread,
May 20, 2010, 8:17:25 PM5/20/10
to
Greetings Everyone,

For the last 7DRL event, I tried two new things at once: learning a new
programming language and writing my first roguelike. My first attempt
was with D, but that crashed and burned. My second attempt was with C#.
That slipped the surly bonds of earth, but it poked along more like a
747 than the SR-71 I had hoped. So for my third attempt, I went back to
my trusty old favorite C++.

Since I have seen some interest on this newsgroup about which language
to use, I am detailing my impressions of the different languages
below. I have tried to limit my observations to things that I noticed
while developing the game. Although I know C++ well, this exercise
made me see more clearly some difficulties in C++ that I had forgotten
about long ago.

Some of the issues I raise are problems with the quality of
implementation. I can only use what is available. So for the record
I am developing on 64 bit Linux (Debian testing). For D, I used the
D2 version of gdc. For C#, I used the Debian package of mono 2.4.4.
For C++, I used g++ 4.4.3 with C++0X extensions (loooove those
extensions). All three languages have decent interfaces with libtcod.

For your amusement, I have made it a bit of a rant. Some things are
trivial, some are not.

It is quite possible that I committed a grievous performance sin
in the C# code, so feel free to debug it for me ;). Using bzr

http://bazaar.canonical.com/en/

you can get the C++ and C# versions with the commands

bzr clone http://www.gps.caltech.edu/~walter/Pit_of_Hate/cxx
bzr clone http://www.gps.caltech.edu/~walter/Pit_of_Hate/csharp

and follow the instructions in README. It is not exciting as a game.
You can only run around the screen while monsters chase you. No combat
etc.

For those keeping score, sloccount says the C# code totals 979 lines
after I comment out some unused code. The equivalent C++ code totals
1150.

Cheers,
Walter Landry

D
------

The Good:
---------

1) Fast compilation

My entire project (which is admittedly not large) compiled in under
a second. This makes it easy to quickly try out incremental
changes. It felt like I was developing with the rapid-turnaround
of a dynamic language (e.g. Python or Ruby) but with the
type-safety and speed of a compiled language. Woot! This was way
cool.

2) Modules

No circular dependency issues. No preprocessor gotcha's. No
include guards. Not as clumsy or random as header files; an elegant
method for a more civilized language.

The Bad:
--------

3) Tools really, really suck

D is transforming itself from version 1 to version 2. So you have
to choose which language to learn. I chose D2, since it seemed
like a nicer language and where all the energy is. I seem not to
be the only one thinking this way.

http://www.micropoll.com/akira/mpresult/928402-256449

Unfortunately, D2 is not quite done.

The official compiler is dmd. The front end is open source, but
the back end is not. That means that, among other things, there is
no support for 64 bit Linux or Windows. This is not encouraging
for a language that has been around for over a decade.

So for 64 bit support you are forced to use either the port to gcc
(gdc) or llvm (ldc). The gdc developers seem to have neglected to
put up all of the scary warnings that ldc had, so I used gdc. I
had to compile a patched gcc myself. It was actually not that bad,
though I have compiled gcc many times in the past.

But neither of these ports have all of the updates for D2. Then
you get to try to figure out whether the bug is in your code or in
the compiler. Ah, it brings back memories of tracking down C++
compiler bugs last millennium. Fun, it is not.

Also, I could never get the debugger to work. Grumble, grumble.

4) Documentation

There are no books on D and 'D' is a terrible search term. It would
be so much better if they had kept the original name 'Mars'. The
results you do find are scant and mixed between D1 and D2. The D
website pales in comparison to a good reference website like
cplusplus.com.

5) I could not get enums to work.

6) The standard IO module is in std.stdio. Why not std.io?


Different:
----------

7) If you do not specify a default case for a switch statement, it
throws an error.


C#
-------

The Good:
---------

1) Fast Compilation

It seemed about as fast as D. I got farther along so the code got
a bit larger than with D, but the compile time is only about 1
second for the whole project. Not enough time for me to browse
r.g.r.d.

2) Modules

Same as D. All good.

3) Reflection

Monodevelop can use reflection to browse dll's and give you the
objects, constants, and function signatures. This is really handy
since the documentation for libtcod's C# bindings seems to have
disappeared down the throat of a Sarlacc. Using reflection is way
better than looking at C++ header files cluttered up with
preprocessor directives, private members, inline functions, and
template implementations.

4) Bounds checking

While developing, automatic bounds checking caught a few bugs
early. I feel like I would have caught them in C++, but only after
a confusing segfault and some quality time with valgrind. Of
course, bounds checking may have contributed to poor performance...

5) Lambda functions

Being able to easily define a function for Find() is so much nicer
than having to define a function object. Not everything from
functional languages is bad.

6) Static Variables

Static variables are much more nailed down than in C++, so I do not
have to think about it as much. Less thinking == good.

7) Automatic operator overloads

If I overload operator+, I automatically get operator+=. Niiiice.


The Bad:
--------

8) Performance

The roguelike I am writing uses variable sized glyphs. This makes
pathfinding more complicated and CPU-intensive than a more
traditional roguelike. I also wanted to use a sound model, but
that ended up being far too CPU-intensive. So performance turned
out to be important.

It was very hard to reason about performance. I felt like I was
allocating a million little things on the heap that normally get
allocated on the stack. How does that impact performance? I don't
know!

The profiler was rather unhelpful. Half of the time is spent in
C++ code which does not get profiled. But that is OK, because, in
this case, I really only cared about the performance of the C#
code. Even with that concession, the two routines using the most
time were 'memset' with 17% and '/usr/bin/mono' with 15%.
Everything else was less than 10%. Thanks for nothing.

9) Memory leak

I got a serious memory leak that pretty quickly ate up all memory.
I was creating a Dictionary of DjikstraPath's. DjikstraPath has to
be Dispose'd, but I could not use 'using' because Dictionary's are
not Disposable. Disposing of each DjikstraPath independently
caused mysterious crashes. So I ended up using a different
algorithm. How I long for destructors.

10) Everything is a class

C# requires everything to be inside a class. There are no free
functions, and sometimes not everything is an object. For a far
more thorough and entertaining rant about this, you can read

http://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html

11) Incremental compilation is unwieldy

It seems that I have to make the equivalent of a shared library in
order to get any sort of incremental compilation. I guess this is
the downside of not having to worry about circular dependencies.
This is not such a big deal since the compiler is so fast.

12) Files have to have namespaces

You have to have a namespace for each class. This is great if my
roguelike ever gets large enough to require this kind of partioning
(we all have such dreams...), but for small projects it is a bit
overbearing.

13) Debugging

I could not get the debugger mdb to work. So I got to spend far
too much time with System.Console.WriteLine (couldn't they have
made it a little shorter?).

14) I can not make my own 'built-in' type

I want a 2D Point object that uses natural notation. Something like

Pos p(1,1);
Pos q=p+Pos(3,2);

C# separates value based objects (structs) from reference based
objects (classes). So it would seem natural to use a struct. But
you still have to use 'new' when creating an instance of a struct.
This changes the previous example into

Pos p=new Pos(1,1);
Pos q=p+(new Pos(3,2);

Natural, it is not.

15) Equals is complicated

Overloading operator== means that you also have to overload !=,
isEquals, and HashCode. Really farkin' annoying.

16) Can not overload [] or ()

So to make a fancy vector class, you have to use unnatural notation
like array.at(x,y).

17) Emacs mode

The emacs csharp mode does not handle multiple case's in a row. It
indents the break in the wrong place.

18) No multimap or multiset

19) Non-Generic Collections

C# started with non-generic collections, which have now been
superseded by generic collections. It looks like it is time to
trigger Hejlberg's garbage collector.

20) Collections are limited

I have an event queue that stores @ and all of the monsters. It is
sorted by the time when they can next act. So it might look like

Time Entity
---- ------
2 B
2.2 @
2.3 d
2.3 c
4.5 A

'A' just cast a spell that takes a long time to recover, so it is
currently B's turn. Note that both 'd' and 'c' have the same time.
This simulates 'c' waiting for 'd' to act. For example, if 'd' is
blocking the corridor in front of 'c'.

'B' attacks @, which takes 1 unit of time. I have to move B back
in the queue after d but before A. I can implement the event queue
with a List. Then I move B by calling RemoveAt(0) and Insert(3,B).

But then I think that I want to try a LinkedList since they are
better at insertions and removals. But LinkedList does not take
RemoveAt() or Insert(). I have to call RemoveFirst() and AddAfter().

So I can not write a generic function that moves an element farther
back in a generic container. I have to specialize for each type of
container. It is as if the C# designers copied the good parts of
the STL, but left out the insanely great parts.

21) const and readonly too limiting

'const' can only be used for compile-time constants. 'readonly'
can be evaluated at runtime, but can only be used for fields in an
object. So there is no good way to enforce const'ness inside a
function. I guess I could construct an object that has a readonly
field. But give me a break, I just want "area=width*height" to be
constant.

22) Assert

After a fair bit of wrestling with compiler options, I still could
not get Debug.Assert() working under mono.

23) Difficult to split up the implementation

Big files are bad.

http://www.leshatton.org/Documents/Ubend_IS697.pdf

But if you have a class in a file, you have to implement all of the
member functions in that file. Unless you use partial classes.
But then classes can be expanded from arbitrary places. So if I
forget to compile in an implementation of a virtual function, it
will silently use the base class. No thanks.

24) False compiler alarm

The compiler exits with an *error* when it thinks that I am using
an uninitialized variable. This is very annoying when the logic is
a bit complicated but ensures that the variable will be
initialized. False alarms make me angry. You wouldn't like me
when I'm angry.

The Different:
--------------

25) Public

You have to specify 'public' for every public member. You can not
have a public section as in C++.

26) Delegates

Delegates are how C# handles function pointers. For what I have
done, they were just different. Not better, not worse.

27) Default initialization

Everything gets default initialized to 0. This seems wasteful, but
it is probably not that bad for a roguelike.

C++
---

The Good:
---------

1) Performance

After rewriting the code in C++, the pathfinding code ran 5 times
faster. Now I can use the good pathfinding algorithm, instead of
using some god-awful performance hack that makes players think that
the monsters are blind, deaf, and drunk.

2) Simple Classes

If I have a quick and simple class, I can just put it in a .hxx
file and not have to worry about adding it to the build system
(Assuming that the build system automatically checks dependencies.
Mine does, doesn't yours?).

3) Easy to separate the implementation

It is really nice to be able to put member functions into their own
files. So Foo::bar() goes into Foo/bar.cxx, Foo::baz() goes into
Foo/baz.cxx, etc. Simplicity.

4) Easy integration with C/C++

I am using libtcod (C with C++ interface) and Roguelikelib (C++),
and it is nice to not have to deal with any glue code to interface
with them.

5) Debugging

Valgrind is a magical tool that works unreasonably well at catching
all of the undefined behavior in C++ that people love to complain
about. Gdb has excellent emacs integration that makes bug stomping
almost too easy. Both of these tools work, and they work well.
Moreover, libstdc++ has a debug mode with a checked STL
implementation. I have never used it since valgrind+gdb always
found my bugs fast enough.

The Bad:
--------

6) Compile time

Compiling my code can take up to 4-5 seconds, depending on
optimizations. Change one file and it takes 0.8 seconds. Unless
it is a header file, in which case it goes back up. This is not
yet a large project, and I know from painful experience that it
will only get worse.

7) Multidimensional arrays

The built-in multidimensional arrays are not dynamically
resizeable. You can make your own, though you have to be careful.
Boost's multi_array, for example, is slower than molasses in
January.

8) Value vs Reference

When passing variables into a function, I definitely have to think
more about declaring them as "Foo foo" vs "Foo &foo" vs "const Foo
&foo". Remember: Less thinking == good.

9) Random syntactic annoyances

When defining classes, you have to have a semicolon ';' after the
closing curly brace. Also, you need include guards inside header
files. Finally, the syntax for passing pointers to member
functions is a bit obtuse, and the C++0X extensions do not really
help.

10) No lambda's ... yet

If I want to use for_each in C++, I have to write function objects.
No thank you. And no, boost::lambda is not going to cut it.
Someday I will upgrade to gcc 4.5 and get lambdas, but not today.
Range-based for would be great as well, but no one has actually
implemented them. Even so, having to write your own loops is not
so bad, especially with the 'auto' keyword.

11) Formatted strings

If I want to make a string with a number such as "You have 2 HP
left!!", I always end up using stringstream, and it always takes three
lines to do what should happen in one.

12) Headers are compiled at the same time as the main source files

When I make a major change that causes a lot of compilation errors,
I am constantly jumping around to bugs in different places. So I
never quite finish finding all of the bugs in one file before I
have to deal with bugs in another file.

13) References are difficult to use

References have to be bound at all times and there is no 'null'
reference. So you can not make an array of them and assign them
later. In the end, I just ended up using pointers.

14) Header files do not gracefully handle circular dependencies

You have to use forward declarations and split the code into
implementation files. This can get a bit convoluted, especially if
you want the code to be inline or a template.

15) Construction order

When writing a constructor, member variables are built in the order
that they are defined in the class. If you try to use a member
before it is built, it does not tell you. It just fails
mysteriously at runtime.

16) while(); {} vs while() {}

These two forms are both valid, but the first one does not execute
the inner braces of the loop. It is not too hard to get the first
one when you want the second.

The Different:
--------------

17) No garbage collector

Porting from D to C# was extremely straightforward. In contrast, I
had to modify the design of the C# code to get it to work in C++.
This included debugging a number of subtle issues. For example, it
is dangerous to use pointers as a unique id for an object, because
they can get invalidated when a vector grows. I ended up using
std::unique_ptr which solved some, but not all of the problems. I
feel that if I had just written the code in C++, I would not have
run into the subtle issues. Programming languages really change
the way you think.

I could have attached a garbage collector, but part of what I
wanted to find out is to what degree garbage collection affects
performance.

Jeff Lait

unread,
May 20, 2010, 10:33:42 PM5/20/10
to
On May 20, 8:17 pm, Walter Landry <wlan...@caltech.edu> wrote:
>
> 9) Random syntactic annoyances
>
>    When defining classes, you have to have a semicolon ';' after the
>    closing curly brace.  

First, an excellent comparison, thank you for the rant.

And this semicolon is one of my most hated semicolons. I wonder why
it feels so odd to require it?

> 11) Formatted strings
>
>    If I want to make a string with a number such as "You have 2 HP
>    left!!", I always end up using stringstream, and it always takes three
>    lines to do what should happen in one.

This is why we have sprintf(). Of course, to use sprintf, you need to
first write your own string class to handle variable length strings in
a safe & sane way. But printf really was a good idea for formatting
strings. I really hate the << craziness.

> 13) References are difficult to use
>
>    References have to be bound at all times and there is no 'null'
>    reference.  So you can not make an array of them and assign them
>    later.  In the end, I just ended up using pointers.

Well, it is true references are difficult to use as pointers :> I
think the other languages misdirected you - references IMHO are when
you'd be just as happy with a value.
--
Jeff Lait
(POWDER: http://www.zincland.com/powder)

Konstantin Stupnik

unread,
May 21, 2010, 1:05:45 AM5/21/10
to
Nice read. Thanks.

> 9) Random syntactic annoyances
>
> When defining classes, you have to have a semicolon ';' after the
> closing curly brace.

Hell yeah...
And ';' after closing curly brace of namespace is PROHIBITED.

namespace MyNamespace {
class MyClass{
};
}

Kinda asymmetric.

> 11) Formatted strings
>
> If I want to make a string with a number such as "You have 2 HP
> left!!", I always end up using stringstream, and it always takes three
> lines to do what should happen in one.

eventLog.Add(FORMAT("You have %{} HP ",you.hp));

Some time ago I wrote type&size safe, customizable
FORMAT that have speed comparable to sprintf.
And quite happy with it.
I can share it if anyone interested.

corremn

unread,
May 21, 2010, 1:38:13 AM5/21/10
to
On May 21, 2:05 pm, Konstantin Stupnik <konstantin.stup...@gmail.com>
wrote:

> Nice read. Thanks.
>
> > 9) Random syntactic annoyances
>
> >    When defining classes, you have to have a semicolon ';' after the
> >    closing curly brace.
>
> Hell yeah...
> And ';' after closing curly brace of namespace is PROHIBITED.
>
Not prohibited by my compiler. Infact I though it was required,
because nearly everything else I declare in a header file needs it,
enums, structs, classes etc.. *Off to check the standard...

Kenneth 'Bessarion' Boyd

unread,
May 21, 2010, 2:06:34 AM5/21/10
to
On May 20, 9:33 pm, Jeff Lait <torespondisfut...@hotmail.com> wrote:
> On May 20, 8:17 pm, Walter Landry <wlan...@caltech.edu> wrote:
>
>
>
> > 9) Random syntactic annoyances
>
> >    When defining classes, you have to have a semicolon ';' after the
> >    closing curly brace.  
>
> First, an excellent comparison, thank you for the rant.
>
> And this semicolon is one of my most hated semicolons.  I wonder why
> it feels so odd to require it?

Try...

struct a {int x; int y;} z;

Yes, this declares a variable of type struct a at the same time as
defining struct a. C++ gets this awful construct from its backward
compatibility with C. This is one of the worst constructs in writing
a non-state machine parser for either C or C++.

So no, the ; after a struct definition isn't *strictly* required; it's
only required if you don't want to declare variables at the same time.

> > 13) References are difficult to use
>
> >    References have to be bound at all times and there is no 'null'
> >    reference.  So you can not make an array of them and assign them
> >    later.  In the end, I just ended up using pointers.

C#/JavaScript/Perl/Python/PHP/.... references are operationally
pointers when translating to C++.

Konstantin Stupnik

unread,
May 21, 2010, 3:04:25 AM5/21/10
to
g++ -ansi -pedantic -c a.cpp
a.cpp:10: error: extra �;�

Walter Landry

unread,
May 23, 2010, 1:07:42 AM5/23/10
to
Konstantin Stupnik <konstanti...@gmail.com> writes:
>> 11) Formatted strings
>>
>> If I want to make a string with a number such as "You have 2 HP
>> left!!", I always end up using stringstream, and it always takes three
>> lines to do what should happen in one.
>
> eventLog.Add(FORMAT("You have %{} HP ",you.hp));
>
> Some time ago I wrote type&size safe, customizable
> FORMAT that have speed comparable to sprintf.
> And quite happy with it.
> I can share it if anyone interested.

I would be interested. Getting something fast and safe is rather
difficult since you will have to allocate memory and sprintf does not.

But to be honest, I do not really like formatting output like that. If
you add a field, you have to add the variable and modify the format
string. So they can get out of sync. I know that format strings are
better for internationalization, but I often do not have to worry about
that.

What I really want is for a line like this

string s="You have " + 2 + " HP left!!";

to work. What I would settle for is for this

string s=(stringstream() << "You have " << 2 << " HP left!!").str();

to work. But the wonderful thing about C++ is that you have a lot of
freedom to fill in the holes in the standard library. So I could write
a class that makes something reasonable work [1]. But I really should not
have to do that.

Cheers,
Walter Landry
wla...@caltech.edu

[1]

class mystream
{
std::stringstream ss;
public:
std::string str()
{
return ss.str();
}

template<typename T>
mystream & operator<<(const T &s)
{
ss << s;
return *this;
}
};


Now I can write

string s=(mystream() << "You have " << 2 << " HP left!!").str();

Keith H Duggar

unread,
May 23, 2010, 12:03:41 PM5/23/10
to
On May 20, 10:33 pm, Jeff Lait <torespondisfut...@hotmail.com> wrote:
> On May 20, 8:17 pm, Walter Landry <wlan...@caltech.edu> wrote:
> > 9) Random syntactic annoyances
>
> > When defining classes, you have to have a semicolon ';' after the
> > closing curly brace.
>
> First, an excellent comparison, thank you for the rant.

I second that. Thank you!

> And this semicolon is one of my most hated semicolons. I wonder why
> it feels so odd to require it?

If I understand you correctly you are asking why it "feels so
odd" to us rather than why it is grammatically required which
Kenneth explained. As to your question, I think it is because
some statement structures such as if ( ) { }, while ( ) { },
and just plain { } blocks, and even namespace declarations
namespace { } do not require such a semicolon. So naturally
we come to think of } as omega, "finality", "the end", finito,
'nuff said. And yet here we have this singularly odd need
for };. Yuck. A sore thumb.

> > 11) Formatted strings
>
> > If I want to make a string with a number such as "You have 2 HP
> > left!!", I always end up using stringstream, and it always takes three
> > lines to do what should happen in one.
>

> This is why we have sprintf(). Of course, to use sprintf, you need to
> first write your own string class to handle variable length strings in
> a safe & sane way. But printf really was a good idea for formatting
> strings. I really hate the << craziness.

See "The March of Progress" in the middle of this page:

http://www.horstmann.com/

> > 13) References are difficult to use
>
> > References have to be bound at all times and there is no 'null'
> > reference. So you can not make an array of them and assign them
> > later. In the end, I just ended up using pointers.
>

> Well, it is true references are difficult to use as pointers :> I
> think the other languages misdirected you - references IMHO are when
> you'd be just as happy with a value.

And also pointers do have their place. Both are useful. Yet some
get the (mistaken) idea that pointers are deprecated now that we
have references. (Note this is not a comment about Walter. I do
not think he suffers from this delusion.)

KHD

Jeff Lait

unread,
May 24, 2010, 12:00:41 AM5/24/10
to
On May 23, 12:03 pm, Keith H Duggar <dug...@alum.mit.edu> wrote:
> On May 20, 10:33 pm, Jeff Lait <torespondisfut...@hotmail.com> wrote:
>
> > > 11) Formatted strings
>
> > >    If I want to make a string with a number such as "You have 2 HP
> > >    left!!", I always end up using stringstream, and it always takes three
> > >    lines to do what should happen in one.
>
> > This is why we have sprintf().  Of course, to use sprintf, you need to
> > first write your own string class to handle variable length strings in
> > a safe & sane way.  But printf really was a good idea for formatting
> > strings.  I really hate the << craziness.
>
> See "The March of Progress" in the middle of this page:
>
>    http://www.horstmann.com/

Thanks for that post! Saves me my reply to Konstantin explaining that
my problem with the << notation is whenever you want to qualify *how*
you want to output your variable. Inline iostream modifiers are
beyond ugly.
--
Jeff Lait
(POWDER: http://www.zincland.com/powder/)

Konstantin Stupnik

unread,
May 23, 2010, 11:55:02 PM5/23/10
to
Walter Landry wrote:
> Konstantin Stupnik <konstanti...@gmail.com> writes:
>>> 11) Formatted strings
>>>
>>> If I want to make a string with a number such as "You have 2 HP
>>> left!!", I always end up using stringstream, and it always takes three
>>> lines to do what should happen in one.
>> eventLog.Add(FORMAT("You have %{} HP ",you.hp));
>>
>> Some time ago I wrote type&size safe, customizable
>> FORMAT that have speed comparable to sprintf.
>> And quite happy with it.
>> I can share it if anyone interested.
>
> I would be interested. Getting something fast and safe is rather
> difficult since you will have to allocate memory and sprintf does not.
You can have buffer on stack that is sufficient for
most cases and heap buffer for rare 'rly big msg' case.
Source below.

> Now I can write
>
> string s=(mystream() << "You have " << 2 << " HP left!!").str();

What I learned about c++ streams is that ... they are SLOW.
Horribly slow.
And once you need something a little more complex, manipulators
can make your message completely unreadable.

#ifndef __WQ_FORMAT_HPP__
#define __WQ_FORMAT_HPP__

#include <stdio.h>
#include <vector>
#include <string>
#include <string.h>

class FormatBuffer;

struct ArgsList{
enum{STACK_ARG_LIST_SIZE=10};
ArgsList():argArrayCount(0),useList(false)
{
}

typedef void (*FmtMethod)(FormatBuffer&,const void*,int,int);

template <class T>
struct FmtGen
{
static void Fmt(FormatBuffer& buf,const void* val,int w,int p)
{
customformat(buf,*(const T*)val,w,p);
}
};

enum ValueType{
vtNone,
vtChar,vtUChar,vtShort,vtUShort,
vtInt,vtUInt,vtLong,vtULong,
vtLongLong,vtULongLong,
vtCharPtr,vtVoidPtr,
vtString,vtCustom
};

struct ArgInfo{
ValueType vt;
union{
char c;
unsigned char uc;
short s;
unsigned short us;
int i;
unsigned int ui;
long l;
unsigned long ul;
long long ll;
unsigned long long ull;
const char* sptr;
const void* val;
};
FmtMethod fmt;

ArgInfo():vt(vtNone),val(0),fmt(0){}

ArgInfo(const ArgInfo& ai)
{
vt=ai.vt;
ull=ai.ull;
fmt=ai.fmt;
}
template <class T>
ArgInfo(const T& t)
{
Init(t);
}

template <class T>
void Init(const T& t)
{
val=&t;
vt=vtCustom;
fmt=FmtGen<T>::Fmt;
}

void Init(char v){vt=vtChar;c=v;}
void Init(unsigned char v){vt=vtUChar;uc=v;}
void Init(short v){vt=vtShort;i=0;s=v;}
void Init(unsigned short v){vt=vtUShort;ui=0;us=v;}
void Init(int v){vt=vtInt;i=v;}
void Init(unsigned int v){vt=vtUInt;ui=v;}
void Init(long v){vt=vtLong;l=v;}
void Init(unsigned long v){vt=vtULong;ul=v;}
void Init(long long v){vt=vtLongLong;ll=v;}
void Init(unsigned long long v){vt=vtULongLong;ull=v;}
void Init(const char* v){vt=vtCharPtr;sptr=v;}
void Init(void* v){vt=vtVoidPtr;val=v;}
void Init(const std::string& s){vt=vtString;sptr=s.c_str();}

void Fmt(FormatBuffer& buf,int w,int p)const
{
fmt(buf,val,w,p);
}
};

std::vector<ArgInfo> argList;
ArgInfo argArray[STACK_ARG_LIST_SIZE];
size_t argArrayCount;
bool useList;

template <class T>
ArgsList& operator,(const T& t)
{
if(useList)
{
argList.push_back(ArgInfo(t));
}else
{
if(argArrayCount==STACK_ARG_LIST_SIZE)
{
argList.assign(argArray,argArray+argArrayCount);
argList.push_back(ArgInfo(t));
useList=true;
}else
{
argArray[argArrayCount].Init(t);
argArrayCount++;
}
}

return *this;
}

const ArgInfo* getArgInfo(size_t idx)const
{
if(useList)
{
if(idx>=argList.size())return 0;
return &argList[idx];
}else
{
if(idx>=argArrayCount)return 0;
return argArray+idx;
}
}
/*
void FmtArg(int idx,int w,int h)const
{
if(useList)
{
if(idx>=argList.size())return;
argList[idx].Fmt(*buf,w,h);
}else
{
if(idx>=argArrayCount)return;
argArray[idx].Fmt(*buf,w,h);
}
}
*/

FormatBuffer* buf;
};


class FormatBuffer{
public:
enum{FORMAT_RESULT_BUFFER_SIZE=256};

FormatBuffer():ptr(buf),end(ptr),length(0),size(FORMAT_RESULT_BUFFER_SIZE),inHeap(false)
{
buf[0]=0;
}
~FormatBuffer()
{
if(inHeap)delete [] ptr;
}
FormatBuffer(const FormatBuffer& src)
{
length=src.length;
if(src.length+1>FORMAT_RESULT_BUFFER_SIZE)
{
inHeap=true;
size=length+1;
ptr=new char[size];
}else
{
inHeap=false;
size=FORMAT_RESULT_BUFFER_SIZE;
ptr=buf;
}
memcpy(ptr,src.ptr,src.length);
end=ptr+length;
*end=0;
}

inline void Fill(char fillc,int w)
{
if(w<=0)return;
if(length+w+1>size)
{
Expand(w);
}
length+=w;
while(w--)
{
*end++=fillc;

}
*end=0;
}

inline void Append(const FormatBuffer& buf)
{
int len=buf.Length();
if(length+len+1>size)
{
Expand(len);
}

memcpy(end,buf.ptr,len);
end+=len;
length+=len;
*end=0;
}

inline void Append(const char* str)
{
Append(str,strlen(str));
}
inline void Append(const char* str,size_t len)
{

if(length+len+1>size)
{
Expand(len);
}

memcpy(end,str,len);
end+=len;
length+=len;
*end=0;
}
inline void Append(char c)
{
if(length+2>size)
{
Expand(1);
}
*end=c;
length++;
end++;
*end=0;
}
inline char* End(size_t len)
{
if(length+len>=size)
{
Expand(len);
}
return end;
}
inline void Grow(size_t grow)
{
if(length+grow>size)
{
Expand(grow);
}
length+=grow;
end+=grow;
*end=0;
}
inline const char* Str()const
{
return ptr;
}
inline size_t Length()const
{
return length;
}
inline ArgsList& getArgList()
{
al.buf=this;
return al;
}
protected:
inline void Expand(int len)
{
size=(length+len+1)*2;
char* newptr=new char[size];
memcpy(newptr,ptr,length+len);
if(inHeap)delete [] ptr;
inHeap=true;
}

char buf[FORMAT_RESULT_BUFFER_SIZE];
char *ptr;
char *end;
size_t length;
size_t size;
bool inHeap;
ArgsList al;
};
/*
void format(FormatBuffer& b,int i,int w,int p)
{
b.Grow(sprintf(b.End(32),"%d",i));
}

void format(FormatBuffer& b,const char* str,int w,int p)
{
b.Append(str);
}
*/

template <typename inttype>
inline void fmtintsign(inttype val,FormatBuffer* buf,int w,bool lz)
{
char tmp[64];
bool negative=val<0;
val=negative?-val:val;
char* ptr=tmp+64;
if(!val)
{
*--ptr='0';
}else
while (val)
{
*--ptr=(val%10)+'0';
val/=10;
}
size_t l=tmp+64-ptr;
if(w && !lz)
{
buf->Fill(' ',w-l-(negative?1:0));
}
if (negative)
{
buf->Append('-');
if(w)w--;
}
if(w && lz)
{
buf->Fill('0',w-l);
}
buf->Append(ptr,(size_t)(tmp+64-ptr));
}

template <typename inttype>
inline void fmtintunsign(inttype val,FormatBuffer* buf,size_t w,bool lz)
{
char tmp[64];
char* ptr=tmp+64;
if(!val)
{
*--ptr='0';
}else
while (val)
{
*--ptr=(val%10)+'0';
val/=10;
}
if(w)
{
buf->Fill(lz?'0':' ',w-(tmp+64-ptr));
}
buf->Append(ptr,(size_t)(tmp+64-ptr));
}

template <typename inttype>
inline void fmtinthex(inttype val,FormatBuffer* buf,int w,bool lz,bool uc)
{
static char
sym[16]={'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};
static char
symuc[16]={'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
char* sptr=uc?symuc:sym;
char tmp[64];
char* ptr=tmp+64;
if(!val)
{
*--ptr='0';
}else
while (val)
{
*--ptr=sptr[val&0xf];
val>>=4;
}
if(w)
{
buf->Fill(lz?'0':' ',w-(tmp+64-ptr));
}
buf->Append(ptr,(size_t)(tmp+64-ptr));
}

inline FormatBuffer& format(const char* fmt,const ArgsList& a)
{
FormatBuffer* buf=a.buf;
const char* ptr=fmt;
const char* str=strchr(fmt,'%');
int nextIdx=0;
while(str)
{
if(str[1]=='%')
{
buf->Append(ptr,str-ptr+1);
str+=2;
}else
{
buf->Append(ptr,str-ptr);
str++;
int idx=0;
int w=0;
int p=0;
bool lz=false;
int hex=0;
if(*str=='x' || *str=='X')
{
hex=*str=='x'?1:2;
str++;
}

if(*str=='{')
{
str++;
if(*str=='x' || *str=='X')
{
hex=*str=='x'?1:2;
str++;
}

if(*str>='0' && *str<='9')
{
while(*str>='0' && *str<='9')
{
idx=idx*10+(*str++-'0');
}
if(idx>0)idx--;
nextIdx=idx+1;
}else
{
idx=nextIdx++;
}
if(*str==':')
{
str++;
if(*str=='0')
{
lz=true;
str++;
}
while(*str>='0' && *str<='9')
{
w=w*10+(*str++-'0');
}
if(*str==',')
{
str++;
while(*str>='0' && *str<='9')
{
p=p*10+(*str++-'0');
}
}
}
while(*str && *str!='}')str++;
if(*str)str++;
}else
{
while(*str>='0' && *str<='9')
{
idx=idx*10+(*str++-'0');
}
if(idx>0)idx--;
}
const ArgsList::ArgInfo* arg=a.getArgInfo(idx);
if(arg)
{
switch (arg->vt)
{
case ArgsList::vtUChar:
case ArgsList::vtChar:
{
if(w)buf->Fill(' ',w);
buf->Append(arg->c);
}break;
case ArgsList::vtShort:
case ArgsList::vtInt:
{
hex?fmtinthex (arg->i,buf,w,lz,hex==2):
fmtintsign(arg->i,buf,w,lz);
}break;
case ArgsList::vtUShort:
case ArgsList::vtUInt:
{
hex?fmtinthex(arg->ui,buf,w,lz,hex==2):
fmtintunsign(arg->ui,buf,w,lz);
}break;
case ArgsList::vtLong:
{
hex?fmtinthex(arg->l,buf,w,lz,hex==2):
fmtintunsign(arg->l,buf,w,lz);
}break;
case ArgsList::vtULong:
{
hex?fmtinthex(arg->ul,buf,w,lz,hex==2):
fmtintunsign(arg->ul,buf,w,lz);
}break;
case ArgsList::vtLongLong:
{
hex?fmtinthex(arg->ll,buf,w,lz,hex==2):
fmtintsign(arg->ll,buf,w,lz);
}break;
case ArgsList::vtULongLong:
{
hex?fmtinthex(arg->ull,buf,w,lz,hex==2):
fmtintunsign(arg->ull,buf,w,lz);
}break;
case ArgsList::vtVoidPtr:
{
buf->Grow(sprintf(buf->End(18),"%p",arg->val));
}break;
case ArgsList::vtCharPtr:
case ArgsList::vtString:
{
if(!p)
{
buf->Append(arg->sptr);
}else
{
int l=strlen(arg->sptr);
if(l>p)l=p;
buf->Append(arg->sptr,l);
}
if(w)buf->Fill(' ',w-strlen(arg->sptr));
}break;
case ArgsList::vtCustom:
{
arg->Fmt(*buf,w,p);
}break;
default:
{
fprintf(stderr,"unexpected vt=%d\n",arg->vt);
}
}
}
}
ptr=str;
str=strchr(str,'%');
}
buf->Append(ptr);
return *buf;
}

#define FORMAT(fmt,...)
format(fmt,(FormatBuffer().getArgList(),__VA_ARGS__)).Str()


#endif

Konstantin Stupnik

unread,
May 24, 2010, 2:31:17 AM5/24/10
to

Why have you wanted to replay with this to me? :)
Probably you wanted to reply to Walter? ;)
I'm adept of *printf like format and I hate iostreams
for many different reasons.

Walter Landry

unread,
May 24, 2010, 6:53:31 PM5/24/10
to
Konstantin Stupnik <konstanti...@gmail.com> writes:

> Walter Landry wrote:
>> Konstantin Stupnik <konstanti...@gmail.com> writes:
>>>> 11) Formatted strings
>>>>
>>>> If I want to make a string with a number such as "You have 2 HP
>>>> left!!", I always end up using stringstream, and it always takes three
>>>> lines to do what should happen in one.
>>> eventLog.Add(FORMAT("You have %{} HP ",you.hp));
>>>
>>> Some time ago I wrote type&size safe, customizable
>>> FORMAT that have speed comparable to sprintf.
>>> And quite happy with it.
>>> I can share it if anyone interested.
>>
>> I would be interested. Getting something fast and safe is rather
>> difficult since you will have to allocate memory and sprintf does not.
> You can have buffer on stack that is sufficient for
> most cases and heap buffer for rare 'rly big msg' case.
> Source below.

Thanks. I am having some problems with it. The statement

std::string s=FORMAT("You have %{} HP left!!",2.3);

does not compile. Also, I could not figure out how to reorder arguments
(needed for internationalization). So something like

std::cout << FORMAT("You have %{1} %{0} left!! %{}","HP",2) << std::endl;

should output

You have 2 HP left!!

Have you looked at FastFormat [1]? That claims to be comparable in
speed to stdio. From my brief look, it feels almost identical to how C#
does formatted output.

>> Now I can write
>>
>> string s=(mystream() << "You have " << 2 << " HP left!!").str();
> What I learned about c++ streams is that ... they are SLOW.
> Horribly slow.

Believe me, I know. I ran into this problem just last week. But for a
roguelike and many other applications, string formatting speed is not
usually an issue. Only when it becomes an issue do I consider
alternatives.

> And once you need something a little more complex, manipulators
> can make your message completely unreadable.

I fully agree. It is just that I do not find myself doing complex
formatting all that often. What I do find to be a pain is having to
modify the code in two places in order to print out a new variable. I
also love being able to overload "operator<<" and then use the same
notation for everything, instead of having to write '.toString()'
methods.

Essentially, iostreams is nice if you do simple operations on complex
objects, while stdio is nice if you do complex operations on simple
objects. Neither of them support complex operations on complex objects
very well. YMMV.

Scott Meyers talks about this at some length in his books ("Effective
C++" Item 2 and "More Effective C++" Item 23). He likes iostreams,
though he does not like their performance or how they handle complicated
formatting. Strangely, he does not mention the problems with
internationalization.

Cheers,
Walter Landry

[1] http://www.fastformat.org/

Jeff Lait

unread,
May 24, 2010, 10:56:51 PM5/24/10
to
On May 24, 2:31 am, Konstantin Stupnik <konstantin.stup...@gmail.com>
wrote:

> Jeff Lait wrote:
> > Thanks for that post!  Saves me my reply to Konstantin explaining that
> > my problem with the << notation is whenever you want to qualify *how*
> > you want to output your variable.  Inline iostream modifiers are
> > beyond ugly.
>
> Why have you wanted to replay with this to me? :)
> Probably you wanted to reply to Walter? ;)
> I'm adept of *printf like format and I hate iostreams
> for many different reasons.

Oops! You are right, I pulled the wrong name off the thread list. My
apologies, I'm glad you took my mistake in good spirits.
--
Jeff Lait
(POWDER: http://www.zincland.com/powder)

Konstantin Stupnik

unread,
May 25, 2010, 12:38:07 AM5/25/10
to
Walter Landry wrote:
> Konstantin Stupnik <konstanti...@gmail.com> writes:
>
>> Walter Landry wrote:
>>> Konstantin Stupnik <konstanti...@gmail.com> writes:
>>>>> 11) Formatted strings
>>>>>
>>>>> If I want to make a string with a number such as "You have 2 HP
>>>>> left!!", I always end up using stringstream, and it always takes three
>>>>> lines to do what should happen in one.
>>>> eventLog.Add(FORMAT("You have %{} HP ",you.hp));
>>>>
>>>> Some time ago I wrote type&size safe, customizable
>>>> FORMAT that have speed comparable to sprintf.
>>>> And quite happy with it.
>>>> I can share it if anyone interested.
>>> I would be interested. Getting something fast and safe is rather
>>> difficult since you will have to allocate memory and sprintf does not.
>> You can have buffer on stack that is sufficient for
>> most cases and heap buffer for rare 'rly big msg' case.
>> Source below.
>
> Thanks. I am having some problems with it. The statement
>
> std::string s=FORMAT("You have %{} HP left!!",2.3);
>
> does not compile.
You need custom formatter for floating point.
Like this:

void customformat(FormatBuffer& fb,double value,int width,int precision)
{
char fmt[32];
sprintf(fmt,"%%%d.%df",width,precision);
int len=sprintf(fb.End(64),fmt,value);
fb.Grow(len);
}


> Also, I could not figure out how to reorder arguments
> (needed for internationalization). So something like
>
> std::cout << FORMAT("You have %{1} %{0} left!! %{}","HP",2) << std::endl;

You guessed right. But index is 1 based.
FORMAT("You have %{2} %{1} left!! %{}","HP",2);

It is also possible to specify width and precision:
%{:2.5}

precision is mostly used as argument for custom formatters.

> should output
>
> You have 2 HP left!!
>
> Have you looked at FastFormat [1]? That claims to be comparable in
> speed to stdio. From my brief look, it feels almost identical to how C#
> does formatted output.

Yeah. I tried it.
Several megabytes header file with LOTS of overloaded
template functions.
It kills compile time.

Martin Read

unread,
May 25, 2010, 2:38:43 PM5/25/10
to
Walter Landry <wal...@geodynamics.org> wrote:
>Scott Meyers talks about this at some length in his books ("Effective
>C++" Item 2 and "More Effective C++" Item 23). He likes iostreams,
>though he does not like their performance or how they handle complicated
>formatting. Strangely, he does not mention the problems with
>internationalization.

An American neglecting to mention i18n problems? Surely not.
--
\_\/_/ turbulence is certainty turbulence is friction between you and me
\ / every time we try to impose order we create chaos
\/ -- Killing Joke, "Mathematics of Chaos"

Ray

unread,
May 27, 2010, 4:13:59 AM5/27/10
to
Walter Landry wrote:

> Also, I could not figure out how to reorder arguments
> (needed for internationalization). So something like
>
> std::cout << FORMAT("You have %{1} %{0} left!! %{}","HP",2) <<
> std::endl;

Does experience really show that format strings with reordering
of arguments are:
A) adequate for internationalization, where different
languages grammaticalize different information about,
f'rexample, gender and number?

B) Honestly easier than just reordering arguments?

Konstantin Stupnik

unread,
May 27, 2010, 5:20:00 AM5/27/10
to
Ray wrote:
> Walter Landry wrote:
>
>> Also, I could not figure out how to reorder arguments
>> (needed for internationalization). So something like
>>
>> std::cout << FORMAT("You have %{1} %{0} left!! %{}","HP",2) <<
>> std::endl;
>
> Does experience really show that format strings with reordering
> of arguments are:

In practice I only used reordering to use one argument
more than once in single format string.
But it's pretty rare too.
Most of the time I use bare placeholders.

Walter Landry

unread,
May 27, 2010, 5:40:22 AM5/27/10
to
Ray <be...@sonic.net> writes:

> Walter Landry wrote:
>
>> Also, I could not figure out how to reorder arguments
>> (needed for internationalization). So something like
>>
>> std::cout << FORMAT("You have %{1} %{0} left!! %{}","HP",2) <<
>> std::endl;
>
> Does experience really show that format strings with reordering
> of arguments are:
> A) adequate for internationalization, where different
> languages grammaticalize different information about,
> f'rexample, gender and number?

It is the approach taken for gettext and Java, so it would seem to be
sufficient at least for some people. I have not heard of any other
approaches that work, but I am far from an expert on I18N.

> B) Honestly easier than just reordering arguments?

You have to write the code in a particular order. For example, consider
the sentence

I have seen the boat.

In the code, you might want to parametrize the verb (seen) and the
article+object (the boat). So you might write the code as

format(_("I have {0} {1}."),verb,object);

In German, the original phrase would be [1]

Ich habe das boot gesehen.

So a German version of the format string, replaced at runtime, would be

"Ich habe {1} {0}."

where verb="gesehen" and object="das boot".

Cheers,
Walter Landry

[1] I think. My German is quite rusty.

Jeff Lait

unread,
May 27, 2010, 10:37:19 AM5/27/10
to
On May 27, 4:13 am, Ray <b...@sonic.net> wrote:
> Walter Landry wrote:
> > Also, I could not figure out how to reorder arguments
> > (needed for internationalization).  So something like
>
> >   std::cout << FORMAT("You have %{1} %{0} left!! %{}","HP",2) <<
> >   std::endl;
>
> Does experience really show that format strings with reordering
> of arguments are:
>    A) adequate for internationalization, where different
>       languages grammaticalize different information about,
>       f'rexample, gender and number?

I think it depends on your target. I mean, if you want native-quality
i18n, then you are going have to make code changes or strictly limit
your dynamic strings. But is there not any room for poor quality i18n
to address accessibility?

I mean, the output of google translate won't win any awards for prose,
but it allows me to muddle through pages that otherwise would be
opaque to me.

>    B) Honestly easier than just reordering arguments?

Then you have #ifdefs or conditional code on the language. String
only changes can be extracted to a string table. The string table can
be plain text easily edited by someone without coding experience. As
a programmer I like to focus on the coding challenge of i18n, but I
think practically speaking a lot of the work is the amount of actual
translation you have to do?

Patric Mueller

unread,
May 31, 2010, 10:23:25 AM5/31/10
to
Jeff Lait <torespon...@hotmail.com> wrote:
> On May 27, 4:13�am, Ray <b...@sonic.net> wrote:
>
> I think it depends on your target. I mean, if you want native-quality
> i18n, then you are going have to make code changes or strictly limit
> your dynamic strings. But is there not any room for poor quality i18n
> to address accessibility?

IMO poor quality i18n won't bring you the native language players.

There are too many other games out there with decent i18n.

Playing a game with poor quality language isn't a lot of fun in your
native language (although in my experience some native speakers are
more pardoning than others [e.g. speakers of English or Slavic
languages]).

But playing a game with not so good language in a foreign language is
a completely different matter.

It's way easier writing badly in a foreign language and getting away
with it than writing badly in your native language. :)

> I mean, the output of google translate won't win any awards for prose,
> but it allows me to muddle through pages that otherwise would be
> opaque to me.

Yes, but for comparison, do a round trip of a roguelike games log from
English to a foreign language back to English. Would you like to play
such a game?

Roguelikes with their inherently dynamic constructed strings are
clearly at a disadvantage here.

OTOH as the survey of last year showed there are more native English
speaker playing the large RL. This correlates with the fact that D&D
and RPG in general is more widespread and even mainstream in the USA
than in the rest of the world.

> Then you have #ifdefs or conditional code on the language. String
> only changes can be extracted to a string table. The string table can
> be plain text easily edited by someone without coding experience. As
> a programmer I like to focus on the coding challenge of i18n, but I
> think practically speaking a lot of the work is the amount of actual
> translation you have to do?

Yes. NetHack-De was started in the last days of 2004, the grammar
framework was mostly finished by 2006, but the translation is still
ongoing. :-)

Especially finding good translations for tricky parts or plays on
words is really time intensive.

Bye
Patric

--
NetHack-De: NetHack auf Deutsch - http://nethack-de.sf.net/
NetHack for AROS: http://sf.net/projects/nethack-aros/
UnNetHack: http://apps.sf.net/trac/unnethack/

Ray

unread,
May 31, 2010, 11:06:52 AM5/31/10
to
Patric Mueller wrote:

> Playing a game with poor quality language isn't a lot of fun in your
> native language (although in my experience some native speakers are
> more pardoning than others [e.g. speakers of English or Slavic
> languages]).

> But playing a game with not so good language in a foreign language is
> a completely different matter.

It's true. I play games with hilariously bad English, and just
get a laugh out of them. But the Francophones I know are upset,
even offended, if the French of something they're playing isn't
perfect.

And, well, my French is nowhere near as good as my English, so
mistakes in French-language stuff tend to baffle me instead of
amusing me. When not-so-good language proficiency meets up with
a not-so-good translation, there are difficulties.

> It's way easier writing badly in a foreign language and getting away
> with it than writing badly in your native language. :)

This I don't agree with. My native language is English, and
as you mentioned earlier, English-speakers tend to forgive
mistakes. When I'm writing something in French, the audience
tolerates no errors.


> Yes, but for comparison, do a round trip of a roguelike games log from
> English to a foreign language back to English. Would you like to play
> such a game?

If the bad translations were funny? Yes!



> Especially finding good translations for tricky parts or plays on
> words is really time intensive.

Yes. I can see this being a major issue, especially with Nethack.
There's a lot more language-based humor in Nethack than in most
roguelikes. I admire the good job you've done with Nethack-De,
and I admire you for undertaking it and doing it. It must have
been a lot of work.

Bear

Reply all
Reply to author
Forward
0 new messages