Our solution provides a novel (first of its kind) translation of the ALF syntactical minimum conformance to C++ except for the following. LinkOperationExpression, used in ALF to create or destroy the links of an association, is not included since associations are not part of the minimum conformance. BitStringUnaryExpression, used in ALF for unary operations on the type BitString, is not included since the type BitString is not conceived in C++. ClassExtentExpression, used in ALF to obtain the objects in the extent of a class, is not included since it is not possible to search all instances of a particular class in C++. Sending instances of a signal in FeatureInvocationExpression is not supported since signals are not part of the minimum conformance. Moreover, it provides the translation of a subset of ALF units, not included in the minimum conformance, for allowing the modeller to define an application using ALF only. In the following, we provide the technology-agnostic description of the supported mappings between ALF abstract syntax elements and corresponding C++ concepts reflecting the order in which ALF syntax elements are described in the official ALF specification (i.e. expressions, statements, units). Since exemplifying all the possible cases of use each syntax element is not possible (they are infinite), we aim at providing a set of representative examples for the reader to be able to reproduce the mappings with the transformation technology of her choice.
Note that, whenever the type of an element (e.g. qualified name, return values) influences the translation from ALF to C++, we exploit our type deductions mechanism for identifying types; in the next sections we highlight the most interesting cases. However, alternative ways to deduce types could be exploited and the mappings described in the following sections are not dependent on the specific deduction mechanism.
Expressions
Expressions are behavioural units that evaluate to collections of values. In this section, we describe the mappings between the covered types of expression from ALF to C++.
Qualified names
QualifiedName is used to identify a UML named element, which may or not be a member of one or more namespaces. To avoid unpredictable C++ code, we did not cover the PotentiallyAmbiguousQualifiedName concept. The remaining concepts are mapped as follows. A qualified name is constituted of non-empty set of bindings, either NameBinding or PositionalTemplateBinding (or a combination of the two). Bindings are separated by colons (‘::’), in case of ColonQualifiedName, or dots (‘.’), in case of DotQualifiedName.
Separation in terms of colons or dots is not univocal in C++ since it depends on the types of the objects represented by NameBinding. More specifically, if the preceding NameBinding represents a class object, colons and dots are mapped to C++’s arrow operator ‘->’ (Case 1, 2, 4); if it represents a property of primitive type, colons or dots are kept in C++ too (Case 3). Regarding PositionalTemplateBinding, since we use smart pointers, the translation is done by wrapping the most internal NameBinding in the shared_ptr<T> construct if the name represents a non-primitive type T (Case 5).
The possibility to distinguish among the various cases is given by our type deduction mechanism. More specifically, before translating QualifiedName, we navigates all the bindings composing it and identify the type of each of them. On one hand, in Case 2, type deduction identifies that classA is declared in a parent scope as an instance object of class ClassA; for this reason, the dot ‘.’ operator in ALF is replaced by the arrow ‘->’ operator in C++ (the same applies to Case 1, 4). On the other hand, in Case 3, property is defined in a parent scope as integer, and thereby the dot ‘.’ operator is kept in the resulting C++. In Case 5, PositionalTemplateBinding is done on ClassA and ClassB, which are both identified as class types in their parent scope by the deduction mechanism; the translation is done by wrapping them in the shared_ptr
\(<>\) construct (Table 1).
Table 1 Mapping of QualifiedName
Literal expressions (Primary expressions)
LiteralExpression is composed of a single primitive literal. Since we aim at providing a translator which provides predictable C++ code, we did not account the primitive UnboundedValueLiteralExpression since there is no standard way to translate it to a safe unbounded type in C++. Note that we do not forbid the use of unsupported ALF concepts at modelling level (e.g. through OCL constraints) since models are not only used for code generation purposes. Nevertheless, warnings are issued when generating code from models containing unsupported ALF concepts.
Concerning the other types, BooleanLiteralExpression, StringLiteralExpression and NaturalLiteralExpression, they have a natural corresponding in C++. For instance, a BooleanLiteralExpression in ALF can either be represented by true or false values of a boolean (bool) in C++.
Name expressions (Primary expressions)
This syntax element represents the value denoted by a QualifiedName. The mapping to C++ is given by the corresponding qualified name (see Sect. 5.1.1).
‘This’ expressions and Parenthesized expressions (Primary expressions)
ThisExpression consists of the keyword this, and it is translated to the same keyword in C++. ParenthesizedExpression represents an expression contained by parentheses; parentheses are simply reproduced in C++, but the contained expression must be properly translated according depending on the expression type.
Property access expressions (Primary expressions)
PropertyAccessExpression is used to access the value of a property owned by the instance of a classifier. The expression is defined in terms of a feature reference, pointing to a target primary expression and to a name of a property of the type of the target primary expression. The translation is done by translating the primary expression according to its specific type and relating it to the name of the property to be accessed. In ALF, primary expression and names (NameBinding) are separated by the dot ‘.’ operator, while in C++ this depends on the type of the object to be accessed; this is solved in the same way as for QualifiedName (see Sect. 5.1.1).
Invocation expressions (Primary expressions)
This expression represents an invocation to a behaviour and a Tuple, which represents the arguments for the parameters of the invocation. An invocation can be of the following types: BehaviorInvocationExpression, FeatureInvocationExpression and SuperInvocationExpression. Tuple and invocation types are described in the following four sections.
Tuple
A Tuple is a list of expressions that describe the arguments for an invocation. They can be positional or named tuples; we provide a translation for positional tuples, since the current specification of C++ does not provide the concepts needed for representing named tuples. A Tuple is translated by iterating on the list of expressions it represents, singularly translate each of them and concatenate their translation using the comma separator ‘,’. The concatenation is put within the parentheses of the translated InvocationExpression. Single expressions are translated according to their type (Table 2).
Behaviour invocation expressions (Primary expressions)
It is the simplest type of invocation, and it is represented by a QualifiedName representing the behaviour to be invoked. The translation follows the same rules as the ones defined for QualifiedName (see Sect. 5.1.1). Note that in order for a behaviour called from a model library to be correctly translated, a C++ library corresponding to the model library should be in place. In case C++ library and model library do not share the same naming convention, a wrapper (external to this code generator) should be provide to bridge the differences.
Feature invocation expressions (Primary expressions)
This expression has a feature reference as its target, and thereby translated following the rules defined for PropertyAccessExpression (see Sect. 5.1.5), and a final NameBinding representing an operation call. In Case 1, an operation call on a property of primitive type keeps the same syntax in C++. In Case 2, an operation call on a class object is translated by separating the final NameBinding and the accessed property by the arrow operator ‘->’. Case 3 and 4 represented cascaded feature invocation. In Case 3, the return value of op() is of primitive type; thereby, it is accessed by the final NameBinding op2() through the dot operator ‘.’. In Case 4, the return value of op1() is a class object; thereby, it is accessed by the final NameBinding op2() through the arrow operator ‘->’ (Table 3).
Table 3 Mapping of FeatureInvocationExpression
Super invocation expressions (Primary expressions)
This invocation is used to invoke an operation of a superclass of the current class. Its syntax is similar to FeatureInvocationExpression, but, instead of a feature reference, it has the keyword super as target. The mapping is the same as for FeatureInvocationExpression except for the keyword super, which is instead substituted by the QualifiedName of the superclass (Table 4).
Table 4 Mapping of SuperInvocationExpression
Instance creation expressions (Primary expressions)
InstanceCreationExpression represents the creation of a new instance of a class or data type. It consists of the keyword new followed by a name (possibly qualified) representing the constructor method and a tuple representing eventual parameters for it. The new instance to be created is wrapped in a smart pointer through the construct make_shared
\({<}{>}\) (Case 1). In case the constructor is retrieved through a PositionalTemplateBinding, the most internal NameBinding is wrapped in a shared_ptr<T
\({>}\) if it represents non-primitive type T (Case 2). Through our type deduction mechanism, we identify the type of the instance to be created as a class type and wrap it in the make_shared
\({<}{>}\) construct for initialising a smart pointer for it (Table 5).
Table 5 Mapping for InstanceCreationExpression
Sequence construction expressions (Primary expressions)
SequenceConstructionExpression groups values into a sequence of a specified type. It is represented by a list of expressions enclosed in curly braces and preceded by the specific type and the multiplicity brackets. A SequenceConstructionExpression that begins with the keyword new indicates an InstanceCreationExpression for which a sequence of values is constructed too (Cases 2 and 3). In Case 1, we can see the construction of a sequence of integers and in Case 2 the construction a new array of strings. The mapping is pretty straightforward, except the fact that arrays are mapped to C++’s ‘vector’. A more complex case is depicted in Case 3, where a sequence of class objects is constructed by directly creating a new object of the class as first element of the sequence. In this situation, the NameBinding representing the sequence is wrapped into a shared_ptr
\({<}{>}\), while the NameBinding representing the new class object is wrapped into a make_shared
\({<}{>}\).
Through our type deduction mechanism, we can identify class types (Case 3) and wrap them in the shared_ptr
\({<}{>}\) construct for leveraging smart pointers (Table 6).
Table 6 Mapping of SequenceConstructionExpression
Sequence access expressions (Primary expressions)
SequenceAccessExpression is exploited to retrieve the element in a specified position of a sequence. It is composed of two expressions, one identifying the sequence followed by one evaluating to an integer representing the index of the element to be retrieved and enclosed in brackets. The two expressions are transformed individually depending on the expression type. The structure of SequenceAccessExpression coincides in ALF and C++.
Increment and decrement expressions
This type of expressions uses the increment operator ‘\(++\)’ or the decrement operator ‘\(--\)’ in a prefix (operator before operand) or postfix (operator after operand) form for increasing or decreasing an integer operand represented by either a feature reference (FeatureLeftHandSide) or a qualified name (NameLeftHandSide), and an index expression. If the operand is represented by a feature reference, the translation is done according to what is defined for PropertyAccessExpression (see Sect. 5.1.5), while, if represented by a qualified name, it is done as for QualifiedName (see Sect. 5.1.1). The expression providing the index, if any, is translated depending on the expression type.
Boolean unary expressions (Unary expressions)
BooleanUnaryExpression is a unary expression composed of: an operand expression which evaluates to a boolean value and the negation operator ‘!’. Its translation is done by properly translating the expression representing the operand, according to the specific expression type, which is preceded by the negation operator ‘!’.
Numeric unary expressions (Unary expressions)
NumericUnaryExpression is a unary expression composed of: an operand expression which evaluates to a boolean value and a numeric operator ‘\(+\)’ or ‘−’. Its translation is done by properly translating the expression representing the operand, according to the specific expression type, which is preceded by the numeric operator.
Cast expressions (Unary expressions)
CastExpression is used to cast an operand expression to the type given by a QualifiedName. The translation is done by translating the operand expression according to the specific expression type and the type according to the rules defined for QualifiedName (see Sect. 5.1.1) (Case 1). In the specific case in which the type to cast to is defined as any, the type is meant to be derived dynamically at runtime. In this case, any is translated to auto_cast (Case 2). Type deduction mechanisms support the translation in distinguishing the two cases. Since cast operations are not safe by definition, it is up to the modeller to ensure that the conversion is safe (Table 7).
Table 7 Mapping of CastExpression
Binary expressions
A binary expression is composed of two operand expressions separated by a binary operator. Its translation is done by properly translating the expressions representing the operands, according to the specific expression types, and separating them by the specific binary operator. Type deduction mechanisms are exploited for deriving the type of the operands.
ArithmeticExpression is characterised by an arithmetic operator (\(+, -, *, /, \%\)). Note that arithmetic operator symbols as well as their associativity and precedence rules coincide in ALF and C++.
Table 8 Mapping of ClassificationExpression
Table 9 Mapping of AssignmentExpression
ShiftExpression is characterised by a shift operator (\(<<\) signed left shift, \(>>\) signed right shift, \(>>>\) unsigned right shift). While signed left and signed right shift operator symbols as well as their associativity and precedence rules coincide in ALF and C++, unsigned right shift (\(>>>\)) is not available in the C++ specification; hence, we do not enforce its translation.
RelationalExpression is characterised by a relational operator (\(<,>, <=, >=\)). Relational operator symbols as well as their precedence rules in ALF and C++ coincide.
ClassificationExpression is a peculiar type of binary expression where, instead of the second operand expression, there is a QualifiedName. ClassificationExpression is used to check the result of the operand expression against a certain type represented by QualifiedName. Operand and type are separated by a classification operator (instanceof, hastype). The operand expression is translated according to the expression type, while the type according to the mapping for QualifiedName (see Sect. 5.1.1). Since the two operators do not have a direct correspondent in C++, we provide the following mappings. In the case of instanceof, the expression checks whether the result of the operand expression has the same dynamic type of the given type represented by QualifiedName or a direct or indirect subclass of it. In order to reproduce this behaviour, we dynamically cast the operand to a pointer representing the type we want to check the operand’s type with through the dynamic_cast
\(<>\) operator, and then we check that the result of the casting is not zero (Case 1). In the case the hastype, the expression checks whether the result of the operand expression has the same dynamic type of the given type represented by QualifiedName. In order to reproduce this behaviour, we extract the type identifiers of the operand and the given type using C++’s typeid() function and compare them through the equality operator ‘\(==\)’ (Case 2) (Table 8).
EqualityExpression, LogicalExpression and ConditionalLogicalExpression coincide in ALF and C++.
Conditional test expressions
ConditionalTestExpression has three operand expressions. The first represents a boolean, and depending on its value, the expression selects either the second or the third operand as result. The mapping is done by translating the three operand expressions according to their expression type and separate them with the symbols ‘?’, between first (boolean) and second operand, and ‘:’ between second and third operand. Conditional test operator symbol ‘?’ and its associativity and precedence rules coincide in ALF and C++.
Assignment expressions
AssignmentExpression represents the assignment of a value represented by a right-hand side expression to a left-hand side which can be either a feature reference (FeatureLeftHandSide) or a qualified name (NameLeftHandSide) and can have an index expression. If the left-hand side is represented by a feature reference (Case 1), the translation is done according to what defined for PropertyAccessExpression (see Sect. 5.1.5), while, if represented by a qualified name (Case 2), it is done as for QualifiedName (see Sect. 5.1.1). The expression providing the index, if any (Case 3), is translated depending on the mapping rules for indexing (see Sect. 5.1.21). A simple assignment is done through the assignment operator ‘\(=\)’. A compound assignment compounds a binary operator with the assignment operator (Case 4) (Table 9).
Table 10 Indexing conversion
Table 11 Mapping of LocalNameDeclaration
Indexing
Since in ALF indexing starts from 1 while in C++ it starts from 0, we need to explicitly make the conversion as follows (Table 10):
-
if the index expression is represented by a numeric literal, then we subtract 1 to it (Case 1);
-
if the index expression is not a numerical literal, we translate the expression and concatenate ‘\(-1\)’ to it (Case 2).
Table 12 Mapping of IfStatement
Table 13 Mapping of SwitchStatement
Statements
Statements are segments of behaviour that, when executed, produce an effect rather than values. A sequence of statements (block) is a list of ALF statements placed side by side in a linear order. These sequences may be included in UML models for specifying behaviours (scenario 2). In this section, we describe the mappings between the covered types of statement from ALF to C++.
In-line statements
InLineStatement allows the modeller to embed code in a language other than ALF in an ALF block. No translation is needed in this case since in-line code, if defined in terms of the target language entailed by the transformation, is simply copied as it is in the output code. In our case, we provide support for C++ in-line code, while ignore in-line code defined in other languages.
Block statements
BlockStatement represents a block to be executed and can be seen as the container of statements sequence enclosed in curly braces. The concept of block is equally conceived in C++. Before translating a block, the type deduction mechanism creates a sub-scope representing the block’s scope within the current scope.
Local name declaration statements
LocalNameDeclaration is a statement which is employed for defining a local name together with its type and initialisation value. It is composed by a name declaration, which can include a multiplicity indicator, and an initialisation expression which can either initialise a sequence, a new instance or be another expression which evaluates to the type of the name to be declared. The syntax of the name declaration has two variants:
-
‘let name : Type’, inherited from UML (Case 1);
-
‘Type name’, specific to ALF (Case 2).
Both cases are translated to C++ in the form ‘Type name’. If the initialisation expression defines the initialisation of a new sequence (Case 1), then the expression is translated according to the rules defined for SequenceCreationExpression (see Sect. 5.1.12). In case it defines the initialisation of a new instance (Case 2), the translation follows the rules for InstanceCreationExpression (see Sect. 5.1.11). For the other types of initialisation expressions, the translation is done by the rules defined for the specific expression type (e.g. ArithmeticExpression in Case 3) (Table 11).
Expression statements
It is an Expression followed by a semicolon, and it translated according to the rules defined for the specific expression type with a semicolon at the end.
If statements
IfStatement represents the conditional execution of a non-empty set of blocks. It is composed by an ordered set of sequential non-final clauses each of which having a condition in terms of a condition expression evaluating to a boolean and a body represented by a block. IfStatement can have a final clause with a block to be executed in case none of the non-final clauses can be executed. Its translation to C++ is done by iterating on the non-final clauses in their order and for each of them transforming the condition expression according to the specific expression type and the statements sequence representing the block (each of the statements will be translated according to the specific type of statement). Sequential non-final clauses are concatenated through the ‘else’ keywords both in ALF and in C++. The final clause is translated by translating the related block and concatenating it to the last non-final clause through the keyword ‘else’. Before translating IfStatement, the type deduction mechanism creates a sub-scope representing the IfStatement block’s scope within the current scope (Table 12).
Table 14 Mapping of ForStatement
Switch statements
SwitchStatement executes one of a set of blocks depending on the value of an expression. The body of the SwitchStatement is made of a list of clauses; each clause consists of a set of case labels and a block. Each case label contains an expression that must evaluate to a single value of a type conforming to the one of the switch expression. As for IfStatement, a switch statement can have a final clause. Case labels are represented by expressions that are dynamically evaluated. In C++, case labels can only be represented by constant expressions; for this reason, we map SwitchStatement to C++’s if-statement. More specifically, we iterate on the set of case labels, which are properly transformed into conditional expressions of if and else if; the final clause is translated into an else without conditional expression. The clauses are translated as follows. An equality condition expression is created to resemble the switch’s cases. Note that multiple cases are translated by creating the related conditional expressions and then using them as operands for a conditional-OR (||) expression. The blocks representing case bodies are translated too. Before translating each clause, the type deduction mechanism creates a sub-scope representing the clause block’s scope within the current scope (Table 13).
While and Do statements
WhileStatement and DoStatement are iteration loops which evaluate a condition expression (returning a boolean), and until it becomes false, it executes a block. The translation is done by transforming the condition expression according to its type and statements sequence representing the block. The structure of while-statement and do-statement in ALF and C++ coincide. Before translating WhileStatement or DoStatement, the type deduction mechanism creates a sub-scope representing the specific block’s scope within the current scope.
For statements
ForStatement iterates the execution of a block while assigning a loop variable to successive values of a sequence until it reaches the end of the sequence. The translation is done by transforming the loop variable according to its type and then transforming the block. When translating the loop variable, not all the cases have a direct translation to C++. The loop variable can be defined as a name label that assumes values within a sequence returned by an expression (Case 1); the loop variable can be declared explicitly (Case 2). These two cases are mapped to the C++’s range-based for-loop. More specifically, the translation to C++ is done by dynamically typing a reference variable named as the name label through the auto. In these two cases, the loop variable is added to the scope of ForStatement through the type deduction mechanism in order to enable its use within the block. Alternatively, the loop variable can be defined as a name label assuming values within a range of integer values with delimiting values represented by two expressions (Case 3). In this case, ForStatement is mapped to a standard for-loop in C++, where the first expression defines the initial value of the loop variable, and the second expression represents the final value. Before translating ForStatement, the type deduction mechanism creates a sub-scope representing the ForStatement block’s scope within the current scope (Table 14).
Break statements
BreakStatement is represented by the keyword ‘break’ followed by a semicolon, and it is used to stop the execution of an enclosing SwitchStatement, ForStatement, DoStatement and WhileStatement. The translation to C++ is straightforward since the same construct is used in C++ for the same purposes.
Return statements
If an operation is expected to return a value, ReturnStatement is used to determine that value and exit the operation. It is composed of the keyword ‘return’, which is translated to the same keyword in C++, followed by an expression evaluating to the return value, which is translated according to the specific expression type.
Units
Units enable the definition of structural elements (mostly in the fUML subset) textually using ALF. In this section, we describe the mappings between the covered types of unit to C++.
Namespaces
NamespaceDefinition defines the context for a set of owned members. It can be defined as either a package, through PackageDefinition (see Sect. 5.3.2), or a classifier, through ClassifierDefinition (see Sect. 5.3.3). The visibility of the name of an owned member outside the owner namespace’s scope is defined by a visibility indicator (‘public’, ‘private’, ‘protected’) on the declaration of the owned member. Visibility indicator values coincide in ALF and C++ for properties and methods, but not for classes, which in C++ do not have any visibility indicator.
Packages
PackageDefinition is a type of namespace aiming at simply grouping owned members. In our solution, members owned by a package can only be classifiers of type Class. The notion of package in ALF is mapped the C++’s namespace. The translation of PackageDefinition is done by recreating the package structure, but using the keyword ‘namespace’. Owned members are translated according to the mappings in the next section. Note that the translation of PackageDefinition produces effects on both the C++ header and implementation files (Table 15).
Table 15 Mapping of PackageDefinition
Table 16 Mapping of ClassDefinition
Table 17 Mapping of PropertyDefinition
Table 18 Mapping of OperationDefinition
Classes (Classifiers)
ClassDefinition represents a classifier whose instances are objects and defines a scope for owned properties and operations. The translation of ClassDefinition is done by recreating the class declaration in C++, that is to say a stub declaration and a forward declaration of the class, and ignoring the visibility indicator. Owned members are translated according to the mappings in the next sections. In this case, the translation of ClassDefinition only affects the C++ header file (Table 16).
Properties (Features)
PropertyDefinition is used to define a structural feature of a classifier; in our case, it is used to define attributes of a class. It is composed of a visibility indicator, a type in the form of a QualifiedName, a name in the form of a string and eventually an initialiser expression. The translation is done by reproducing the visibility indicator, transforming the type according to the mapping for QualifiedName (see Sect. 5.1.1) and transforming the initialiser expression, if any, according to the specific expression type. Type deduction is exploited to properly translate the QualifiedName representing the property type; in the example below, the type of classB is identified as an array of ClassB objects which is translated to a vector of smart pointers to ClassB (wrapped in the shared_ptr
\({<}{>}\) construct). The translation of PropertyDefinition only affects the C++ header file (Table 17).
Operations (Features)
OperationDefinition represents a behavioural feature of a class. We do not support abstract operations, nor redefinition or overloading. OperationDefinition is composed of a visibility indicator, a name in the form of a string, a set of formal parameters with direction (we only support ‘in’ direction), an eventual return parameter and a block. The translation is done by reproducing the visibility indicator and the operation name, transforming the parameters list (including eventual return parameter) and finally transforming the block according to the rules defined for BlockStatement (see Sect. 5.2.2). The translation of parameters is done by considering ‘in’ parameters and properly translating their type according to the rules defined for QualifiedName (see Sect. 5.1.1). If the operation does not conceive a return parameter in ALF, the obligatory return parameter in C++ is set to ‘void’. This is not the case of constructors, that is to say when OperationDefinition is annotated with @Create; as in ALF, a return parameter is not expected in C++ either. Note that the translation of OperationDefinition produces effects on both the C++ header (operation declaration) and implementation (operation implementation) files (Table 18).