alpha version, October 2000
There is hardly other such a miserably designed programming language as the C++; the more so in the Epoc environment, where eg. you can't use global variables, or where the plain C strings are hardly used ever, being replaced by the descriptors--which exploit awkward "_L" or "_LIT" macros themselves (what's worse, the easier way of _L is the discommended one--a general feature of the Symbian SDK, and definitely a mark of a bad SDK at that! A decent SDK must make the recommended way the easiest one, forcing thus developers painlessly to accept the guidelines!).
Therefore, the XSdk adds many language extensions to make the C++ more flexible and useable. Note that the ultimate aim is to replace the C++ altogether by the Objective C language (or Java, where the full portability is an issue and the effectivity is not). Not that C++ will cease to be supported--it will have its place just the same way the assembler has it now: great and almost inevitable for some particular tasks, whilst absolutely not fit for rapid development of robust and flexible applications.
Naturally, the C++ does not allow the real object-oriented messaging. Though, with some help of the XPreprocessor, some benefits of the true OOP messages are available even in the C++ programming for the Epoc.
The syntax is quite standard ObjC one: just use the primitive [object method] instead of the C++ object->method one. The arguments can be specified anywhere within the method name using the colons; thence, the following C++ expressions
a->b();
a->b(333);
dict->setObjectforKey(obj,key);
out->writeFormat(@"%d %d",1,1);
can be re-written this way:
[a b];
[a b:333];
[dict setObject:obj forKey:key];
[out writeFormat:@"%d %d",1,1];
Note the last example, which shows that commas can be used instead of colons; particularly for the methods with non-fixed arguments it is the best way.
This new ObjC-like message send primitive brings a number of strong advantages, compared with the plain old C++ one:
Of course, we are still in C++; thence, the old primitive can be used whenever appropriate: in an old code, ported from plain C++ to the XSdk, or alike. You can freely mix the old and new primitives any way you might need to:
[CXString stringWith:obj->aFormat(),[other_obj value]]->retain();
is a perfectly legal code (though a bit strange).
In case you want to send a message to this object, you can either use directly this, or a special receiver self, which has exactly the same meaning:
// implementing a method in some class
anotherMethod();
[this anotherMethod];
[self anotherMethod];
all the three variants are equivalent. The second and third one have the huge advantage they cannot be mistaken for a function call: it is quite clear we are invoking a method of an object.
Finally, in case you need to call a method from a superclass implementation, you can use a special receiver super (or SUPER for class methods):
// implementing an instance method in a class, which is a subclass of Xyz class
Xyz::aMethod();
[super aMethod];
Both the variants are functionally equivalent. The first (standard C++ method) is strongly discommended, since it is not flexible enough to support changes in the class hierarchy, and it can cause extremely hard-to-be-found bugs when you use "copy-paste programming".
There is just one small drawback: since the colon is used for the separation of arguments, you must enclose conditional expressions (which contain colon themselves) to parentheses:
[obj methodWithArg:cond?a:b]; // bug! The "cond?a" would be considered first and the "b" second argument
[obj methodWithArg:(cond?a:b)]; // this is all right
Probably the one most hideous feature of the C++ is its class support--or, more precisely, its lack of a class support. The "class" in C++ is just a type; you are not able to store a class in a variable, nor to send it to a function as an argument, nor to check whether a given object is or is not an instance of some class...
Therefore, the XSdk adds the class support to the C++. Whenever you define a new class Xyz, you not only declare the class properties, but automatically define a class named XyzClassClass, which will have exactly one instance (made automatically, and destroyed when the process ends). The instance serves as a Xyz class in the OOP sense.
Thence, you can ask any object in the XSdk for its class; you will get the one instance which represents it. Besides, you get for free a global identifier XyzClass, which represents the class for class Xyz (it works properly even in the DLLs). Therefore, you can easily check for class membership:
Xyz *anObject; // might be an Xyz subclass, or perhaps not...
if ([anObject myclass]==XyzClass) // now I am sure it's Xyz!
You will see the XFoundation framework offers many class-related services; for example, the previous condition could be rewritten as if [anObject isMemberOfClass:XyzClass]. Besides, even the myclass method is naturally part of the XFoundation.
With the class support naturally comes the ability to use untyped objects. Therefore, there are some new types and constants defined:
| types: | id | any object (including any class, since classes are standard objects) |
| Class | any class | |
| constants: | nil | "not an object" |
| Nil | "not a class" |
See the Foundation framework's types and constants for a detailed description of new types, constants and other services.
Naturally, the classes, being objects themselves, can have their own property variables and methods. The class property variables are hardly ever used in normal environments, but can be a blessing in the Epoc DLLs and applications, where global variables are not available: naturally, anything you would normally place in a global, can be represented by a property variable of some appropriate class. Beware, though: while a global variable is truly global, the class property is a property of the class, and is not accessible from other classes--not even from subclasses, which have their own copy of the property!
Class methods allow classes to offer services. The most natural one, of course, is the ability to create new objects: each class offers a variety of methods to create its instances. It is worth to understand the great flexibility of this mechanism, many times better than the C++ language construct new: presume you have a class ServerChannel, which represents just one channel to a given server; its creation might look like this:
ServerChannel *svrChannel=[ServerChannelClass channelFor:...];
where the ellipsis represents some form of server specification. Now, sometimes there will be just one channel to each server, used concurrently by different parts of the application. Sometimes, though, more channels might be needed to load-balance the requests. This one flexible API easily supports both possibilities: the channelFor class method can easily return an already existing channel to be shared, or a newly created channel, according to current demands. Nothing alike possible with the new, which just makes a new object, regardless you really need it or not!
(Incidentally, you might be afraid of problems with destroying of such an object: how to destroy it, if I don't know whether it is shared or not? This is perfectly solved by the XFoundation semi-automatic garbage collector.)
The preprocessor automatically makes a casting macro for any class, named <class name>Cast. Its argument can be any object; it is quite useable with typeless containers, for the C++ type control is unnecessarily strong:
CXString *s=CXStringCast([anArray objectAtIndex:0]);
CXArray *a=CXArrayCast([aDictionary objectForKey:@"Array"]);
Compared with the normal C (or C++) typecast, the <class name>Cast services do check, whether the object is one of proper class (just like the normal cast does in Java). Unlike the C++ casts, this check is performed dynamically in the runtime; thence, it is quite useable in any conditions, including co-operation of objects from different compile units, DLLs, or so. In case you try an object to cast to a class which it is not a member or a subclass of, the service raises an exception.
Just beware in cases you use typeless containers to store different object types; the following (actually improper, but quite frequently used in object languages) code excerpt will not work properly:
CXString *s=CXStringCast([array objectAtIndex:n]);
if ([s isKindOfClass:CXArrayClass]) {
CXArray *a=CXArrayCast(a);
... // use the array a
} else {
... // use the string s
}
Either use the classical C or C++ cast in the first line, or--much more recommended way--rewrite such a code properly, ie. using the id generic object type until the real class is determined. It is much more comprehensible:
id o=[array objectAtIndex:n];
if ([o isKindOfClass:CXArrayClass]) {
CXArray *a=CXArrayCast(o); // (*)
... // use the array a
} else {
CXString *s=CXStringCast(o);
... // use the string s
}
Finally, sometimes, when the speed is crucial, you might remember the class check naturally costs some time. Thence, in the previous example, you might want to use a classical cast in the line marked "(*)":
CXArray *a=(CXArray*)o;
for the real class was determined by the isKindOfClass: method, and the second check is thence superfluous.
The C++ interface and implementation are particularly hideous. They are messed up (you sometimes define methods in the class declaration), the method templates cannot be easily copy/pasted between the interface and implementation, and naturally there is no support for class properties and methods.
Note that in a near future a full ObjC syntax will be available. Until then, to declare a method, say, "-(unsigned)x:(int)i y:(float)f z:(CXString*)s" just remove the parentheses around types, remove all the colons, and move the arguments into a pair of parentheses after the full method name: "-unsigned xyz(int i,float f,CXString *s)".
Thence, the XSdk uses the easy and intelligible Objective C-like syntax. For an interface, you define:
@interface <ClassName>:<SuperClassName>
{
<InstanceProperties>
}
-<InstanceMethod>
...
{
<ClassProperties>
}
+<ClassMethod>
...
@end
The unused parts can be omitted altogether. The properties are declared the same way as in the C++. Methods too, but they are preceded either by a '-' for instance methods, or by a '+' for class ones. As natural in any sensible object oriented environment, all methods are virtual. Methods without arguments can omit the (void) part altogether.
Do please follow the convention of naming classes with the first capital, while variables have (unlike in the Symbian C++ API!) the first letter always lowercase. Not only it makes the source considerably more intelligible, but also the preprocessor sometimes needs to know whether a identifier is a class name or a variable, and since the preprocessor does not contain the full C++ grammar, it cannot use the type information. Therefore, it checks the first letter of the identifier.
Thence, let's look for example on a (partial) interface of the standard XFoundation exception class. Note the declaration of instance methods withou arguments--C++ would make you to place (void) to each of them:
@interface CXException:CXObject
{
CXString *_name,*_reason;
TInt _epocLeaveNumber;
}
-CXString *name;
-CXString *reason;
-TInt epocLeaveNumber;
-void raise;
{
CXException *_currentException;
}
+CXException *exceptionWithNameformat(CXString *name,CXString *fmt,...);
+void raiseformat(CXString *name,CXString *fmt=nil,...);
@end
We can see each exception instance has three property variables (their names are quite descriptive, I hope). On the other hand, the exception class--which, just like all other classes, exist as exactly one instance, created and destroyed automatically--has one instance method, a reference to a current exception.
There are two class methods. The exceptionWithformat just makes new exception object, and initializes it with its arguments (the reason string can be initialized by a printf-like format, thence the ellipsis). For conveniency, there is the raiseformat method: it makes a new exception object, initializes it, and immediately raises it.
On the other hand, the CXException instances offer more methods: there are three accessor methods (XSdk, just as any other truly object oriented environment, strongly discommends a direct acces to properties), and the fifth method, which raises the exception. Thence, you can write a programme like this:
...
if (i>MAX)
[CXException raise:@"Bad Data"
format:@"the index (%d) cannot exceed %d",i,MAX];
...
CXException *exc;
if (i<=0) {
exc=[CXException exceptionWithName:@"Bad Data"
format:@"the index (%d) must be positive",i];
Exception:
... some housekeeping before leaving the method ...
[exc raise];
}
if (...) {
exc=[CXException exceptionWith...];
goto Exception;
}
...
(The @-syntax for strings will be described below).
The implementation does not differ terribly from the interface: only there is no place for the properties, and the methods are defined here, not declared:
@implementation <ClassName>[:<SuperClassName>]
-<InstanceMethods>
+<ClassMethods>
@end
The C++ does not need the information of superclass, since there is no "supercall" there. Thence, it is generally not possible to know which superclass is the proper one. The XSdk performs some tricks which often select the proper superclass automatically, but you can specify the superclass here explicitly, which is absolutely flawless (the drawback is a need to replace the superclass name twice whenever you change the class hierarchy).
The methods are defined just the same way as in the interface (it allows for easy copy/pasting their definitions, lack of which is a great pain in the neck with the plain C++). Only the semicolon is replaced by a method body.
As an example, here is a partial and imprecise implementation of the CXException class (we omit the technicalities with format list and unknown numbers of arguments):
@implementation CXException
...
-CXString *name
{
return _name;
}
...
-void raise
{
TInt leaveNo=_epocLeaveNumber;
[MyClass setCurrentException:this];
User::Leave(leaveNo);
}
...
+void raise(CXString *name,CXString *fmt)
{
[[self exceptionWithName:name format:fmt] raise];
}
...
@end
Note the macro MyClass, which could be used anywhere to get the class of the object, which's code called the macro. (In principle, you could use directly the XFoundation primitive method myclass() mentioned above, but the C++ overstrict treatment of types would make you to explicitly cast it.)
Note also the using of exceptionWithName:format: in the class method: the self means that the method is called from the same object, which's code works now--the class! Generally, you can call (with self or this) any class method from any class method, and any instance method from any instance method. To call an instance method from a class method, you must get an instance just the same way you would in unrelated code. To call a class method from an instance method you can use the macro MyClass.
Like in Java or Objective C, with the preprocessor you can use protocols. A protocol is more or less an interface, it can be roughly compared to the abstract class (in the C++ protocols are converted to them, eventually) in that it specifies a number of abstract services to be implemented by concrete classes.
Though, the protocols are much more convenient than just the abstract classes: primarily, you can enquire any CXObject whether it conforms to a given protocol or not (even in case you do not have any compile-time information on the object, which is often the problem with the plain C++ without RTTI). Then, you can dynamically cast a CXObject pointer to any of the protocols it conforms to, and call securely any method of any the protocol.
Some protocol-related services will be described in the XFoundation framework, since they are implemented as CXObject methods; here we will describe the protocol syntax. Naturally, it is taken from the Objective C: the protocol is defined the very same way as an interface, but for a few details:
For example, a protocol IntValue, which defines a method intValue, a protocol FloatValue, which defines a method doubleValue, and a protocol Values, which inherits them both and adds a method stringValue, we could define this way:
@protocol IntValue
-int intValue;
@end
@protocol FloatValue
-double doubleValue;
@end
@protocol Values <IntValue, FloatValue>
-CXString *stringValue;
@end
For each protocol, two types and a cast are automatically defined. The types represent the protocol and its class part (for the protocol can contain class methods as well as the instance ones), and are named by the protocol, and by the protocol with the ...Class suffix. The cast is named by the protocol with the ...Cast suffix, and can cast properly any object or class to the appropriate type (provided it does conform to the protocol--otherwise an exception is raised):
IntValue *iv=IntValueCast(anyobject);
IntValueClass *ivc=IntValueCast(anyclass); // no class methods defined here
These casts are directly usable for calling protocol methods:
int i=iv->intValue();
CXString *s=ValuesCast(anyobject)->stringValue();
Note that we do not use the [obj method] primitive. The C++ calling is mandatory here, for the casted values are not objects!!!! Repeat, THE CASTED VALUES ARE NOT OBJECTS! Particularly, iv->isXObject() (see the XFoundation framework) would return NO. That is kind of strange, but it is an inevitable result of the extremely idiotical way the C++ implements the abstract classes. Therefore, incidentally, you also can not cast the protocols back:
id anObject;
...
FloatValue *fv=FloatValueCast(anObject);
...
[fv doubleValue]; // raises a "no object" exception, though...
fv->doubleValue(); // ...this is all right and works well
...
id x=(id)fv; // THIS IS DEFINITELY WRONG!
In case you need to call both the protocol methods and the normal class methods, always (!!!) store the object pointer, and cast it to the protocol when needed.
Finally, we need a means to say that some class does conform to a given set of protocols. Just place the protocol names in angle brackets after the @interface directive the very same way they are placed after the @protocol one:
@interface Xxx:CXObject <Values>
...
@end
The class Xxx conforms to the protocol Values. It means that it must implement all its methods (including the inherited ones).
The XSdk runtime support allows to call some methods fully dynamically. That means the we don't need to know even the method name at the compile time; the call is constructed at the runtime. Though generally used in different conditions (particularly, the dynamic calling is used for the link between GUI and engine), the following code might well be used:
...
id obj=...;
...
char buffer[ENOUGH];
gets(buffer);
CXString *s=[CXString stringWithCString:buffer];
XLog(@"User wants me to call a method \"%@\"",s);
if ([obj respondsTo:s]) {
id retval=[obj perform:s with:nil];
XLog(@"Method returned %@",retval);
}
Since it is quite complicated to make a dynamic call in the C++, the support is somewhat limited: primarily, the methods which are to be called this way must have only one argument of type id, and they must return id (or anything convertable, like an integer or BOOL). Besides, they have to be declared explicitly: such a method is called an action, and must be declared and defined between a pair of directives @actions and @endactions. The declaration looks as if they are without arguments, but there is always an automatically added argument value:
@interface Xxx...
...
-id xxx; // not an action
@actions
-id yyy; // is an action
@endactions
...
@end
@implementation Xxx
...
@actions
-id yyy
{
XLog(@"Got a value %@",value);
return nil;
}
@endactions
...
@end
The typeless containers--CXArray, CXSet, and CXDictionary--are used quite often. Therefore, it is worth to extend the language to provide for more easily used constructors for them than the standard ones.
The XPreprocessor understands the @(...) primitive to make an array, the @[...] one to make a set, and the @{...} primitive to make a dictionary. The usage of the first two constructors is straighforward: the "@(" or "@[" is replaced by a "[CXArray arrayWithObjects:"or "[CXSet setWithObjects:" prefix, and the enclosing ")" or "]" is replaced by a ",nil]". Besides, there is a special provision to interpret empty lists as "[CXArray array]" or "[CXSet set]". Therefore, in the following pairs there are functionally equivalent lines:
CXArray *a=@();
CXArray *a=[CXArray array];
CXArray *a=@(@"aa",anObject,@"bb");
CXArray *a=[CXArray arrayWithObjects:
@"aa",anObject,@"bb",nil];
CXSet *s=@[[anObject aMethod]];
CXSet *s=[CXSet setWithObjects:[anObject aMethod],nil];
CXArray *a=@(@[@(@"a",@"b"),@"c"],@(@"d",@[@"e",@"f"]));
CXArray *a=[CXArray arrayWithObjects:
[CXSet setWithObjects:
[CXArray arrayWithObjects:@"a",@"b",nil],@"c",nil],
[CXArray arrayWithObjects:
@"d",[CXSet setWithObjects:@"e",@"f",nil],nil],nil];
The CXDictionary constructor @{...} works in principle the same way, but needs slightly more preprocessing, for its contents are pairs "<key>=<value>". The constructor is automatically converted to the dictionaryWithObjectsAndKeys method:
CXDictionary *d=@{};
CXDictionary *d=[CXDictionary dictionary];
CXDictionary *d=@{@"a"=@"b",key=value,@"c"=@"d"};
CXDictionary *d=[CXDictionary dictionaryWithObjectsAndKeys:
@"b",@"a", value,key, @"d",@"c", nil];
We have already described the [super method] (or [SUPER class_method]) primitives; let's describe in more details what they are good for, since the C++ programmers are not used to them.
Often you need to call the implementation of a method from the superclass--even if the method is overwritten in the current class. There is no support for it in the C++: you had use the superclass' name explicitly (in a costruction line Superclass::method), which could lead to nasty bugs when the class hierarchy changes.
Thence, let's see a small example with the class methods; for instances it would work precisely the same way:
@interface X:CXObject
+void print;
@end
@implementation X
+void print
{
printf("X ");
}
@end
@interface Y:X
+void print;
+void call;
@end
@implementation X
+void print
{
printf("Y ");
}
+void call
{
[this print];
[SUPER print];
}
@end
Now, the statement [YClass call] would print "Y X".
This is sort of a mess, since there are three string representations: (a) the plain C string, which is the only one supported by C++, but good for nothing. (b) the Epoc descriptors, much better than the plain C, but still suffering from a seriously bad design--even that, they are used in half of the Epoc services. (c) the CXString objects from the XFoundation, by far the most luxurious API, but admittedly the least efficient one (which can be an issue in a few specific projects).
The using of macros like _L or _LIT is particularly clumsy. The XSdk therefore offers a special way to enter any string; an example we have seen above, in the CXException examples. Each string begins with a '@'; then there can be some flag characters, and the normal string in parentheses follows. After that, another string in parentheses can be given for localisable string (it will contain a comment for translator, as described below).
The non-localizable strings can have one of the four following formats:
@"... text ..."
@d"... text ..."
@."... text ..."
@.d"... text ..."
The first one just makes an object of the CXString class, and initializes it by the given string data (incidentally, the object is currently made at runtime, but as soon as the compiler properly supports build-time made constant objects, these strings will be made so). The second form with the 'd' flag represents a descriptor. Thence, the two following code excerpts are fully equivalent:
// Symbian Epoc SDK:
_LIT(Kxxxx,"... text ...");
...
User::InfoPrint(Kxxxx);
...
// XSdk:
User::InfoPrint(@d"... text ...");
All the equal strings in scope of one source (or header) file are automatically coalesced, which means that something like methodWithThreeStrings(@d"YES",@d"YES",@d"YES") would be an equivalent of just one _LIT macro, with triple reuse of the appropriate constant. Analogically are the strings coalesced for the CXString variant.
In very rare cases when you might want string not to be coalesced (eg. other software will search for them in the generated DLL or EXE file), use the remaining two forms with the dot flag: they are equivalent to the ones without the dot, but the strings defined this way are never coalesced. Thence, something like methodWithThreeStrings(@.d"YES",@.d"YES",@.d"YES") would be an equivalent of three _LIT macros, all with the same string.
The localizable string support is not fully finished yet. Thence preliminarily, these strings can have any of the following formats:
@"... text ...""[...group...:]... comment ..."
@d"... text ...""[...group...:]... comment ..."
@i"... text ...""[...group...:]... comment ..."
The first form just defines a localizable string, which will be available to translators with the comment from the second parentheses. The whole construction is a CXString object (this time always generated in runtime from a resource file).
Naturally, the strings are coalesced with the scope of the whole compilation unit (generally an application, but it might be, say, an engine subproject, generating its own DLL). Both the string and comment are regarded, thence you can easily make strings which are not coalesced this way:
@"Yes""blahblah:The 'yes' button in the blahblah dialogue"
...
@"Yes""The user's possible answer in..."
These strings will not be coalesced, allowing so the translator to use different equivalents for them in the target language.
The lack of static variables is a pain in the neck in Epoc. Even though the class properties cover the vast majority of cases when you might need to use a static variable, there still are cases when you might need them.
Thence, the XSdk offers a special support for statics via the @Static directive. It can have three forms:
@Static;
@Static(<identifier>)
@Static(<identifier>,<value>)
The first one is used just for technicality, and must stand amongst declarations in some block, which encloses the usage of the other two forms.
The second form return a static value, bound to the identifier; the value has type void*, and default value returned for unknown identifier is NULL. Naturally, you can cast it to any value the void* is convertable to.
Finally, the last form binds the value given to the identifier. The value is automatically converted to the void*, thence it must be convertable so (otherwise a compilation error occurs). The identifiers must be unique in scope of the DLL where they are used.
The standard usage of the static looks like this:
void foo(void)
{
Static;
...
Xyz *anObj=(Xyz*)@Static(FooStaticXyzObject);
...
if (anObj==nil) {
anObj=[Xyz object];
@Static(FooStaticXyzObject,anObj);
}
...
}
but it can be used wherever the plain static variable would fit:
void bar(void)
{
Static;
...
BOOL someSwitch=(BOOL)@Static(SomeSwitchInBar);
...
if (...toggle?...) {
someSwitch=!someSwitch;
@Static(SomeSwitchInBar,someSwitch);
}
...
if (...set?...) {
someSwitch=YES;
@Static(SomeSwitchInBar,someSwitch);
}
...
}
Note: of course it would not be possible to implement this support without the Dll::Tls service; thence, they are relatively slow (compared with the normal direct access to a global variable). Alas, in Epoc it cannot be cured--you have to have it in mind whenever the speed is crucial.
Since the preprocessor is really just a preprocessor, and does not contain a complete C/C++ lexical analyzer, there are some assumptions it has to take. Generally therefore, it is less syntactically forgiving than the standard compilator. Particularly, you have to be cautious with:
Copyright © 1999-2000 X.soft, all rights reserved