📄 chapter 11 constructors and destructors.htm
字号:
the token queue to write super statements for the remaining base classes.
<MENU><IMG src="Chapter 11 Constructors and Destructors.files/RULE60S.gif">
<P><FONT face=arial size=-1><B>Figure {RULE60S}</B> In this rule, we never
parse the super statement directly. Instead, we call two functions called
CallBaseClassCtors() and CallBaseClassDtors().</FONT></P></MENU>The last
modification that we need to make is to <TT>RuleSuperStatement()</TT>. We need
to do three things, primarily. First of all, we have to make sure that the base
class being accessed actually exists. The user cannot call a
constructor/destructor on a base class that was never inherited. Next, if the
current method is a constructor, we need to make sure that the method being
invoked is a valid constructor for the specified base class. The same rule
applies for destructors. Finally, we need to flag the base class as having been
constructed/destructed.
<P>Again, we use the token queue to implement destructors. We do this in order
to take advantage of existing functionality within the compiler. This approach
saves us from having to copy and maintain several sections of code, all of which
do the same basic thing. All super statements within constructors are expanded
to their full syntax. For example, <PRE> super A;
</PRE>is expanded to <PRE> super A::A();
</PRE>This allows us to bypass all of the syntactic exceptions that we would
otherwise encounter. In a similar manner, destructors are expanded to their full
form. <PRE> super A;
</PRE>is expanded to <PRE> super A::~A();
</PRE>Notice that the destructor is preceeded by a tilde character, which is not
valid SAL syntax. In truth, this is not legal. However, the compiler is "fooled"
in two ways. First of all, the name of the destructor is actually "~A", i.e.,
the tilde is actually a part of the destructor's identifier. Second, the name is
manufactured by the compiler itself as a string. When we call OutSymbol() and
pass it a string, the string can contain literally any type of garbage. Since
the contents of the string are never put through the scanner, the compiler never
has a problem.
<P>The tilde was left out of SAL's grammar in order to keep people from calling
the destructor directly. The destructor is called only when an object is cleaned
up by the memory manager.
<P>We look up constructors by utilizing a method belonging to
<TT>ClassBlock</TT> called <TT>IsCtorDtor()</TT>.
<TT>ClassBlock::IsCtorDtor()</TT> takes three parameters, all of which are
tokens. The first and last tokens must be identifiers, and the middle token must
be the double-colon, "::".
<P>The first token should be an identifier of the base class, and the third
token should be an identifier for the name of the constructor/destructor being
called. The return value of this method is a pointer to the <TT>BaseIdent</TT>
object for the parent class. In order to use <TT>CallBaseClassCtors()</TT> or
<TT>CallBaseClassDtors()</TT>, <TT>RuleSuperStatement()</TT> needs a specific
prototype. <PRE> proc SuperStatement(follow: Set; sblock: ^ClassBlock; DoDtor: bool);
</PRE>Figure {RULE46S} demonstrates one way of implementing
<TT>RuleSuperStatement</TT>. The only token that gets explicitly eaten by rule
super statement is supersy. This method essentially translates super statement
into a procedure call of the constructor/destructor To to this, it uses
<TT>LookAhead()</TT> to get the three tokens to use for calling
<TT>ClassBlock::IsCtorDtor()</TT>. Then instead of parsing the token stream
directly, the implementation calls <TT>RuleIdentExpression()</TT>, treating the
next group of tokens as if it were a standard method invocation (which would
normally have been handled through <TT>RuleIdentExpression()</TT>, anyway).
<P>
<MENU><IMG src="Chapter 11 Constructors and Destructors.files/RULE46S.gif">
<P><FONT face=arial size=-1><B>Figure {RULE46S}</B> The method described here
expands each super statement out to its full syntax. (It essentially will
always take the lower path) It then verifies that the base class exists, and
that the specified constructor/destructor exists, as well. Finally, it calls
RuleIdentExpr(), instead of RuleProcFuncCall(). </FONT></P></MENU>We must be
able to deal with the offsets to shared classes. To do this we use this code
(Inserted as seen in diagram): <PRE> if baseIdent->getIsShared() then
Emit(xLLD, LOCALSTART); // Get pointer to self. It is always the first parameter.
Emit(xLSA, baseIdent->getOffset()); // Load the address of the delta-offset to the shared group
Emit(xCOPT, 4); // Make a copy of the address
Emit(xLSD, 0); // Load the delta-offset
Emit(xADDD); // Add it to current self
if doDtor then
Emit(xTR);
Emit1(xJPZ);
else
Emit(xTS);
Emit1(xJPNZ);
end if;
lPC = ForwardJump();
end if;
</PRE>We must also point the forward jump to the correct spot in the code. So we
insert this code after the call to RuleIdentExpression if(baseIdent) <PRE> if baseIdent && baseIdent->getIsShared() then
JumpFix(lPC);
end if;
</PRE>
<H3>11.6.3 Code Generation for Constructors and Destructors</H3><!-------------------------------------------------------------------------------->There
remains essentially three areas that need to be fleshed out in this phase for
everything to work.
<UL>
<LI><B>Initialize local/global class instances.</B> For every variable that is
a class instance and not just a pointer to a class, we need to generate code
that automatically calls the constructor.
<LI><B>Destroy local/global class instances.</B> Every variable that is a
class instance requires that its destructor be called. This
requires some trickery, especially considering the fact that a function can
have multiple points of exit.
<LI><B>The super statement for constructors and destructors.</B> If we are
doing the body for a constructor or a destructor, we need to make sure base
class constructors/destructors are invoked. </LI></UL>
<H4>11.6.3.1 Variable Initialization</H4>In chapter 9 in section 9.2.7 we were
introduced to a new function called <TT>InitVars()</TT>. This function does a
lot of work for us. One of its jobs is to make a call to a function called
<TT>NewonItem()</TT>, which calls the class's meta-constructor, which in turn
invokes the constructor for the most-derived class. This sequence of events is
not hard to implement and we have already covered it in previous chapters. If
followed, we are guaranteed that the constructor for the most-derived class will
be called.
<P>Let's refer for a moment back to chapter 8, in section 8.1. In this section,
we discussed the initialization steps required for how to set up a procedure or
function body. Let's take a closer look at item 3 in figure {RULE60}, which is
labeled "initialize local arrays and records". Section 8.1.3 discusses this in
greater detail. A close examination of this section and function
<TT>InitVars()</TT> mentioned in section 9.2.7 will reveal that
<TT>InitVars()</TT> accomplishes the same task. In fact, we can substitute
function <TT>InitVars()</TT> for the code mentioned in section 8.1.3.
<P>In addition to properly initializing local variables, we also need to
initialize global variables. Again, most of the work has already been done for
us. We can call <TT>InitVars()</TT> in this case, too. Let's look at section
8.4. In figure {RULE63} in step 6, we allocate local arrays and records. This
step is discussed in detail in section 8.4.6. Again, we can substitute the code
in the compiler in this point with a simple call to <TT>InitVars()</TT>.
<TT>InitVars()</TT> will pass back a flag that we need to keep. We call it like
this:
<P><PRE> CallDtors:= InitVars(...); // Pass in current FunctionBlock pointer, or null for the main proc
</PRE>The return value from <TT>InitVars()</TT> will save us the trouble of
having to call another function to destroy local classes if there aren't any.
<P>
<H4>11.6.3.2 Variable Cleanup</H4>In order to accomplish the reverse of
<TT>InitVars()</TT>, we can write a function called <TT>FiniVars()</TT>. This
will basically walk backwards through the symbol table for the current scope and
call the metadestructor for all items that have one. We can make it work at the
global and local level by passing in a pointer to a <TT>FunctionIdent</TT>,
which by default can be set to <TT>NULL</TT>. If the pointer is <TT>NULL</TT>,
we will know that we are doing cleanup at the global level. The call to
<TT>FiniVars()</TT> should take place immediately before the call to
<TT>CallBaseClassDtors()</TT>. Local items are always constructed <I>after</I>
the base classes, and are alway destructed <I>before</I> the base classes.
<P><PRE> proc FiniVars(block: ^FunctionBlock:= NULL) // Calls the destructor, where needed.
var
OP: int8;
ident, stop ^Ident;
begin
ident:= null;
stop:= null;
if block<>null then
OP:= xLLD;
ident:= block->;first_var();
else
OP:= xLGD;
ident:= table.GetCurrentModule()->Elements.to_front();
end if;
while ident <> null do // destroy items
if ident->obj = varobj
and (ident->typ()> = arraytyp and ident->typ()< = classtyp)
and not ident->plev()
and ident->getExtra()->MetaDtorProcNum() <> 0 then
Emit (OP,ident->offset()); //*** Get addr
<B>DeleteItem(ident); //*** Destroy item See SAL-Class.cpp</B>
end if;
if block <> null then
ident:= table.prev(); // Next local var
else
ident = table.getCurrentModule()->Elements.next();
end if;
loop;
end proc;
</PRE>
<P>Calling the <TT>DeleteItem()</TT> funciton in this manner will insure that
the meta-destructor will get called. Since the meta-destructor and the class
destructor are one in the same, our task is done.</P>
<P>Our call to <TT>FiniVars()</TT> goes right after the call to
<TT>RuleStatementSequence()</TT>. In figure {RULE60} in chapter 8 this would be
right after step 4. In figure {RULE63} in chapter 8, this would be right after
step 7, and would <I>not</I> be optional, as might be indicated by the diagram.
Unfortunately, the task of cleaning up local and global class instances is not
as easy as it was for initializing them. The main problem is that although
functions and procedures all have a single point of entry, they do not
necessarily have a single point of exit. We can place the calls to
<TT>FiniVars()</TT> after the calls to <TT>RuleStatementSequence()</TT> in each
case, but what happens if the programmer uses the return statement somewhere in
the body of code. The code generated by our call to <TT>FiniVars()</TT> will be
completely skipped. There many solutions that remedy
this, all of which modify rule <TT>ReturnStatement()</TT>. We will
present one here.
<P>The approach that we will take here will spare us a lot of extra coding when
it comes time to implement exceptions and exception handling. This method
involves the use of subroutines. We can recall in section 8.3, when we
implemented the return statement, all we did is generate a return value and emit
the <TT>RTN</TT> instruction. What we could do is add a call to a cleanup
subroutine before emitting the <TT>RTN</TT>. If the reader will recall,
the call frame for each procedure contains five dwords. One of these is
the address to a cleanup subroutine. See figure {CALLFRM}
<P>
<MENU><IMG src="Chapter 11 Constructors and Destructors.files/CALLFRM.gif">
<P><FONT face=arial size=-1><B>Figure {CALLFRM}</B> The layout of a
function/procedure call frame.</FONT></P></MENU>A subroutine is a lightweight
procedure. It can be called like a procedure, and will return to the point
immediately fallowing the original call. However, it does not have a formal call
frame, and it relies on the one from the current procedure. The return address
is stored at the top of the procedure stack, at S. More details can be found in
the <A href="http://topaz.cs.byu.edu/text/html/Textbook/Chapter11/">reference
section</A>, in the document entitied <I>"SAL VM Instruction Set"</I>. There are
three instructions for performing subroutines:
<P>
<MENU><B><TT>JSR</TT></B> Jump to SubRoutine. This instruction works like the
unconditional branch, <TT>JMP</TT>, but it goes to a subroutine, instead. The
return address for the subroutine is placed at the current address in the S
register, and then S is incremented by four bytes.
<P><B><TT>JSRS</TT></B> Jump to SubRoutine on the eeS. This instruction takes
the address of the subroutine from the top of the EES. The return address for
the subroutine is placed at the current address in the S register, and then S
is incremented by four bytes.
<P><B><TT>RTS</TT></B> ReTurn from Subroutine. Returns from a subroutine. The
return PC address is found by decrementing S by four bytes, and taking the
dword at that address.
<P></P></MENU>In order to have the <TT>return</TT> statement behave correctly,
we need to modify <TT>RuleReturnStatement()</TT> and
<TT>RuleProcFuncBlock()</TT>. In <TT>RuleRetur
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -