www.digitalmars.com         C & C++   DMDScript  

D - how to live without #define

reply "Sandor Hojtsy" <hojtsy index.hu> writes:
One of my programs have a list of boolean options. The options can be loaded
from a text config file, and option values should be immediatly avaiable
during execution. In C :

enum Option {
   firstOption,
   secondOption,
   ....
   lastOption,
   optionQ
};

struct OptionDesc

   bool value;
   char *name;
};

OptionDesc options[optionQ] =
{
  {false, "firstOption"},
  {false, "secondOption"},
  ...
  {false, "lastOption"},
}

And then loading can be done based on name, and values can be retrieved by
indexing the options array with the enum name:

bool d = options[someOption].value;

But the evil *redundancy* is lurking here! Options are listed two times, and
because of this, you can make several errors:
- error in order of the options
- error in option names
- options missing from one of the lists

The C solution is #define-s. See:

#define OPTION_LIST  \
  OH(firstOption),           \
  OH(secondOption),      \
  ....                                 \
  OH(lastOption)

#define OH(a)  a

enum Options {
  OPTION_LIST,
  optionQ
};

struct OptionDesc

   bool value;
   char *name;
};

#undef OH
#define OH(a)  {false, #a}

OptionDesc options[optionQ] =
{
  OPTION_LIST
}

Voila! Ugly, but no redundancy in the source code.
Now how you do this in D?
Without #defines you have to maintain the consistency of the two lists by
yourself. Why can't the complier help in such a mechanical task?

What would be the ideal solution?
In this particular case: reflection. If the program could access enum value
names at runtime, it could load the options from the config file by name.

In general: we should consider all the aspects the C #define was used for,
and try to provide an alternative for the yet unadressed ones.
Sep 23 2002
parent reply "Walter" <walter digitalmars.com> writes:
"Sandor Hojtsy" <hojtsy index.hu> wrote in message
news:ammnbk$1gvd$1 digitaldaemon.com...
 Voila! Ugly, but no redundancy in the source code.

I thought I was the only one to use that technique!
 Now how you do this in D?
 Without #defines you have to maintain the consistency of the two lists by
 yourself. Why can't the complier help in such a mechanical task?

In this case, you can do it with named array initializers: OptionDesc options[optionQ] = [ firstOption : {false, "firstOption"}, ... ]; It's not perfect, but it gets you most of the way there.
 In general: we should consider all the aspects the C #define was used for,
 and try to provide an alternative for the yet unadressed ones.

You can always do some things with a text processor that can't be done reasonably with a symbolic processor, but I think D comes pretty close to having a superior solution for at least the common things done with #define's. I find the C preprocessor inadequate many times, and have turned to specialized C programs that generate C source for table building. You can see an example in idgen.c in the D front end source.
Sep 23 2002
next sibling parent "anderson" <anderson firestar.com.au> writes:
"Walter" <walter digitalmars.com> wrote in message
news:amncsh$2a3t$1 digitaldaemon.com...
 Now how you do this in D?
 Without #defines you have to maintain the consistency of the two lists


 yourself. Why can't the complier help in such a mechanical task?

In this case, you can do it with named array initializers: OptionDesc options[optionQ] = [ firstOption : {false, "firstOption"}, ... ]; It's not perfect, but it gets you most of the way there.

I still think extentable enums would be a better solution.
Sep 23 2002
prev sibling next sibling parent reply Burton Radons <loth users.sourceforge.net> writes:
Walter wrote:
 "Sandor Hojtsy" <hojtsy index.hu> wrote in message
 news:ammnbk$1gvd$1 digitaldaemon.com...
In general: we should consider all the aspects the C #define was used for,
and try to provide an alternative for the yet unadressed ones.

You can always do some things with a text processor that can't be done reasonably with a symbolic processor, but I think D comes pretty close to having a superior solution for at least the common things done with #define's. I find the C preprocessor inadequate many times, and have turned to specialized C programs that generate C source for table building. You can see an example in idgen.c in the D front end source.

No, no, Sandor wasn't advocating metaprogramming or preprocessing, just a high level of introspection. This level of introspection is in my port (I think it's the find method on the EnumInfo, wrong OS to check). But it's had questionable value, as most enums are of the form: typedef int GLenum; enum : GLenum { GL_FALSE = (GLenum) 0, .... } So you can't name it. Oh, and note the cast. Let's make it so that if you're defining an enum, an explicit cast is implied. The good thing about using intermediaries instead of the preprocessor is that it compiles based on the output file, so you get far better debugging. When you really need wacky code processing, the C processor is both a PITA and makes it impossible to tell what goes wrong.
Sep 23 2002
next sibling parent reply "Sandor Hojtsy" <hojtsy index.hu> writes:
"Burton Radons" <loth users.sourceforge.net> wrote in message
news:amo25r$1ga$1 digitaldaemon.com...
 Walter wrote:
 "Sandor Hojtsy" <hojtsy index.hu> wrote in message
 news:ammnbk$1gvd$1 digitaldaemon.com...
In general: we should consider all the aspects the C #define was used



and try to provide an alternative for the yet unadressed ones.

You can always do some things with a text processor that can't be done reasonably with a symbolic processor, but I think D comes pretty close


 having a superior solution for at least the common things done with
 #define's. I find the C preprocessor inadequate many times, and have


 to specialized C programs that generate C source for table building. You


 see an example in idgen.c in the D front end source.

No, no, Sandor wasn't advocating metaprogramming or preprocessing, just a high level of introspection.

Exactly, in this particular case. I think it would be a more ellegant solution. And Walter: Beware! Your (named indexes) solution now include three lists! One list of the enum value names, when defining the enum type, and the two other lists (merged) when initializing the array. Only the possibility of the "order error" was removed, with yet more redundancy. But what about the "missing element error"?
 This level of introspection is in my
 port (I think it's the find method on the EnumInfo, wrong OS to check).

Wow!
   But it's had questionable value, as most enums are of the form:

      typedef int GLenum;
      enum : GLenum
      {
          GL_FALSE = (GLenum) 0,
          ....
      }
 So you can't name it.

Interesting. That is a workaround for a feature which was considered a Good Think (scoped enum names). Why do we need workaround here? Because the language doesn't have native support for non-scoped enum names, and they are still required! If it had, you would have no problem naming the enum type.
  Oh, and note the cast.  Let's make it so that if
 you're defining an enum, an explicit cast is implied.

I don't understand.
Sep 23 2002
next sibling parent "Walter" <walter digitalmars.com> writes:
"Sandor Hojtsy" <hojtsy index.hu> wrote in message
news:amp0es$11ge$1 digitaldaemon.com...
 And Walter:
 Beware! Your (named indexes) solution now include three lists! One list of
 the enum value names, when defining the enum type, and the two other lists
 (merged) when initializing the array. Only the possibility of the "order
 error" was removed, with yet more redundancy. But what about the "missing
 element error"?

The missing element error is caught if the enum value isn't added but the array one is. The missing array value isn't caught, unless you have runtime checking for array values initialized to the default (which you can do in a unittest block). P.S. I've had many subtle bugs from the "order error", so fixing that in D was important to me.
Sep 24 2002
prev sibling parent reply Burton Radons <loth users.sourceforge.net> writes:
Sandor Hojtsy wrote:
 "Burton Radons" <loth users.sourceforge.net> wrote in message
 news:amo25r$1ga$1 digitaldaemon.com...
 
Walter wrote:

"Sandor Hojtsy" <hojtsy index.hu> wrote in message
news:ammnbk$1gvd$1 digitaldaemon.com...

In general: we should consider all the aspects the C #define was used



for,
and try to provide an alternative for the yet unadressed ones.

You can always do some things with a text processor that can't be done reasonably with a symbolic processor, but I think D comes pretty close


to
having a superior solution for at least the common things done with
#define's. I find the C preprocessor inadequate many times, and have


turned
to specialized C programs that generate C source for table building. You


can
see an example in idgen.c in the D front end source.

No, no, Sandor wasn't advocating metaprogramming or preprocessing, just a high level of introspection.

Exactly, in this particular case. I think it would be a more ellegant solution. And Walter: Beware! Your (named indexes) solution now include three lists! One list of the enum value names, when defining the enum type, and the two other lists (merged) when initializing the array. Only the possibility of the "order error" was removed, with yet more redundancy. But what about the "missing element error"?
This level of introspection is in my
port (I think it's the find method on the EnumInfo, wrong OS to check).

Wow!
  But it's had questionable value, as most enums are of the form:

     typedef int GLenum;
     enum : GLenum
     {
         GL_FALSE = (GLenum) 0,
         ....
     }
So you can't name it.

Interesting. That is a workaround for a feature which was considered a Good Think (scoped enum names). Why do we need workaround here? Because the language doesn't have native support for non-scoped enum names, and they are still required! If it had, you would have no problem naming the enum type.

I can't think of a syntax. Scoped enums look like a good thing, but the problem is that lots of the time the enumeration is a language keyword, requiring either "In", "_in", or "IN", none of which look very nice in use. Perhaps: enum typedef GLenum : int { GL_FALSE = 0, .... } Insane, but it works. It doesn't speak "nonscoped enum", but then again, "for" doesn't speak "looping construct"; we only think of it that way because that's what we've learned to associate with it. Oh, right, introspection. I doubt Walter would go for getting the name of an enumeration unless if it has demonstrable necessary value, as it's by far the most memory-intensive part of introspection and hasn't been used in any code of mine yet - and unlike safe varargs, I don't think it's because it's not really useable. Preprocessing could be part of a standard package-building and distribution system. I could have a file like: char [] [] names; int [] values; input syntax line (name is string) (value is integer) { if (type != "line") return; names ~= name; values ~= value; } body { GL_FALSE 0 GL_TRUE 1 ... } output GLenum { println ("typedef int GLenum;"); println ("enum : GLenum\n{"); for (int c; c < names.length; c ++) println (" %s = %d,", names [c], values [c]); println ("}"); } output GLenumToString { char [] [int] match; for (int c = names.length - 1; c >= 0; c --) match [values [c]] = names [c]; println ("char [] GLenumToString (GLenum value)\n{"); println (" switch (value)\n{"); int [] keys = match.keys; for (int c; c <= keys.length; c ++) println (" case 0x%x: return \"%s\";", keys [c], match [keys [c]]); println (" default: return null;"); println (" }\n}"); } Then in the appropriate source: /++ glenums GLenum ++/ /++ glenums GLenumToString ++/ It would then build a preprocessor, run it, and insert the result. One big advantage here is that it doesn't make a mess of debugging; if I were making complex functions, I could trace them.
 Oh, and note the cast.  Let's make it so that if
you're defining an enum, an explicit cast is implied.

I don't understand.

My example was "GL_FALSE = (GLenum) 0,"; I can't write that "GL_FALSE = 0,", because that would be an implicit cast to a typedef. For the definition only, this explicit cast should be implied, as there's about a thousand GL enumerations.
Sep 24 2002
parent reply "Sandor Hojtsy" <hojtsy index.hu> writes:
"Burton Radons" <loth users.sourceforge.net> wrote in message
news:amr5ab$19t2$1 digitaldaemon.com...
  But it's had questionable value, as most enums are of the form:

     typedef int GLenum;
     enum : GLenum
     {
         GL_FALSE = (GLenum) 0,
         ....
     }
So you can't name it.

Interesting. That is a workaround for a feature which was considered a


 Think (scoped enum names).
 Why do we need workaround here? Because the language doesn't have native
 support for non-scoped enum names, and they are still required!
 If it had, you would have no problem naming the enum type.

I can't think of a syntax.

You can't think of a (new) syntax for defining an unscoped enum? unscoped enum GLenum { GL_FALSE = 0; ... }
 Scoped enums look like a good thing, but the
 problem is that lots of the time the enumeration is a language keyword,

For example?
 requiring either "In", "_in", or "IN", none of which look very nice in

 Perhaps:

      enum typedef GLenum : int
      {
          GL_FALSE = 0,
          ....
      }

 Insane, but it works.  It doesn't speak "nonscoped enum", but then
 again, "for" doesn't speak "looping construct"; we only think of it that
 way because that's what we've learned to associate with it.

for speaks "for all" I still can't see how your syntax works. What is the name of the enum type? How many types are you defining here?
 Oh, right, introspection.  I doubt Walter would go for getting the name
 of an enumeration unless if it has demonstrable necessary value, as it's
 by far the most memory-intensive part of introspection

I don't mean that all executables should include enum name introspection tables for all enums! Only for the enums the code is actually using in instrospection. I don't see the size difference between automatically including the enum names where necessary, or manually writing them into the source code as string literals. In the end, they should eat the same memory. The executable will be of the same size. Only source code will be shorter and cleaner.
 and hasn't been used in any code of mine yet - and unlike safe varargs, I

 it's because it's not really useable.  Preprocessing could be part of a
 standard package-building and distribution system.

I think a medium sized project, with an apropriate language should be OK without preprocessing.
 I could have a file like:

Is this a D source code? It uses some features unknown to me.
      char [] [] names;
      int [] values;

      input
      syntax line (name is string) (value is integer)
      {
          if (type != "line")
              return;
          names ~= name;
          values ~= value;
      }
      body
      {
          GL_FALSE 0
          GL_TRUE 1
          ...
      }

      output GLenum
      {
          println ("typedef int GLenum;");
          println ("enum : GLenum\n{");
          for (int c; c < names.length; c ++)
              println ("    %s = %d,", names [c], values [c]);
          println ("}");
      }

      output GLenumToString
      {
          char [] [int] match;

          for (int c = names.length - 1; c >= 0; c --)
             match [values [c]] = names [c];

          println ("char [] GLenumToString (GLenum value)\n{");
          println ("    switch (value)\n{");

          int [] keys = match.keys;

          for (int c; c <= keys.length; c ++)
              println ("        case 0x%x: return \"%s\";", keys [c],
 match [keys [c]]);

          println ("        default: return null;");
          println ("    }\n}");
      }

 Then in the appropriate source:

      /++ glenums GLenum ++/
      /++ glenums GLenumToString ++/

 It would then build a preprocessor, run it, and insert the result.

It? You mean a makefile will build a preprocessor (by invoking the D compiler on what?), the makefile will run the preprocessor, and the preprocessor will insert the result to an other source file, modifying it?
 One
 big advantage here is that it doesn't make a mess of debugging; if I
 were making complex functions, I could trace them.

IMHO source code generation makes debugging slow. You find the error in the pretty generated source, then lookup the generator by hand, try to find the error in that file. You should correct it inside strings (no syntax color), where all kinds of escaping is needed such as backslash-backslash-n, backslash-quote, %%, and such kinds of ugly hard-to-read constructs. Actually a code generator will be so ugly you will start to think about generating the generator itself, with some other tool.
 Oh, and note the cast.  Let's make it so that if
you're defining an enum, an explicit cast is implied.

I don't understand.

My example was "GL_FALSE = (GLenum) 0,"; I can't write that "GL_FALSE = 0,", because that would be an implicit cast to a typedef. For the definition only, this explicit cast should be implied, as there's about a thousand GL enumerations.

Yes. But that problem will also be solved by native unscoped enums, won't it?
Sep 25 2002
parent reply Mark Evans <Mark_member pathlink.com> writes:
One of the things that makes Python so powerful is runtime introspection.  It's
truly awesome.  I would always vote in favor of that feature in any language.

I agree with Sandor's point about escape sequences.  They are ugly and
error-prone.

As usual I don't have an answer, just lots of opinions!

Mark
Sep 25 2002
parent reply Patrick Down <pat codemoon.com> writes:
Mark Evans <Mark_member pathlink.com> wrote in
news:amt5d2$jgm$1 digitaldaemon.com: 

 One of the things that makes Python so powerful is runtime
 introspection.  It's truly awesome.  I would always vote in favor of
 that feature in any language. 

Yes it is truly awesome.
 
 I agree with Sandor's point about escape sequences.  They are ugly and
 error-prone.

It is ugly but often worth it. I think I've litterly saved myself weeks of time just by writing a little code generator for various stuff.
Sep 25 2002
parent reply Mark Evans <Mark_member pathlink.com> writes:
 
 I agree with Sandor's point about escape sequences.  They are ugly and
 error-prone.

It is ugly but often worth it. I think I've litterly saved myself weeks of time just by writing a little code generator for various stuff.

But typically one uses a different language, e.g. Perl/Python/Icon, to auto-gen code in a target language like C, C++, or D. In the time taken to write / test / modify / finalize a code gen tool, I can design preprocessor stuff that will simply compile directly. I have done few projects for which code gen offered a reasonable return on investment. Changes are inevitable, and it's often simpler to make them by hand, than modify a code gen program with all its ugly escape sequences and testing issues. The evolution trend of compilers is toward an incorporation of code generation, such as vtables, templates, and now with .NET, runtime compilation (as I understand .NET). So to ask D for such features is not entirely out of line. Mark
Sep 25 2002
parent reply "Walter" <walter digitalmars.com> writes:
"Mark Evans" <Mark_member pathlink.com> wrote in message
news:amtl9n$152h$1 digitaldaemon.com...
 But typically one uses a different language, e.g. Perl/Python/Icon, to

 code in a target language like C, C++, or D.

Not me - check out \dmd\src\dmd\idgen.c (!) Using some other language is convenient at the time, but usually turns into a significant nuisance 3 years later when you need to build a new version from the source, and can't find a copy of Perl version X.Y.Z or Perl X.Y.Z won't run on the machine you're porting your code to, etc. (That's why I abandoned using Bison.)
 In the time taken to write / test / modify / finalize a code gen tool, I

 design preprocessor stuff that will simply compile directly.  I have done

 projects for which code gen offered a reasonable return on investment.

 are inevitable, and it's often simpler to make them by hand, than modify a

 gen program with all its ugly escape sequences and testing issues.

 The evolution trend of compilers is toward an incorporation of code

 such as vtables, templates, and now with .NET, runtime compilation (as I
 understand .NET).  So to ask D for such features is not entirely out of

I agree that the trend is towards ever more powerful constructs.
Oct 02 2002
parent Mark Evans <Mark_member pathlink.com> writes:
convenient at the time, but usually turns into a significant nuisance 3
years later when you need to build a new version from the source, and can't
find a copy of Perl version X.Y.Z or Perl X.Y.Z 

Well, your mistake is using an ugly cobbled-together excuse for a language like Perl instead of a real language like Python or Icon....Uh-oh, I just started a war..... :-) Mark
Oct 02 2002
prev sibling parent Mac Reiter <Mac_member pathlink.com> writes:
So you can't name it.  Oh, and note the cast.  Let's make it so that if 
you're defining an enum, an explicit cast is implied.

I'm sorry. The phrase "an explicit cast is implied" just struck me as extremely funny... Mac
Sep 24 2002
prev sibling parent reply "Sandor Hojtsy" <hojtsy index.hu> writes:
"Walter" <walter digitalmars.com> wrote in message
news:amncsh$2a3t$1 digitaldaemon.com...
 "Sandor Hojtsy" <hojtsy index.hu> wrote in message
 news:ammnbk$1gvd$1 digitaldaemon.com...
 Voila! Ugly, but no redundancy in the source code.

I thought I was the only one to use that technique!
 Now how you do this in D?
 Without #defines you have to maintain the consistency of the two lists


 yourself. Why can't the complier help in such a mechanical task?

In this case, you can do it with named array initializers:

enum Option { firstOption, secondOption, ... lastOption, optionQ }
 OptionDesc options[optionQ] =
 [
     firstOption : {false, "firstOption"},
     ...
 ];

You still need the enum type declaration. So now you have 3 lists of the same names. Sandor
Sep 23 2002
parent reply "Walter" <walter digitalmars.com> writes:
"Sandor Hojtsy" <hojtsy index.hu> wrote in message
news:amp115$123m$1 digitaldaemon.com...
 You still need the enum type declaration. So now you have 3 lists of the
 same names.

Yes, I know. But the redundancy does give better error checking.
Sep 24 2002
parent reply Mark Evans <Mark_member pathlink.com> writes:
 You still need the enum type declaration. So now you have 3 lists of the
 same names.

Yes, I know. But the redundancy does give better error checking.

Ugh. I think enums should be improved, too. C enums are just not very friendly animals. Take a look at the meta-preprocessor stuff in this code package, http://www.codeproject.com/cpp/tcxunitconverter.asp and ask whether there should be a better way. I do not want to define the same things in 3 places! The preprocessor meta-macros in the preceeding are ugly as sin, but have the virtue of localizing all definitions in one place. I am not proposing preprocessor tricks for D, just some new-think about enums. Mark
Sep 24 2002
parent "Walter" <walter digitalmars.com> writes:
"Mark Evans" <Mark_member pathlink.com> wrote in message
news:amqpnk$t0j$1 digitaldaemon.com...
 I do not want to define the same things in 3 places!  The preprocessor
 meta-macros in the preceeding are ugly as sin, but have the virtue of

 all definitions in one place.

There's a more general problem there about synchronizing multiple tables, enums, symbols, and values. At its most complex, you've got YACC and LEX. D goes a big step forward by supporting named initializers, but it is never going to support things like token concatenation (because that would prevent the separation of lexing and parsing). Best I can suggest is do to things like \dmd\src\d\idgen.c, where one program generates the source for another.
Sep 24 2002