Creating a new void-safe project

Now that we've been introduced to the Eiffel void-safe facilities, let's look at what it takes to set up a new void-safe software project. Here we'll look at the void-safety related project settings and how the can be used. Then we'll look deeper into the use of some of the void-safe tools.

Project settings for void-safe projects

There are two project settings that are related to void-safety. These settings can be set with great granularity throughout your project to allow you maximum flexibility, particularly when including classes or libraries that are void-unsafe or that have been converted to void-safety, but must do double duty in the void-safe and void-unsafe worlds.

The "Void-safe" setting

The Void-safe setting determines whether and how the Eiffel compiler checks your project against the void-safe related validity rules.

This is the essential void-safe project setting. It can assume one of the following values:

  1. No: No checking against any of the void-safety validity rules. Attachment marks attached and detachable are simply ignored.
  2. Conformance: The attachment marks are not ignored for type conformance checks (with respect to VJAR/VBAR and related validity rules).
  3. Initialization: Validity rules are selectively checked. The initialization rule (VEVI) and the target rule (VUTA) are checked only for attached entities and attached call targets -- i.e., detachable cases are not checked.
  4. Transitional: It is an obsolete level which is for users who have already migrated their code to void-safety using an old version of the compiler which did not implement all the void-safety validity rules (especially with agent initialization).
  5. Complete: Complete checking against all void-safety validity rules.

So, for a new void-safe project, you would want to set this option first to Conformance, then Initialization and finally to Complete. This will let you migrate your code progressively without much changes at each steps.

The "Full class checking" setting

This setting instructs the compiler to recheck inherited features in descendant classes. This setting is True and cannot be changed for projects with some void-safety level enabled.

Void-safe libraries

As of EiffelStudio version 13.11, all libraries distributed with EiffelStudio are void-safe except the EiffelCOM library.

Note: During a period of transition, there are different Eiffel configuration files (.ecf's) for void-unsafe and void-safe projects (for example, base.ecf and base-safe.ecf). If you have set the Void-safe setting to check for void-safety, then when you add a library to your project in EiffelStudio, you will see only the void-safe configurations by default. Starting with version 16.11 there is only one version of each of the configuration files for each library. The single configuration files serve both void-unsafe and void-safe projects.

Using generic classes

Void-safety affects generic classes. Fortunately, from the viewpoint of those writing clients to the generic classes in the EiffelBase library, not much has changed. Still, you should understand the interplay between void-safety and genericity.

Consider a generic class like LIST [G]. The formal generic parameter G represents an arbitrary type. In a generic derivation of LIST [G], say LIST [STRING], the formal generic type is replaced by an actual generic type, in this case STRING.

Remember that unconstrained genericity, LIST [G], for example, is really a case of constrained genericity in which the generic parameter is constrained to ANY, that is, it could be written LIST [G -> ANY].

With the advent of void-safe Eiffel, the unconstrained generic class name LIST [G] now equates to LIST [G -> detachable ANY]. Because any type, say T, (synonymous with attached T in void-safe Eiffel) conforms to detachable T, this change facilitates the production of generic classes, but has little effect on writers of clients to those classes.

This change works for all the generic classes in EiffelBase ... except for one: ARRAY. Arrays are a special case because we often create arrays with a pre-allocated number of elements. In the case of expanded types, there's not a problem. For example, in this code my_array: ARRAY [INTEGER] ... create my_array.make (1, 100)we create my_array with one hundred INTEGER elements. INTEGER is an expanded type, and each element is initialized by applying the default initialization rule for INTEGER, i.e, the integer representation of zero.

However, if my_array had been declared of a type with reference semantics, say STRING (meaning, of course, attached STRING, the default rule would not work well, because the default initialization for references types is Void which would not be allowed in an array of elements of any attached type.

The solution to this challenge is fairly simple. For arrays of elements of detachable or expanded types, there is no different behavior. When dealing with arrays of elements of attached types, we must be careful.

Creating an array using ARRAY's creation procedure make may still be safe in some cases. Specifically, make can be used with arrays of elements of attached types if the arguments have values such that an empty array will be created, that is, when min_index = max_index + 1

In all other situations involving arrays of elements of attached types, make may not be used to do the creation. Rather, you should use the creation procedure make_filled which takes three arguments. The first is an object of the type of the array, and the second and third are the minimum and maximum indexes, respectively. When the array is created, each of the elements will be initialized with a reference to the object of the first argument.

So, a call using make_filled would look like this: my_array: ARRAY [STRING] ... create my_array.make_filled (" ", 1, 100)Upon creation, each element of the array will reference the same object; an object of type STRING composed of one space character.

Using the attribute keyword carefully

The keyword attribute should be used with some care. You might be tempted to think that it would be convenient or add an extra element of safety to use self-initializing attributes widely. And in a way, you would be correct. But you should also understand that there is a price to pay for using self-initializing attributes and stable attributes. It is that upon every access, an evaluation of the state of the attribute must be made. So, as a general rule, you should avoid using self-initializing attributes only for the purpose of lazy initialization.

More about the attached syntax

The complete attached syntax is: attached {SOME_TYPE} exp as l_expIn this section, we will see more ways in which to use this versatile language facility.

As a CAP-like construct which yields a local variable

In the introduction to the attached syntax, we used an example which showed how the attached syntax is directly relevant to void-safety. That is, the code: if x /= Void then -- ... Any other instructions here that do not assign to x x.f (a) end

is a CAP for x ... but that's only true if x is a local variable or a formal argument to the routine that contains the code.

So to access a detachable attribute safely, we could declare a local variable, make an assignment, and test for Void as above. Something like this: my_detachable_attribute: detachable MY_TYPE ... some_routine local x: like my_detachable_attribute do x := my_detachable_attribute if x /= Void then -- ... Any other instructions here that do not assign to x x.f (a) end ...The attached syntax can both check the attached status of a detachable attribute and also provide a new local variable. So the routine becomes: some_routine do if attached my_detachable_attribute as x then -- ... Any other instructions here that do not assign to x x.f (a) end ...

As a test for attachment

In its simplest form, the attached syntax can be used to test attached status only: if attached x then do_something else do_something_different end

So in this simple form, attached x can be used instead of x /= Void. The two are semantically equivalent, and which one you choose is a matter of personal preference.

As a tool for "once per object"

There is a code pattern for functions that exists in some Eiffel software to effect "once-per-object / lazy evaluation".

Note: As of EiffelStudio version 6.6, the use of this code pattern effecting "once per object" is no longer necessary. V6.6 includes explicit support for once routines which can be adjusted by a once key to specify once per object.

This "once-per-object" code pattern employs a cached value for some object which is not exported. When it is applied, the "once-per-object" function checks the attachment status of the cached value. If the cached value is void, then it is created and assigned to Result. If the cached value was found already to exist, then it is just assigned to Result.

Here's an example of this pattern used to produce some descriptive text of an instance of its class:

feature -- Access descriptive_text: STRING local l_result: like descriptive_text_cache do l_result := descriptive_text_cache if l_result = Void then create Result.make_empty -- ... Build Result with appropriate -- descriptive text for Current. descriptive_text_cache := Result else Result := l_result end ensure result_attached: Result /= Void result_not_empty: not Result.is_empty result_consistent: Result = descriptive_text end feature {NONE} -- Implementation descriptive_text_cache: like descriptive_text This example will not compile in a void-safe project (class types are attached by default). The problem is that the attribute descriptive_text_cache is of an attached type, therefore will be flagged by the compiler as not properly set (VEVI). Of course, it will be ... that's the whole idea here: not to initialize descriptive_text_cache until it's actually used. So it sounds like descriptive_text_cache should be declared detachable. That is: descriptive_text_cache: detachable like descriptive_textThis change will make this routine compile in a void-safe project. But you should notice that there is a ripple-down effect due to the change. Within the routine, l_result is typed like descriptive_text_cache, so it also will be detachable. Therefore we might expect trouble, because later in the routine we have: Result := l_resultBecause we know Result is attached and l_result is detachable, we might expect a compiler error in which the source of an assignment does not conform to its target (VJAR).

But we don't get such an error. The reason is two-fold. First, l_result is a local variable whose use can be protected by a CAP. Second, the CAP in this case is the check to ensure that l_result is not void. We only make the assignment to Result if l_result is not void. So the compiler can prove that l_result cannot be void at the point at which the assignment occurs ... therefore, no error.

Because the attached syntax can test attached status and provide a local variable, it can be used to remove some unnecessary code from this routine. The version of the routine that follows shows the attached syntax being used to test the attached status of descriptive_text_cache and yield the local variable l_result in the case that descriptive_text_cache is indeed attached. descriptive_text: STRING do if attached descriptive_text_cache as l_result then Result := l_result else create Result.make_empty -- ... Build Result with appropriate -- descriptive text for Current. descriptive_text_cache := Result end ensure result_attached: Result /= Void result_not_empty: not Result.is_empty result_consistent: Result = descriptive_text end feature {NONE} -- Implementation descriptive_text_cache: like descriptive_text

As a replacement for assignment attempt

The assignment attempt ( ?= ) has traditionally been used to deal with external objects (e.g., persistent objects from files and databases) and to narrow the type of an object in order to use more specific features. The latter is a process known by names such as "down casting" in some technological circles. A classic example is doing specific processing on some elements of a polymorphic data structure. Let's look at an example. Suppose we have a LIST of items of type POLYGON: my_polygons: LIST [POLYGON]POLYGONs could be of many specific types, and one of those could be RECTANGLE. Suppose too that we want to print the measurements of the diagonals of all the RECTANGLEs in the list. Class RECTANGLE might have a query diagonal returning such a measurement, but POLYGON would not, for the reason that the concept of diagonal is not meaningful for all POLYGONs, e.g., TRIANGLEs.

As we traverse the list we would use assignment attempt to try to attach each POLYGON to a variable typed as RECTANGLE. If successful, we can print the result of the application of diagonal. l_my_rectangle: RECTANGLE ... from my_polygons.start until my_polygons.exhausted loop l_my_rectangle ?= my_polygons.item if l_my_rectangle /= Void then print (l_my_rectangle.diagonal) print ("%N") end my_polygons.forth endThe attached syntax allows us to check both attached status and type, and provides us with a fresh local variable when appropriate: from my_polygons.start until my_polygons.exhausted loop if attached {RECTANGLE} my_polygons.item as l_my_rectangle then print (l_my_rectangle.diagonal) print ("%N") end my_polygons.forth endAs with the other examples of the attached syntax, it is no longer necessary to make a declaration for the local variable, in this case l_my_rectangle.

More about CAPs

Use of check instructions

In void-safe mode, the compiler will accept code that it can prove will only apply features to attached references at runtime ... and you help this process along by using the tools of void-safety, like attached types and CAPs. On the other hand, the compiler will reject code that it cannot guarantee is void-safe. Sometimes this may cause you a problem. There may be subtle situations in which you feel quite certain that a section of code will be free of void calls at runtime, but the compiler doesn't see it the same way, and rejects your code. In cases like this, you can usually satisfy the compiler by using check instructions.

Technically speaking, check instructions are not CAPs. But they are useful in cases in which an entity is always expected to be attached at a certain point in the code. In the following example, the attribute my_detachable_any is detachable. But at the particular point at which it is the source of the assignment to l_result, it is expected always to be attached. If it is not attached at the time of the assignment, and therefore l_result becomes void, then an exception should occur. The check instruction provides this behavior.

The following sample shows the check instruction at work. There are reasons why this is not the best use use of check in this case, and we will discuss that next.

-- A not-so-good example of using check. my_detachable_any: detachable ANY ... my_attached_any: ANY local l_result: like my_detachable_any do l_result := my_detachable_any check attached l_result end Result := l_result end

Here the assertion in the check guarantees that l_result is attached at the time of its assignment to Result. If my_detachable_any is ever not attached to an object, then an exception will be raised.

So what's wrong with the sample above? It would be fine in workbench code, but what happens if the code is in finalized mode, in which assertions are typically discarded?

The answer is that the check in the sample above would no longer be effective, and the resulting executable would no longer be void-safe.

The solution to this problem is found in a different form of the check instruction. Consider the same example, but this time using check ... then ... end:

-- A better way of using check. my_detachable_any: detachable ANY ... my_attached_any: ANY do check attached my_detachable_any as l_result then Result := l_result end end

Here, in the improved version of the example, the check ... then ... end is used along with the attached syntax. This streamlines the code a bit by eliminating the need to declare a separate local entity, while achieving the same effect as the previous example. If my_detachable_any is attached at runtime, then the temporary variable l_result is created and attached to the same object. Then the body of the check ... then ... end is executed. If my_detachable_any is not attached, an exception occurs.

Another important benefit, one that solves the problem with the original example, comes from the way in which check ... then ... end is handled by the compiler. The check ... then ... end form is always monitored, even if assertion checking is turned off at all levels, as is usually done in finalized code.

Choosing CAPs versus the Attached Syntax

The attached syntax is convenient because it can check attached status and deliver a new local variable at the same time. But there are cases in which you might choose instead to define a local variable and use a CAP. Suppose you had code acting on several similar and detachable expressions, and you use the attached syntax in each case: foobar do if attached dictionary_entry ("abc") as l_abc then l_abc.do_something end if attached dictionary_entry ("def") as l_def then l_def.do_something end if attached dictionary_entry ("ghi") as l_ghi then l_ghi.do_something end endThis routine causes three local variables to be allocated for the duration of routine foobar, one each for l_abc, l_def, and l_ghi. And it is no better to do this: foobar do if attached dictionary_entry ("abc") as l_entry then l_entry.do_something end if attached dictionary_entry ("def") as l_entry then l_entry.do_something end if attached dictionary_entry ("ghi") as l_entry then l_entry.do_something end endEven though the names are the same, still three separate local variables are allocated for foobar.

In cases like this, you could effect a minor performance improvement by declaring one local variable and reusing it. In the following code, only one local variable is used and access to it is protected by the CAP if l_entry /= Void then. foobar local l_entry: like dictionary_entry do l_entry := dictionary_entry ("abc") if l_entry /= Void then l_entry.do_something end l_entry := dictionary_entry ("def") if l_entry /= Void then l_entry.do_something end l_entry := dictionary_entry ("ghi") if l_entry /= Void then l_entry.do_something end end

Stable attributes

Remember that stable attributes are actually detachable attributes, with the difference that they can never be the target of an assignment in which the source is Void or anything that could have a value of Void.

Stable attributes are useful in situations in which there are valid object life scenarios in which some particular attribute will never need an object attached, or will only need an object attached late in the scenario. So in this case, the attribute is used only under certain runtime conditions. Declaring these attributes as stable eliminates the need to make attachments during object creation. Yet once needed, that is, once the attribute is attached, it will always be attached.

Also, you should remember that unlike other attributes, you can access stable attributes directly in a CAP: my_stable_attribute: detachable SOME_TYPE note option: stable attribute end ... if my_stable_attribute /= Void then my_stable_attribute.do_something end ...

See Also: Types as "attached" or "detachable".

cached: 06/12/2024 6:39:28.000 PM