Classes¶
Classes are data structures with associated state and functions. A
variable of class type either refers to a class instance, or contains a
special nil
value. Note that object is another name for a class
instance. Storage for a class instance is not necessarily tied to the
scope of the variable(s) referring to that class instance. It is
possible for multiple variables to refer to the same class instance.
The new-expression
can be used to create an instance of a class
(Class New). Depending on the memory management strategy, a
class instance is either deleted automatically or can be deleted using
the delete-statement
(Deleting Unmanaged Class Instances).
A class declaration (Class Declarations) generates a class type (Class Types). A variable of a class type can refer to an instance of that class or any of its derived classes.
A class is generic if it has generic fields. A class can also be generic if it inherits from a generic class. Generic classes and fields are discussed in Generic Types.
Class Declarations¶
A class is defined with the following syntax:
class-declaration-statement:
'class' identifier class-inherit[OPT] { class-statement-list[OPT] }
class-inherit:
: basic-class-type
class-statement-list:
class-statement
class-statement class-statement-list
class-statement:
variable-declaration-statement
method-declaration-statement
type-declaration-statement
empty-statement
A class-declaration-statement
defines a new class type symbol
specified by the identifier. It inherits from the class specified in the
class-inherit
clause, when provided (Inheritance).
The body of a class declaration consists of a sequence of statements where each of the statements either defines a variable (called a field), a procedure or iterator (called a method), or a type alias. In addition, empty statements are allowed in class declarations, and they have no effect.
If a class declaration contains a type alias or a parameter field, or it contains a variable or constant without a specified type and without an initialization expression, then it declares a generic class type. Generic classes are described in Generic Types.
Note
Future:.
Privacy controls for classes and records are currently not specified, as discussion is needed regarding its impact on inheritance, for instance.
Class Lifetime and Borrows¶
The lifetime of a class instance is the time between its creation and its deletion. It is legal to access the class fields or methods only during its lifetime.
Each allocation of a class instance specifies a memory management
strategy. Four memory management strategies are available: owned
,
shared
, borrowed
, and unmanaged
.
owned
and shared
class instances always have their lifetime
managed by the compiler. In other words, the compiler automatically calls
delete
on these instances to reclaim their memory. For these
instances, =
and copy initialization can result in the transfer or
sharing of ownership. See the Owned Objects and Shared Objects
sections for more details. When borrowed
is used as a memory management
strategy in a new-expression
, it also creates an instance that has its
lifetime managed by the compiler (Class New).
Class instances that are unmanaged
have their lifetime managed
explicitly by the programmer. The delete
keyword must be used to
reclaim their memory.
Regardless of the memory management strategy used, class types support
borrowing. A borrowed
class instance refers to the same class
instance as another variable but has no impact on the lifetime of that
instance. The process of getting such a reference to an instance is
called borrowing.
There are several ways to borrow an instance. To borrow explicitly the
instance managed by another variable, call the .borrow()
method.
Additionally, coercions are available that are equivalent to calling the
.borrow()
method. For example:
Example (borrowing.chpl).
class C { } proc test() { var own = new owned C(); // 'own' manages the memory of the instance var b = own.borrow(); // 'b' refers to the same instance but has no // impact on the lifetime. var bc: borrowed C = own; // 'bc' stores the result of own.borrow() // due to coercion from owned C to // borrowed C // Note that these coercions can also apply // in the context of procedure calls. // the instance referred to by 'own' is // deleted here, at the end of the containing // block. }
The .borrow()
method is available on all class types (including
unmanaged
and borrowed
) in order to support generic programming.
For nilable class types, it returns the borrowed nilable class type.
Errors due to accessing an instance after the end of its lifetime are particularly difficult to debug. For this reason, the compiler includes a component called the lifetime checker. It identifies some cases where a borrowing variable can be accessed beyond the lifetime of an instance it refers to.
Note
Future:
The details of lifetime checking are not yet finalized or specified. Additional syntax to specify the lifetimes of function returns will probably be needed.
Class Types¶
A class type is formed by the combination of a basic class type and a memory management strategy. Class types can be used in a variety of scenarios such as variable declarations or type specifiers on formal arguments.
class-type:
basic-class-type
'owned' basic-class-type
'shared' basic-class-type
'borrowed' basic-class-type
'unmanaged' basic-class-type
A basic-class-type can be specified using the name of any non-generic class. To use a generic class in a class-type, it must be fully-instantiated. More information on instantiating generic types can be found in the Type Constructors Section.
basic-class-type:
identifier
identifier ( named-expression-list )
A basic class type, including a generic class type that is not fully specified, may appear in the inheritance lists of other class declarations.
If a class type’s memory management strategy is unspecified, it will be
generic. (This is not the case for instances of classes. When a new class
instance is created with an unspecified memory management strategy it
will default to owned
.)
Variables of class type cannot store nil
unless the class type is
nilable (Nilable Class Types).
The memory management strategies have the following meaning:
owned
the instance will be deleted automatically when theowned
variable goes out of scope, but only oneowned
variable can refer to the instance at a time. See the Owned Objects section for more details.shared
will be deleted when all of theshared
variables referring to the instance go out of scope. See the Shared Objects section for more details.borrowed
refers to a class instance that has a lifetime managed by another variable.unmanaged
the instance must havedelete
called on it explicitly to reclaim its memory.
It is an error to apply more than one memory management strategy to a class type. However, in some cases, generic code needs to compute a variant of the class type using a different memory management strategy. Casts from the class type to a different memory management strategy are available for this purpose (see Explicit Class Conversions).
Example (duplicate-management.chpl).
class C { } var x: borrowed unmanaged C;
Example (changing-management.chpl).
class C { } type borrowedC = borrowed C; type ownedC = (borrowedC:owned);
Nilable Class Types¶
Variables of a class type cannot store nil
and do not have a default
value unless the class type is nilable. To create a nilable class type,
use the postfix ?
operator. For example, if C
is a class, then
C?
indicates the nilable class type with generic memory management strategy.
The ?
operator can be combined with memory management specifiers as
well. For example, borrowed C?
indicates a nilable class using the
borrowed
memory management strategy. Note that the ?
operator
applies only to types.
A nilable type can also be created with a cast to class?
. For example,
if T
is a class type, then T: class?
indicates its nilable counterpart,
or T
itself if it is already nilable. T: borrowed class?
produces
the nilable borrowed
variant of T
.
To create a non-nilable class type from a nilable class type, apply a
cast to class
or to a more specific type. For example, if T
is
a class type, then T: class
indicates its non-nilable counterpart,
or T
itself if it is already non-nilable. T: borrowed class
produces the non-nilable borrowed
variant of T
.
The postfix !
operator converts a class value to a non-nilable type.
If the value is not nil
, it returns a copy of that value if it is
borrowed
or unmanaged
, or a borrow from it if it is owned
or shared
. If the value is in fact nil
, it halts.
An alternative to !
is to use a cast to a non-nilable type. Such a
cast will throw NilClassError
if the value was in fact nil
.
See Explicit Class Conversions.
Non-nilable class types are implicitly convertible to nilable class types. See Implicit Class Conversions.
Class methods generally expect a receiver of type borrowed C
(see Class Methods). Since such a class method call might
involve dynamic dispatch, it is a program error to call a class method
on a class receiver storing nil
. The compiler will not
resolve calls to class methods if the receiver has nilable type. If the
programmer knows that the receiver cannot store nil
at that moment,
they can use !
to assert that the receiver is not nil
and to
convert it to the non-nilable borrowed type. For example:
Example (nilable-classes-bang.chpl).
class C { proc method() { } } var c: owned C? = new C(); // Invoke c.method() only when c is non-nil. if c != nil { c!.method(); // c! converts from 'owned C?' to 'borrowed C' }
The borrow()
method is an exception. Suppose it is invoked on an
expression of a class type C
. It will return borrowed C
for any
non-nilable C
type (e.g. owned C
). It will return
borrowed C?
for any nilable C
type (e.g. C?
).
Class Values¶
A class value is either a reference to an instance of a class or nil
(The nil Value). Class instances can be created using a
new
expression (Class New).
For a given class type, a legal value of that type is a reference to an
instance of either that class or a class inheriting, directly or
indirectly, from that class. nil
is a legal value of any non-nilable
class type.
The default value of a concrete nilable class type is nil
. Generic
class types and non-nilable class types do not have a default value.
For this reason, rectangular arrays of non-nilable classes cannot be
resized, since the new array values don’t have a logical default
value. For similar reasons, associative and sparse arrays of
non-nilable classes are not currently supported.
Example (declaration.chpl).
class C { } var c : owned C?; // c has class type owned C?, meaning // the instance can be nil and is deleted automatically // when it is not. c = new C(); // Now c refers to an initialized instance of type C. var c2 = c.borrow(); // The type of c2 is borrowed C?. // c2 refers to the same object as c. class D : C {} // Class D is derived from C. c = new D(); // Now c refers to an object of type D. // Since c is owned, the previous is deleted. // the C and D instances allocated above will be reclaimed // at the end of this block.When the variable
c
is declared, it initially has the value ofnil
. The next statement assigned to it an instance of the classC
. The declaration of variablec2
shows that these steps can be combined. The type ofc2
is alsoborrowed C?
, determined implicitly from the initialization expression. Finally, an object of typeowned D
is created and assigned toc
.
The nil Value¶
Chapel provides nil
to indicate the absence of a reference to any
object. Invoking a class method or accessing a field of the nil
value results in a run-time or compile-time error.
nil
can be assigned to a variable of any nilable class type. There
is a restriction for using nil
as the default value or the actual
argument of a function formal, or as the initializer for a variable or a
field. Such a use is disallowed when the declared type of the
formal/variable/field is non-nilable or generic, including generic
memory management.
Class Fields¶
A variable declaration within a class declaration defines a field
within that class. Each class instance consists of one variable per each
var
or const
field in the class.
Example (defineActor.chpl).
The code
class Actor { var name: string; var age: uint; }defines a new class type called
Actor
that has two fields: the string fieldname
and the unsigned integer fieldage
.
Field access is described in Field Accesses.
Note
Future:
ref
fields, which are fields corresponding to variable declarations withref
orconst ref
keywords, are an area of future work.
Class Methods¶
Methods on classes are referred to as class methods. They can be instance methods or type methods. See Methods for more information about methods.
Within a class method, the type of this
is generally the non-nilable
borrowed
variant of the class type. It is different for type methods
and for methods without parentheses that return a type
or param
(see below). Additionally, it might be a different type if the class
method is declared as a secondary method with a type expression.
For example:
Example (class-method-this-type.chpl).
class C { proc primaryMethod() { assert(this.type == borrowed C); } } proc C.secondaryMethod() { assert(this.type == borrowed C); } proc (owned C?).secondaryMethodWithTypeExpression() { assert(this.type == owned C?); } var x:owned C? = new owned C(); x!.primaryMethod(); // within the method, this: borrowed C x!.secondaryMethod(); // within the method, this: borrowed C x.secondaryMethodWithTypeExpression(); // within the method, this: owned C?
For type methods on a class, this
will accept any management or
nilability variant of the class type and it will refer to that type in
the body of the method. In other words, this
will be instantiated to
match the receiver at the call site. For example:
Example (class-type-method-this.chpl).
class C { proc type typeMethod() { writeln(this:string); // print out 'this', which is a type } } (C).typeMethod(); // prints 'C' (owned C).typeMethod(); // prints 'owned C' (borrowed C?).typeMethod(); // prints 'borrowed C?'
When a type method is defined only in a parent class, this
will be a
type that is the corresponding variant of the parent class. For example:
Example (class-type-method-inherit.chpl).
class Parent { } class Child : Parent { } proc type Parent.typeMethod() { writeln(this:string); // print out 'this', which is a type } Child.typeMethod(); // prints 'Parent' (borrowed Child?).typeMethod(); // prints 'borrowed Parent?'
Similarly, a class method without parentheses that returns with param
or type
intent will have a this
that accepts any nilability or
management. See also Methods without parentheses.
Example (class-parenless-method-nilability.chpl).
class C { proc parenlessParam param { return 0; } proc parenlessType type { return this.type; } } var x: owned C? = new owned C?(); writeln(x.parenlessParam); // prints '0' writeln(x.parenlessType:string); // prints 'owned C?'
Nested Classes¶
A class defined within another class or record is a nested class. A nested class can be referenced only within its immediately enclosing class or record.
Inheritance¶
A class inherits, or derives, from the class specified in the class
declaration’s class-inherit
clause when such clause is present.
Otherwise the class inherits from the predefined object
class
(The object Class). In either case, there is exactly one
parent class. There can be many classes that inherit from a particular
parent class.
It is possible for a class to inherit from a generic class. Suppose for
example that a class C
inherits from class ParentC
. In this
situation, C
will have type constructor arguments based upon generic
fields in the ParentC
as described
in The Type Constructor. Furthermore, a fully specified C
will be a subclass of a corresponding fully specified ParentC
.
The object Class¶
All classes are derived from the object
class, either directly or
indirectly. If no class name appears in class-inherit
clause, the
class derives implicitly from object
. Otherwise, a class derives
from object
indirectly through the class it inherits. A variable of
type object
can hold a reference to an object of any class type.
Accessing Base Class Fields¶
A derived class contains data associated with the fields in its base classes. The fields can be accessed in the same way that they are accessed in their base class unless a getter method is overridden in the derived class, as discussed in Overriding Base Class Methods.
Shadowing Base Class Fields¶
A field in a derived class declared with the same name as a field in a base class will cause a compilation error.
Overriding Base Class Methods¶
If a method in a derived class is declared with a signature identical to
that of a method in a base class, then it is said to override the base
class’s method. Such methods may be considered for dynamic dispatch if
certain criteria are met. In particular, dynamic dispatch will be used
when the method receiver has a static type of the base class but refers
to an instance of a derived class type. Additionally, a method eligible
for dynamic dispatch must not be a class method (see Class Methods),
must not return type
, and must not return param
.
Rationale.
Class methods, methods that return
type
, and methods that returnparam
are not considered as candidates for dynamic dispatch because they are resolved at compile-time based on the static type of the method receiver.
In order to have identical signatures, two methods must have the same names, and their formal arguments must have the same names, intents, types, and order.
The return type of the overriding method must either be the same as the return type of the base class’s method or be a subclass of the base class method’s return type.
Methods that override a base class method must be marked with the
override
keyword in the procedure-kind
. Additionally, methods
marked with override
but for which there is no parent class method
with an identical signature will result in a compiler error.
Rationale.
This feature is designed to help avoid cases where class authors accidentally override a method without knowing it; or fail to override a method that they intended to due to not meeting the identical signature condition.
Methods without parentheses are not candidates for dynamic dispatch.
Rationale.
Methods without parentheses are primarily used for field accessors. A default is created if none is specified. The field accessor should not dispatch dynamically since that would make it impossible to access a base field within a base method should that field be shadowed by a subclass.
Class New¶
To create an instance of a class, use a new
expression. For example:
Example (class-new.chpl).
class C { var x: int; } var instance = new C(1);
The new expression can be defined by the following syntax:
new-expression:
'new' type-expression ( argument-list )
An initializer for a given class is called by placing the new
operator in front of a type expression. Any initializer arguments follow
the class name in a parenthesized list.
Syntactically, the type-expression
includes owned
, shared
,
borrowed
, and unmanaged
. However these have important
consequences for class new expressions. In particular, suppose C
is
a type-expression
that results in a class type. Then:
new C()
is the same asnew owned C()
new owned C()
allocates and initializes an instance that will be deleted at the end of the current block unless it is transferred to anotherowned
variable. It results in something of typeowned C
.new shared C()
allocates and initializes the instance that will be deleted when the lastshared
variable referring to it goes out of scope. Results in something of typeshared C
.new borrowed C()
allocates and initializes an instance that will be automatically deleted at the end of the current block. This process is managed by anowned
temporary. Unlikenew owned C()
, this results in a value of typeborrowed C
and ownership of the instance cannot be transferred out of the block. In other words,new borrowed C()
is equivalent to(new owned C()).borrow()
new unmanaged C()
allocates and initializes an instance that must havedelete
called on it explicitly to avoid a memory leak. It results in something of typeunmanaged C
.
See also Class Lifetime and Borrows and Class Types.
Class Initializers¶
A new
expression allocates memory for the desired class and invokes
an initializer method on the uninitialized memory, passing any
arguments following the class name. An initializer is implemented by a
method named init
and is responsible for initializing the fields of
the class.
Any initializers declared in a program are user-defined initializers. If the program declares no initializers for a class, the compiler must generate an initializer for that class based on the types and initialization expressions of fields defined by that class.
User-Defined Initializers¶
A user-defined initializer is an initializer method explicitly declared
in the program. An initializer declaration has the same syntax as a
method declaration, with the restrictions that the name of the method
must be init
and there must not be a return type specifier. When an
initializer is called, the usual function resolution mechanism
(Function Resolution) is applied with the exception that
an initializer may not be virtually dispatched.
A user-defined initializer is responsible for initializing all fields. An initializer may omit initialization of fields, but all fields that are initialized must be initialized in declaration order.
Initializers for generic classes (Generic Types) handle generic fields without default values differently and may need to satisfy additional requirements. See Section User-Defined Initializers for details.
Example (simpleInitializers.chpl).
The following example shows a class with two initializers:
class MessagePoint { var x, y: real; var message: string; proc init(x: real, y: real) { this.x = x; this.y = y; this.message = "a point"; } proc init(message: string) { this.x = 0; this.y = 0; this.message = message; } } // class MessagePoint // create two objects var mp1 = new MessagePoint(1.0, 2.0); var mp2 = new MessagePoint("point mp2");The first initializer lets the user specify the initial coordinates and the second initializer lets the user specify the initial message when creating a MessagePoint.
Field Initialization Versus Assignment¶
Within the body of an initializer, the first use of a field as the left-hand side of an assignment statement will be considered initialization. Subsequent uses of the assignment operator on the field will invoke regular assignment as defined by the language.
Example (fieldInitAssignment.chpl).
The following example documents the difference between field initialization and field assignment.
class PointDoubleX { var x, y : real; proc init(x: real, y: real) { this.x = x; // initialization writeln("x = ", this.x); // use of initialized field this.x = this.x * 2; // assignment, use of initialized field this.y = y; // initialization } } var p = new PointDoubleX(1.0, 2.0);The first statement in the initializer initializes field
x
to the value of the formalx
. The second statement simply reads the value of the initialized field. The third statement reads the value of the field, doubles it, and assigns the result to the fieldx
.
If a field is used before it is initialized, an compile-time error will be issued.
Example (usedBeforeInitialized.chpl).
In the following code:
class Point { var x, y : real; proc init(x: real, y: real) { writeln(this.x); // Error: use of uninitialized field! this.x = x; this.y = y; writeln(this.y); } } var p = new Point(1.0, 2.0);The first statement in the initializer reads the value of uninitialized field
x
, so the compiler will issue an error:usedBeforeInitialized.chpl:4: In initializer: usedBeforeInitialized.chpl:5: error: field "x" used before it is initialized
Omitting Field Initializations¶
In order to support productive and elegant initializers, the language allows field initializations to be omitted if the field has a type or if the field has an initialization expression. The compiler will insert initialization statements for such fields based on their types and default values.
Example (fieldInitOmitted.chpl).
In the following code:
class LabeledPoint { var x : real; var y : real; var msg : string = 'Unlabeled'; proc init(x: real, y: real) { this.x = x; this.y = y; // compiler inserts "this.msg = 'Unlabeled'"; } proc init(msg : string) { // compiler inserts "this.x = 0.0;" // compiler inserts "this.y = 0.0;" this.msg = msg; } } var A = new LabeledPoint(2.0, 3.0); var B = new LabeledPoint("Origin");The first initializer initializes the values of fields
x
andy
, and the compiler inserts initialization for themsg
field by using its default value. The second initializer initializes themsg
field, and the compiler inserts initialization for fieldsx
andy
based on the type of those fields (Default Initialization).
In order to reduce ambiguity and to ensure a well-defined order for side-effects, the language requires that all fields be initialized in field declaration order. This applies regardless of whether field initializations are omitted from the initializer body. If fields are initialized out of order, a compile-time error will be issued.
Example (fieldsOutOfOrder.chpl).
In the following code:
class Point3D { var x = 1.0; var y = 1.0; var z = 1.0; proc init(x: real) { this.x = x; // compiler inserts "this.y = 1.0;" this.z = y * 2.0; } proc init(x: real, y: real, z: real) { this.x = x; this.z = z; this.y = y; // Error! } } var A = new Point3D(1.0); var B = new Point3D(1.0, 2.0, 3.0);The first initializer leverages the well-defined order of omitted field initialization to use the default value of field
y
in order to explicitly initialize fieldz
.The second initializer initializes field
z
before fieldy
, causing a compile-time error to be issued.
Rationale.
Without this rule the compiler could insert default initialization for field
y
beforez
is explicitly initialized. The following statement would then be assignment to fieldy
, despite appearing to be initialization. This subtle difference may be confusing and surprising, and is avoided by requiring fields to be initialized in field declaration order.
Limitations on Instance Usage in Initializers¶
As the initializer makes progress, the class instance is incrementally initialized. In order to prevent usage of uninitialized memory, there are restrictions on usage of the class instance before it is fully initialized:
Methods may not be invoked on partially-initialized instances
this
may not be passed to functions while partially-initialized
These rules allow all methods and functions to assume that class
instances have been initialized, provided their value is not nil
.
Rationale.
The compiler could conceivably attempt to analyze methods and functions to determine which fields are used, and selectively allow method calls on partially-initialized class instances. Instead, it is simpler for the language to forbid method calls on partially-initialized instances.
Methods may be called and this
may be passed to functions only after
the built-in complete
method is invoked. This method may not be
overridden. If any fields have not been initialized by the time the
complete
method is invoked, they will be considered omitted and the
compiler will insert initialization statements as described earlier. If
the user does not invoke the complete
method explicitly, the
compiler will insert a call to complete
at the end of the
initializer.
Rationale.
Due to support for omitted field initialization, there is potential for confusion regarding the overall status of initialization. This confusion is addressed in the design by requiring
complete
to explicitly mark the transition between partially and fully initialized instances.
Implementors’ note.
Even if the user explicitly initializes every field, the
complete
method is still required to invoke other methods.Example (thisDotComplete.chpl).
In the following code:
class LabeledPoint { var x, y : real; var max = 10.0; var msg : string = 'Unlabeled'; proc init(x: real, y: real) { this.x = x; this.y = y; // compiler inserts initialization for 'max' and 'msg' this.complete(); // 'this' is now considered to be fully initialized this.verify(); writeln(this); } proc init(msg : string) { // compiler inserts initialization for fields 'x', 'y', and 'max' this.msg = msg; // Illegal: this.verify(); // Implicit 'this.complete();' } proc verify() { if x > max || y > max then halt("LabeledPoint out of bounds!"); } } var A = new LabeledPoint(1.0, 2.0); var B = new LabeledPoint("Origin");The first initializer leverages the
complete
method to initialize the remaining fields and to allow for the usage of theverify
method. Calling theverify
method or passingthis
towriteln
before thecomplete
method is called would result in a compile-time error.The second initializer exists to emphasize the rule that even though all fields are initialized after the initialization of the
msg
field, the compiler does not consider the type initialized until thecomplete
method is called. If the second initializer tried to invoke theverify
method, a compile-time error would be issued.
Invoking Other Initializers¶
In order to allow for code-reuse, an initializer may invoke another
initializer implemented for the same type. Because the invoked
initializer must operate on completely uninitialized memory, a
compile-time error will be issued for field initialization before a call
to init
. Because each initializer either explicitly or implicitly
invokes the complete
method, all fields and methods may be used
after such a call to init
.
Example (thisDotInit.chpl).
In the following code:
class Point3D { var x, y, z : real; proc init(x: real, y: real, z: real) { this.x = x; this.y = y; this.z = z; // implicit 'this.complete();' } proc init(u: real) { this.init(u, u, u); writeln(this); } } var A = new Point3D(1.0);The second initializer leverages the first initializer to initialize all fields with the same value. After the
init
call the type is fully initialized, thecomplete
method has been invoked, and sothis
can be passed to thewriteln
function.
Initializing Fields in Conditional Statements¶
Fields may be initialized inside of conditional statements, with the restriction that the same set of fields must be initialized in every branch. If the user omits any field initializations, the compiler will insert field initializations up to and including the field furthest in field declaration order between the conditional branches. If the else branch of a conditional statement is omitted, the compiler will generate an empty else branch and insert field initialization statements as needed.
Example (initFieldConditional.chpl).
In the following code:
class Point { var x, y : real; var r, theta : real; proc init(polar : bool, val : real) { if polar { // compiler inserts initialization for fields 'x' and 'y' this.r = val; } else { this.x = val; this.y = val; // compiler inserts initialization for field 'r' } // compiler inserts initialization for field 'theta' } } var A = new Point(true, 5.0); var B = new Point(false, 1.0);The compiler identifies field
r
as the latest field in both branches, and inserts omitted field initialization statements as needed to ensure that fieldsx
,y
, andr
are all initialized by the end of the conditional.
Conditionals may also contain calls to parent initializers (Initializing Inherited Classes) and other initializers defined for the current type, provided that the initialization state is the same at the end of the conditional statement.
Example (thisDotInitConditional.chpl).
In the following code:
class Parent { var x, y : real; } class Child : Parent { var z : real; proc init(cond : bool, val : real) { if cond { super.init(val, val); this.z = val; this.complete(); } else { this.init(val, val, val); } } proc init(x: real, y: real, z: real) { super.init(x, y); this.z = z; } } var c = new Child(true, 5.0);The first initializer must invoke the
complete
method at the end of the if-branch in order to match the state at the end of the else-branch.
Miscellaneous Field Initialization Rules¶
Fields may not be initialized within loop statements or parallel statements.
The Compiler-Generated Initializer¶
A compiler-generated initializer for a class is created automatically if there are no initializers for that class in the program. The compiler-generated initializer has one argument for every field in the class, each of which has a default value equal to the field’s default value (if present) or the default value of the field’s type (if not). The order and names of arguments matches the order and names of field declarations within the class.
Generic fields are discussed in Section The Compiler-Generated Generic Initializer.
The compiler-generated initializer will initialize each field to the value of the corresponding actual argument.
Example (defaultInitializer.chpl).
Given the class
class C { var x: int; var y: real = 3.14; var z: string = "Hello, World!"; }there are no user-defined initializers for
C
, sonew
expressions will invokeC
’s compiler-generated initializer. Thex
argument of the compiler-generated initializer has the default value0
. They
andz
arguments have the default values3.14
and"Hello, World!
”, respectively.
C
instances can be created by calling the compiler-generated initializer as follows:
The call
new C()
is equivalent tonew C(0,3.14,"Hello, World!")
.The call
new C(2)
is equivalent tonew C(2,3.14,"Hello, World!")
.The call
new C(z="")
is equivalent tonew C(0,3.14,"")
.The call
new C(2, z="")
is equivalent tonew C(2,3.14,"")
.The call
new C(0,0.0,"")
specifies the initial values for all fields explicitly.
The postinit Method¶
The compiler-generated initializer is powerful and flexible, but cannot
satisfy all initialization patterns desired by users. One way for users
to leverage the compiler-generated initializer while adding their own
functionality is to implement a method named postinit
. The
postinit
method may also be implemented for types with user-defined
initializers.
The compiler will insert a call to the postinit
method after the
initializer invoked by the new
expression finishes, if the method
exists. The postinit
method accepts zero arguments and may not
return anything. Otherwise, this method behaves like any other method.
Example (postinit.chpl).
In the following code:
class Point3D { var x, y : real; var max = 10.0; proc postinit() { verify(); } proc verify() { writeln("(", x, ", ", y, ")"); if x > max || y > max then writeln(" Point out of bounds!"); } } var A = new Point3D(); var B = new Point3D(1.0, 2.0); var C = new Point3D(y=5.0); var D = new Point3D(50.0, 50.0);Each of the
new
expressions invokes the compiler-generated initializer, then invokes theverify
method via thepostinit
method:(0.0, 0.0) (1.0, 2.0) (0.0, 5.0) (50.0, 50.0) Point out of bounds!
For classes that inherit, the user may invoke the parent’s postinit
method or let the compiler insert a call automatically
(The postinit Method for Inheriting Classes).
Initializing Inherited Classes¶
User-defined initializers also allow for control over initialization of
parent classes. All the fields of the parent type must be initialized
before any fields of the child type, otherwise a compile-time error is
issued. This allows for parent fields to be used in the definition of
child fields. An initializer may invoke a parent’s initializer using the
super
keyword.
If the user does not explicitly call the parent’s initializer, the compiler will insert a call to the parent initializer with zero arguments at the start of the initializer.
Example (simpleSuperInit.chpl).
In the following code:
class A { var a, b : real; proc init() { this.init(1.0); } proc init(val : real) { this.a = val; this.b = val * 2; } } class B : A { var x, y : real; proc init(val: real, x: real, y: real) { super.init(val); this.x = x; this.y = y; } proc init() { // implicit super.init(); this.x = a*2; this.y = b*2; } } var b1 = new B(4.0, 1.0, 2.0); var b2 = new B();The first initializer explicitly calls an initializer for class
A
. Once the parent’s initializer is complete, fields of classB
may be initialized.The second initializer implicitly invokes the parent’s initializer with zero arguments, and then uses the parent’s fields to initialize its own fields.
As stated earlier, the compiler will insert a zero-argument call to the
parent’s initializer if the user has not explicitly written one
themselves. The exception to this rule is if the initializer body
invokes another initializer on the current type
(Invoking Other Initializers). This other initializer
will either contain an implicit or explicit call to the parent
initializer, and so the calling initializer should not attempt to
initialize the parent itself. This also means that parent fields may not
be accessed before explicit calls to init
.
Example (superInitThisInit.chpl).
In the following code:
class Parent { var x, y: real; } class Child : Parent { var z : real; proc init(x: real, y: real, z: real) { super.init(x, y); this.z = z; } proc init(z: real) { this.init(0.0, 0.0, z); } } var c = new Child(5.0);The second initializer does not contain an implicit call to the parent’s initializer because it explicitly invokes another initializer.
Calling Methods on Parent Classes¶
Once super.init()
returns, the dynamic type of this
is the
parent’s type until the complete
method
(Limitations on Instance Usage in Initializers) is
invoked (except when the child’s fields are initialized and used). As a
result, the parent’s methods may be called and this
may be passed to
functions as though it were of the parent type.
Rationale.
After
super.init()
returns the instance is in some partially-initialized, but valid, state. Allowingthis
to be treated as the parent allows for additional functionality and flexibility for users.
Example (dynamicThisInit.chpl).
In the following code:
class Parent { var x, y : real; proc foo() { writeln("Parent.foo"); } } class Child : Parent { var z : real; proc init(x: real, y: real, z: real) { super.init(x, y); // parent's compiler-generated initializer foo(); // Parent.foo() this.z = z; this.complete(); foo(); // Child.foo() } override proc foo() { writeln("Child.foo"); } } var c = new Child(1.0, 2.0, 3.0);Once the parent’s initializer is finished, the parent method
foo
may be called. After thecomplete
method is invoked, a call tofoo
resolves to the child’s overridden (Overriding Base Class Methods) implementation:Parent.foo Child.foo {x = 1.0, y = 2.0, z = 3.0}
The Compiler Generated Initializer for Inheriting Classes¶
The compiler-generated initializer for inheriting classes will have arguments with default values and names based on the field declarations in the parent class. Formals for the parent type will be listed before formals for the child type.
Example (compilerGeneratedInheritanceInit.chpl).
In the following code:
class Parent { var x, y: real; } class Child : Parent { var z : real; } var A = new Child(); var B = new Child(1.0, 2.0, 3.0); // x=1.0, y=2.0, z=3.0 var C = new Child(y=10.0);Any
new
expressions using theChild
type can invoke an initializer with three formals namedx
,y
, andz
that all have default values based on their types.
The postinit Method for Inheriting Classes¶
The postinit
method on inheriting classes allows users to invoke the
parent’s postinit
method using the super
keyword. If the user
does not explicitly invoke the parent’s postinit
, the compiler will
insert the call at the top of the user’s postinit
method. If the
parent type has a postinit
method but the inheriting class does not,
the compiler will generate a postinit
method that simply invokes the
parent’s postinit
method.
Example (inheritancePostinit.chpl).
In the following code:
class Parent { var a, b : real; proc postinit() { writeln("Parent.postinit: ", a, ", ", b); } } class Child : Parent { var x, y : real; proc postinit() { // compiler inserts "super.postinit();" writeln("Child.postinit: ", x, ", ", y); } } var c = new Child(1.0, 2.0, 3.0, 4.0);The compiler inserts a call to the parent’s
postinit
method in the child’spostinit
method, and invokes the child’spostinit
method after the compiler-generated initializer finishes:Parent.postinit: 1.0, 2.0 Child.postinit: 3.0, 4.0
Field Accesses¶
The field in a class is accessed via a field access expression.
field-access-expression:
receiver-clause[OPT] identifier
receiver-clause:
expression .
The receiver-clause specifies the receiver, which is the class instance whose field is being accessed. The receiver clause can be omitted when the field access is within a method. In this case the receiver is the method’s receiver. The receiver clause can also be omitted when the field access is within a class declaration. In this case the receiver is the instance being implicitly defined or referenced.
The identifier in the field access expression indicates which field is accessed.
A field can be modified via an assignment statement where the left-hand side of the assignment is a field access expression.
Accessing a parameter or type field returns a parameter or type, respectively. In addition to being available for access with a class instance receiver, parameter and type fields can be accessed from the instantiated class type itself.
Example (useActor1.chpl).
Given a variable
anActor
of typeActor
as defined above, the codevar s: string = anActor.name; anActor.age = 27;reads the field
name
and assigns the value to the variables
, and assigns the fieldage
in the objectanActor
the value27
.
Variable Getter Methods¶
All field accesses are performed via getters. A getter is a method
without parentheses with the same name as the field. It is defined in
the field’s class and has a ref
return intent
(The Ref Return Intent). If the program does not define it,
the default getter, which simply returns the field, is provided.
Example (getterSetter.chpl).
In the code
class C { var setCount: int; var x: int; proc x ref { setCount += 1; return x; } proc x { return x; } }an explicit variable getter method is defined for field
x
. It returns the fieldx
and increments another field that records the number of times x was assigned a value.
Class Method Calls¶
Class method calls are similar to other method calls which are described in Method Calls. However, class method calls are subject to dynamic dispatch.
The receiver-clause (or its absence) specifies the method’s receiver in the same way it does for field accesses Field Accesses.
See (The Method Receiver and the this Argument) for more details of about method receivers.
Common Operations¶
Class Assignment¶
Classes are assigned by reference. After an assignment from one variable
of a class type to another, both variables reference the same class
instance. Assignments from an owned
variable to another owned
or
shared
variable are an exception. They transfer ownership, leaving
the source variable empty i.e. storing nil
.
Example (owned-assignment.chpl).
// assume that C is a class var a:owned C? = new owned C(); var b:owned C?; // default initialized to store `nil` b = a; // transfers ownership from a to b writeln(a); // a is left storing `nil`
In contrast, assignment for shared
variables allows both variables
to refer to the same class instance.
The following assignments between variables or expressions with different memory management strategies are disallowed:
to
owned
fromshared
orborrowed
, as it would not ensure unique ownership of the instanceto
shared
fromborrowed
, as the original owner would be unaware of the shared ownershipto
owned
,shared
, orborrowed
fromunmanaged
, as both the source and the destination would appear responsible for deleting the instance
Deleting Unmanaged Class Instances¶
Memory associated with unmanaged
class instances can be reclaimed
with the delete
statement:
delete-statement:
'delete' expression-list ;
where the expression-list specifies the class objects whose memory will
be reclaimed. Prior to releasing their memory, the deinitialization
routines for these objects will be executed
(Class Deinitializer). The expression-list can contain
array expressions, in which case each element of that array will be
deleted in parallel using a forall
loop over the array. It is legal
to delete a class variable whose value is nil
, though this has no
effect. If a class instance is referenced after it has been deleted, the
behavior is undefined.
Example (delete.chpl).
The following example allocates a new object
c
of class typeC
and then deletes it.var c : unmanaged C? = nil; delete c; // Does nothing: c is nil. c = new unmanaged C(); // Creates a new object. delete c; // Deletes that object. // The following statements reference an object after it has been deleted, so // the behavior of each is "undefined": // writeln(c.i); // May read from freed memory. // c.i = 3; // May overwrite freed memory. // delete c; // May confuse some allocators.
Class Deinitializer¶
A class author may create a deinitializer to specify additional actions
to be performed when a class instance is deleted. A class deinitializer
is a method named deinit()
. It must take no arguments (aside from the
implicit this
argument). If defined, the deinitializer is called each
time a delete
statement is invoked with a valid instance of that
class type. The deinitializer is not called if the argument of delete
evaluates to nil
. Note that when an owned
or shared
reaches
its deinit point (see Deinit Points), it may call delete
on a
class instance which in turn will run the deinitializer and then reclaim
the memory.
Example (classDeinitializer.chpl).
class C { var i: int; proc deinit() { writeln("Bye, bye ", i); } } var c : unmanaged C? = nil; delete c; // Does nothing: c is nil. c = new unmanaged C(1); // Creates a new instance. delete c; // Deletes that instance: Writes out "Bye, bye 1" // and reclaims the memory that was held by c. { var own = new owned C(2); // Creates a new owned instance // The instance is automatically deleted at // the end of this block, so "Bye, bye 2" is // output and then the memory is reclaimed. }
Owned Objects¶
Including owned
(or shared
) in a class type directs
the compiler to manage the deallocation of a class instances of that type.
owned
is meant to be used when only one reference to an
object needs to manage that object’s storage at a time.
Also see the above section on Class Lifetime and Borrows.
Using owned¶
The new
keyword allocates owned
classes by default.
Additionally, it is possible to explicitly request an owned
class instance
class MyClass { }
var myOwnedObject = new MyClass();
// or, equivalently
var myOwnedObject = new owned MyClass();
When myOwnedObject
goes out of scope, the class instance it refers to will
be deleted. It is possible to transfer the ownership to another owned
variable before that happens.
Copy initializing from myOwnedObject
or assigning it to another
owned
will leave myOwnedObject
storing a nil value
and transfer the owned class instance to the other value.
var otherOwnedObject = myOwnedObject;
// now myOwnedObject stores nil
// the value it stored earlier has moved to otherOwnedObject
myOwnedObject = otherOwnedObject;
// this assignment moves the value from the right-hand-side
// to the left-hand-side, leaving the right-hand-side empty.
// after the assignment, otherOwnedObject stores nil
// and myOwnedObject stores a value that will be deleted
// when myOwnedObject goes out of scope.
owned
forms part of a type and can be used in type expressions:
var emptyOwnedObject: owned MyClass;
Borrowing from owned¶
The borrow
method returns the pointer managed by the
owned
. This pointer is only valid as long as the
owned
is storing that pointer.
The compiler includes a component called the lifetime checker that can, in many cases, check that a borrow does not refer to an object that could be deleted before the borrow. For example:
proc test() {
var a: owned MyClass = new owned MyClass();
// the instance referred to by a is deleted at end of scope
var c: borrowed MyClass = a.borrow();
// c "borrows" to the instance managed by a
return c; // lifetime checker error! returning borrow from local variable
// a is deleted here
}
Coercions for owned¶
The compiler includes support for introducing automatic coercions
from owned
to the borrow type. This is equivalent
to calling the borrow
method. For example:
proc f(arg: borrowed MyClass) {
writeln(arg);
}
var myOwned = new owned MyClass();
f(myOwned); // compiler coerces to borrowed MyClass via borrow()
Additionally, the compiler includes support for coercing a value
of type owned T
to owned U
when T
is a subclass of U
.
For example:
class Person { }
class Student : Person { }
var myStudent = new owned Student();
var myPerson:owned Person = myStudent;
// relies on coercion from owned Student to owned Person
// moves the instance from myStudent to myPerson, leaving
// myStudent containing nil.
owned Default Intent¶
The default intent for owned
is const ref
.
See more on argument intents in the Procedures Primer
Methods on owned Classes¶
- record owned¶
owned
manages the deletion of a class instance assuming that thisowned
is the only thing responsible for managing the lifetime of the class instance.- proc init=(ref src: owned)¶
Copy-initializer. Creates a new
owned
that takes over ownership from src. src will refer to nil after this call.
- proc type adopt(in obj: owned)¶
Creates a new owned class reference, taking over the ownership of the argument. The result has the same type as the argument. If the argument is non-nilable, it must be recognized by the compiler as an expiring value.
Note
This is part of an new interface that will replace
owned.create
andowned.retain
. However, adopt is not as widely used as create and retain yet, so there may be some bugs we have not found yet. If you discover any bugs with adopt, please report them to us and fall back on create and retain.
- proc type adopt(in obj: unmanaged)
Starts managing the argument class instance obj using the owned memory management strategy. The result type preserves nilability of the argument type.
It is an error to directly delete the class instance after passing it to owned.adopt().
Note
This is part of an new interface that will replace
owned.create
andowned.retain
. However, adopt is not as widely used as create and retain yet, so there may be some bugs we have not found yet. If you discover any bugs with adopt, please report them to us and fall back on create and retain.
- proc type release(ref obj: owned)¶
Empty obj so that it manages nil and return the instance previously managed by this owned object.
If the argument is nil it returns nil.
Note
This is part of an new interface that will replace
owned.clear
. However, release is not as widely used as clear yet, so there may be some bugs we have not found yet. If you discover any bugs with release, please report them to us and fall back on clear.
- proc type create(in take: owned)¶
Creates a new owned class reference, taking over the ownership of the argument. The result has the same type as the argument. If the argument is non-nilable, it must be recognized by the compiler as an expiring value.
- proc type create(p: unmanaged)
Starts managing the argument class instance p using the owned memory management strategy. The result type preserves nilability of the argument type.
It is an error to directly delete the class instance after passing it to owned.create().
- proc deinit()¶
The deinitializer for
owned
will destroy the class instance it manages when theowned
goes out of scope.
- proc ref clear()¶
Empty this
owned
so that it stores nil. Deletes the previously managed object, if any.
- proc ref retain(newPtr: unmanaged)¶
Change the instance managed by this class to newPtr. If this record was already managing a non-nil instance, that instance will be deleted.
- proc ref release()
Empty this
owned
so that it manages nil. Returns the instance previously managed by thisowned
.
- proc borrow()¶
Return the object managed by this
owned
without impacting its lifetime at all. It is an error to use the value returned by this function after theowned
goes out of scope or deletes the contained class instance for another reason, such as with = orowned.retain
. In some cases such errors are caught at compile-time.
- proc type borrow() type