Skip to main content
gamedev.net
🔒 Locked

Nitpicky question...

Started by smitty1276 31 replies 1.8k views
Original Post
smitty1276
smitty1276
A buddy of mine got one of those little quizes that potential employers sometimes give, and it had a question on it that I'm curious about. It was wanting to know the pros and cons of various ways of declaring or using constants. The ways given were... #define BLAH ... const int BLAH enum { BLAH = 0, ... }; typedef enum {BLAH, ... } BLAHTYPE; I was thinking that #define has some benefit, because it doesn't actually allocate any memory at runtime. But then I started thinking about the const int approach, and I'm not sure that that one actually does either. Wouldn't that be dealt with at compile time as well? And then the typedef approach, that probably has some casting issues or something, but I'm not really sure at this point. Any one have thoughts or opinions? (Strictly curiosity, by the way... no homework or anything).
stylin
stylin
Prefer not to use #defines. They're not type-safe, don't respect scoping rules and can lead to serious logic errors when the macro goes awry. By default, use a const.

Enums can be useful to isolate a specfic type of constant, eg. days of the week, weapon types, etc. Use these when you need to be sure a specific type is passed, instead of just an integral constant. They also have uses unnamed in classes using certain template techniques.

typedef enum {} enum_type: I have no idea why this would be useful.
:stylin: "Make games, not war." "...if you're doing this to learn then just study a modern C++ compiler's implementation." -snk_kid
Will F
Will F
A bit more info on const int BLAH vs a #define from the C++ FAQ Lite
Quote:

const identifiers are often better than #define because:
* they obey the language's scoping rules
* you can see them in the debugger
* you can take their address if you need to
* you can pass them by const-reference if you need to
* they don't create new "keywords" in your program.
Enigma
Enigma
  • #define
    • pros:
      • none
    • cons:
      • names do not respect scoping rules
      • no type safety
      • no symbol table entries
      • do not prevent user passing invalid constants

  • const int
    • pros:
      • names respect scoping rules
      • type safe
      • have symbol table entries
    • cons:
      • all constants have same type
      • do not prevent user passing invalid constants
      • on older compilers cannot be used to provide a constant integral class member for use in declaring the size of a member array

  • enum {...};
    • pros:
      • names respect scoping rules
      • type safe
      • have symbol table entries
      • different enums have different types
    • cons:
      • do not prevent user passing invalid constants
      • enum type cannot be referred to and therefore enum values cannot be stored in a type safe manner

  • typedef enum {...}...;
    • pros:
      • names respect scoping rules
      • type safe
      • have symbol table entries
      • different enums have different types
      • enum type can be referred to and therefore stored
    • cons:
      • do not prevent user passing invalid constants

Then there's my preferred method, which is to declare a new class with static instances representing the constants. It has more overhead to generate and cannot be used to declare the size of a member array, but has no other cons that I am aware of:
class MyConstant{	public:		static MyConstant const Value1;		static MyConstant const Value2;		static MyConstant const Value3;		bool operator==(MyConstant const constant) const		{			return id_ == constant.id_;		}		bool operator!=(MyConstant const constant) const		{			return !operator==(constant);		}		MyConstant operator&(MyConstant const constant) const		{			return MyConstant(id_ & constant.id_);		}		MyConstant operator|(MyConstant const constant) const		{			return MyConstant(id_ | constant.id_);		}		// I prefer to have a named constant for "none" and no implicit		// conversion to bool		operator bool() const		{			return id_ != 0;		}	private:		MyConstant(unsigned int id)			:			id_(id)		{		}		MyConstant & operator=(MyConstant const & constant);		unsigned int const id_;};MyConstant const MyConstant::Value1 = MyConstant(0x1);MyConstant const MyConstant::Value2 = MyConstant(0x2);MyConstant const MyConstant::Value3 = MyConstant(0x4);

The supported operations can be changed as required (remove logical ops, add relational ops etc.). The static constants can also be changed to static functions which return references to local static objects if non-local static initialisation order is an issue.

Enigma
mfawcett
mfawcett
Quote:
Original post by Enigma
*** Source Snippet Removed ***
The supported operations can be changed as required (remove logical ops, add relational ops etc.). The static constants can also be changed to static functions which return references to local static objects if non-local static initialisation order is an issue.

Enigma

I do something similar.
class Justification{public:	enum { LEFT, CENTER, RIGHT, STRETCH };	int asInt(void) const { return justification; }	static Justification left(void) { return LEFT; }	static Justification center(void) { return CENTER; }	static Justification right(void) { return RIGHT; }	static Justification stretch(void) { return STRETCH; }private:	int justification;	Justification(int i) : justification(i) { }};


You could typedef the enum if you wanted, but the only reason the enums are there are for convenience for users using switch statements.

Since I am using static functions, you can't do a switch like:
switch (justification.asInt()){	case Justification::left():		break;	case Justification::center():		break;	// etc..}

because Justification::left() is not an ICE (actually, this bugs me, because just like std::numeric_limits::max(), it really is a constant, so why can't we switch on it?). So without the enums you can do:

switch (justification.asInt()){	case 0:		break;	case 1:		break;	// etc..}

but using the integral constants loses context information for the casual reader. My preferred method is:
switch (justification.asInt()){	case Justification::LEFT:		break;	case Justification::CENTER:		break;	// etc..}

I'm certainly not tied to this implementation though, I just never bothered coming up with better.

--Michael Fawcett
smitty1276
smitty1276
Wow, Enigma, that was a very helpful answer. Thanks! You too mfawcett.
Way Walker
Way Walker
Quote:
Original post by Enigma
  • #define
    • pros:
      • none



There is one pro: In C, const means "read only" so #define is the way to get a true constant. Sure, "none" is more dramatic, but one should give credit where credit is due.

Quote:

  • cons:
    • names do not respect scoping rules



This is the big one.

Quote:

  • no type safety



In C, this is only an issue with variadic functions, but what isn't an issue with variadic functions? At least, I haven't seen an example where type safety was an issue (I would love an example to prove me wrong). I'm not entirely sure this is even a big problem in C++, but my knowledge there is weaker.

Quote:

  • no symbol table entries



I've seen #define names in debuggers before, so unless you need to take the address this isn't really an issue (and I think C99 even allows a way around that).

Quote:

  • do not prevent user passing invalid constants



True, but listed for every entry.

I'm not supporting #define, it just rubs me the wrong way when someone complains about #define's typesafety, especially when talking about constants (there's a better case for typesafety issues in macros). It's as much a knee-jerk reaction as some people's reaction to #define/goto/etc.

Take what I say with a grain of salt, aside from RAII I dislike C++.
Telastyn
Telastyn
The problem comes in the general compatability between certain integers and pointers. A non-typesafe integer can be mistakenly assigned to a pointer, causing bogus memory to be accessed, leading to a crash; followed by the tedious process of tracking down the run-time bug. Compile time bugs are far more straightforward in their fixing.

That said, sometimes having the #define be not-type safe is an advantage.

Not often though. Frankly, I don't find much use for constants anymore. Far more often the ability to specify the constants programatically or configure them via file/server at initialization is of greater benefit.
ZQJ
ZQJ
#define has one pro - you can do something like the following:

#define some_value (get_some_value())

This is done somewhere in the standard Linux header files (I think it's for thread safety); but I can't remember where.

Edit: I remember, it's errno. And it's defined more like:

#define errno (*get_errno_addr())
LessBread
LessBread
Preprocessor abuse...
"I thought what I'd do was, I'd pretend I was one of those deaf-mutes." - the Laughing Man
snk_kid
snk_kid
Quote:
Original post by Way Walker
There is one pro: In C, const means "read only" so #define is the way to get a true constant.


Erm const has been C since C89.

Erzengeldeslichtes
Erzengeldeslichtes
Quote:
Original post by Enigma
  • #define
    • pros:
      • none

I disagree. The pro to #define is that it is available to preprocessor conditionals.

const int VERSION = 5;#if VERSION == 5 //I warn you: !defined VERSIONconst char* Out = "Welcome";#elseconst char* Out = "Hello";#endif

Out is "Hello".
#define VERSION 5#if VERSION == 5const char* Out = "Welcome";#elseconst char* Out = "Hello";#endif

Out is "Welcome".


However, that is the only "pro" I see in C++. I would list the flexability to do ZQJ's bit of voodoo as both a pro and a con...

[Edited by - Erzengeldeslichtes on October 21, 2005 6:39:49 AM]
----Erzengel des Lichtes光の大天使Archangel of LightEverything has a use. You must know that use, and when to properly use the effects.♀≈♂?
snk_kid
snk_kid
Quote:
Original post by LessBread
Preprocessor abuse...


To some, but beauty is in the eyes of the beholder - Boost.Preprocessor [grin][lol]
ZQJ
ZQJ
Quote:
Original post by LessBread
Preprocessor abuse...


Preprocessor abuse it may be, but it's about the only way to make C error reporting thread safe. I guess that's not really the point of the topic though.
mfawcett
mfawcett
Quote:
Original post by Way Walker
Quote:

  • do not prevent user passing invalid constants



True, but listed for every entry.

I'm not supporting #define, it just rubs me the wrong way when someone complains about #define's typesafety, especially when talking about constants (there's a better case for typesafety issues in macros). It's as much a knee-jerk reaction as some people's reaction to #define/goto/etc.

Take what I say with a grain of salt, aside from RAII I dislike C++.

Not true for every entry. The examples Enigma and I posted both stop the user from passing invalid constants. Although I'm sure they won't be to your liking since you dislike C++.
--Michael Fawcett
me22
me22
typedef enum {BLAH, ... } BLAHTYPE;
can be better done as
enum BLAHTYPE {BLAH, ... };
at least in C++.

Quote:
Original post by smitty1276
I was thinking that #define has some benefit, because it doesn't actually allocate any memory at runtime. But then I started thinking about the const int approach, and I'm not sure that that one actually does either. Wouldn't that be dealt with at compile time as well?


enums are also guaranteed to not allocate memory at runtime, which is not the case with consts. This is especially relevant for static consts in templated classes, because if they end up /not/ being no-storage-allocated-compile-time-constants, every time you instatiate the class you use up some memory. It's also a good way to be sure that your array size constants or integral template parameters are actually compile-time constant.
ZQJ
ZQJ
Quote:
Original post by me22
This is especially relevant for static consts in templated classes, because if they end up /not/ being no-storage-allocated-compile-time-constants, every time you instatiate the class you use up some memory.


Surely if they're static they only use up the memory once (and are initialised at startup)?

Way Walker
Way Walker
Quote:
Original post by snk_kid
Quote:
Original post by Way Walker
There is one pro: In C, const means "read only" so #define is the way to get a true constant.


Erm const has been C since C89.


I know that. However, const in C means something different than it does in C++. The canonical example is the size of an array:
const int size = 10;int main(void) {    int array[size] = {0};    return 0;}

This produces an array in C++, a VLA in C99, and is illegal in C89. const means "constant" in C++ and "read only" in C.

Quote:
Original post by mfawcett
Not true for every entry. The examples Enigma and I posted both stop the user from passing invalid constants. Although I'm sure they won't be to your liking since you dislike C++.


Yes, of course. I was refering to the four options given by the OP and in Enigma's list. Your preferred method was basically a footnote in Enigma's post. I'm sure this was because it's not directly related to smitty1276's question.

For what it's worth, the best word I can think of to describe your solution is "intriguing". Using classes to encapsulate constants is an interesting idea. I have two objections:

1) It's not perfect. For integer constants, it doesn't really buy you anything more than a typedef'd enum. A compiler should warn you when using a constant that isn't valid for the enum, and you should really be using the named constants anyway. So, it looks to me, in its current form, to be a lot of work for something not much better than a typedef'd enum. Its major benefit is that you can now have non-integer enums. I wonder if it can be "perfected" (obviously you can't prevent a determined hacker, but see my second objection). I remember seeing something on trying to create a max() template that behaved, in all respects, better than "#define MAX(a,b) (((a)>(b))?(a):(b))". It took a lot, but I think it ended up with a solution. Of course, the only real difference is that the template wouldn't evaluate its arguments twice.

2) I cut my teeth on Smalltalk where there's a lot more trust/respect for your fellow programmers. All messages are public. C++'s private/protected is approximated by putting messages in a category designated private. If another programmer decides to use a message from this category, he's been warned and accepts the consequences. It's assumed that he has weighed the consequences and decided it's worth it. Similarly, I've heard some Java programmers argue that private should never be used and everything that would have been private should be protected. This allows access to those internals should the need arise, but still hides them from a casual user.
Polymorphic OOP
Polymorphic OOP
Quote:
Original post by Enigma
  • #define
    • pros:
      • none
    • cons:
      • names do not respect scoping rules
      • no type safety
      • no symbol table entries
      • do not prevent user passing invalid constants


That's really untrue. There are many benefits of using #defines. For one, they are useable as preprocessor constants, unlike const integer types and enums constants. This makes them able to be used in preprocessor expressions, and are particularly useful for any preprocessor metaprogramming.

As well, #defines allow you to give names to floating point constants and still have them be preprocessor and compile-time constants. const floats and const doubles are not useable at compile-time nor at preprocessor time, whereas #defines are on both accounts since they represent literals.

#defines can also be just as typesafe as literals, since that's what they usually directly represent anyway. Using PI in an expression as typesafe as using 3.14159... as a literal in an expression. The #define just gives it a name.

Still, you should prefer using const integral types instead wherever you can, since a compile-time integer constant is mostly what you need and you get proper scoping and semantics of variables. #defines, however, do still have their uses. Saying they have no "Pros" is just wrong.
mfawcett
mfawcett
Quote:
Original post by Way Walker
1) It's not perfect. For integer constants, it doesn't really buy you anything more than a typedef'd enum. A compiler should warn you when using a constant that isn't valid for the enum, and you should really be using the named constants anyway. So, it looks to me, in its current form, to be a lot of work for something not much better than a typedef'd enum. Its major benefit is that you can now have non-integer enums. I wonder if it can be "perfected" (obviously you can't prevent a determined hacker, but see my second objection). I remember seeing something on trying to create a max() template that behaved, in all respects, better than "#define MAX(a,b) (((a)>(b))?(a):(b))". It took a lot, but I think it ended up with a solution. Of course, the only real difference is that the template wouldn't evaluate its arguments twice.


But you can't use the integer constants except for read access, so it will always be one of the valid integer constants. In the typedef'd enum situation, you have:

typedef enum { LEFT, CENTER, RIGHT } Justification;void justify(Justification j){	switch (j)	{		case LEFT:			// stuff.			break;		case CENTER:			// stuff.			break;		case RIGHT:			// stuff.			break;		default:			// This is possible with typedef'd enums	}}int main(void){	Justification j;	// Valid with typdef'd enums	justify(j);		// What will this do?}


With the method I posted, it is not possible to generate invalid Justifications.
class Justification{public:	enum { LEFT, CENTER, RIGHT };	int asInt(void) const { return justification; }	static Justification left(void) { return LEFT; }	static Justification center(void) { return CENTER; }	static Justification right(void) { return RIGHT; }private:	int justification;	Justification(int i) : justification(i) { }};void justify(Justification j){	switch (j.asInt())	{		case Justification::LEFT:			// stuff.			break;		case Justification::CENTER:			// stuff.			break;		case Justification::RIGHT:			// stuff.			break;		default:			// This is impossible with our implementation	}}int main(void){	Justification j;	// Impossible!  Compile-time error	justify(Justification::left());}
--Michael Fawcett

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.