[sv-ec] When are derived class data members created?

From: Steven Sharp <sharp_at_.....>
Date: Mon Apr 23 2007 - 14:30:53 PDT
I have been asked to request a clarification on the sequencing of
creating derived class data members versus calling the base class
constructors.  There is also some uncertainty about what implementation
of a virtual function gets called in a constructor, if the derived
class object has not been fully initialized at that point.  Here is
some of the discussion around it here:

QUOTE 1:

If derived class data members are not created until AFTER the base class
constructors are called, this is too late in some cases.  It is typical
(at least in C++) to have virtual methods which get called from the base
class constructor, but those virtual methods in the derived class may reference
derived class data object.

I have run into this in a couple of cases and the symptoms can be very 
strange (from a null pointer reference when calling a queue method to an
int getting a value of 0 instead of the value assigned by the class).

Sometimes this can be worked around, but the work around is always tedious.
The bigger problem is that the symptoms can be very strange and so it is
very hard to even find when you have this problem.

Below is a very simple testcase which demonstrates the problem. In the example,
the base class object calls the virtual function initialize(). When a
derived class object is instantiated, its initialize() function is exectuted.
This function zeros out the queue... since the queue doesn't exist yet
I get a null pointer reference on the call to size().

The order that c++ does things is:
  allocate memory for the entire class structure
  initialize all objects in the derived class
  execute the constructors from the base class up to the derived class
I think that systemverilog is supposed to do the same thing. But, it
appears that sv is doing the allocation and initialization in a bottom
up manner while it executes the constructors which is not correct.
    
module test;
  class base;
    virtual function void initialize();
      return;
    endfunction
    function new();
      initialize();
    endfunction
  endclass
  class der extends base;
    protected int queue[$];
    function new();
      super.new();
    endfunction
    virtual function void initialize();
      init_queue();
    endfunction
    function void init_queue();
      for(i=0; i<queue.size(); ++i)                      
        queue.push_back(0);
    endfunction
  endclass

  der foo = new;
endmodule


QUOTE 2:

Here is what I think should happen in a class constructor, and why

1.  Call super.new() if this is a derived class.  SV3.1a Section 11.16
is very clear to me: the first action a constructor takes it to call the
base class constructor.  If the base class constructor requires arguments,
these can be specified at the "top" of the class declaration (just before
the semi-colon that appears after the keyword class) - in this case there
is, IMO, no visiblity over class items in the derived class, so the issue
raised does not apply.  You can also specfiy arguments to a
base class constructor by an explicit call to 'super.new (...);' in a user-
defined constructor - in this case there is an issue as to what can be
used in this argument list.  It is my expectation that only arguments to the
user-defined constructor be allowed in the super.new(...) call.  Attempts
to access non-static class properties or methods is not allowed in this
context precisely because they have not yet been initialized.

2. All automatic class properties declared in this class are initialized
to their default (X, e.g.) value.

3. User-supplied initialization expressions for automatic class properties
are evaluated and assigned.

4.  The body of the (implicit or user-defined) constructor, minus the
explicit super.new(...) if it exists, is executed.  Implicit constructors
just return.  User-defined constructors are allowed to do anything an
automatic non-virtual method is allowed to do.  However, calling virtual
methods from a class constructor is a bad idea (and should probably be
disallowed) because the state of the vtable and the values of derived
class properties is undefined at this point!

I believe this approach to constructor behavior is consistent with
the LRM (although that's not saying much because the LRM is so vague).  It
is also consistent with the way C++ classes work.  It is also intuitively
appealing to me to allow initialization to flow from base classes to
derived classes.


QUOTE 3:

Upon further inspection of your example, I see you are calling initialize within
 the new function.

In C++, calling a virtual method within new will call that class' method, not th
e derived method. 

From the C++ LRM ISO-14882-2003, section 12.7:

   "When a virtual function is called directly or indirectly from a constructor 
...the function called is the one defined in the constructor's ... own class or 
in one of its bases, but not a function overriding it in a class derived from th
e constructor..."


Perhaps the SV behavior should be the same: : "turn off" the virtual mechanism
when executing the new method. This may lead to undesirable behavior, but at
least the method call won't end up referring to non-existent/uninitialized
variables, and it'll be aligned with what C++ does.


QUOTE 4:

I learned C++ at a time when calling virtual functions virtually from a class co
nstructor was understood to be asking for trouble - you could only get useful/pr
edictable results if you were very careful about what members the virtual functi
on accessed when called as part of construction.  It looks like the C++ language
 definition has tried to address this by having the vtable change as the constru
ctor execution context changes.


QUOTE 5:

Adam is correct about how c++ works, and the c++ behavior has the effect
of doing precisely what I would want to do in this type of case. If you can
disable the vtable lookup during construction then that would solve my
issues for sure (and would be consistent with c++.

One note is that if you have a pure virtual function then it is not
allowed to be called in the constructor (this makes sense since there is
no implementation).

Here is a very simple example in c++ and sv. You can see that the issue
with sv is that the derived class version is executed before the object
is created. 

//C++ Version
#include <iostream>
using namespace std;

struct base {
  base() { cout << "base::base()" << endl; initialize(); }
  virtual void initialize() { cout << "base::initialize()" << endl; }
};
struct derived : public base {
  derived() {cout << "derived::derived()" << endl; initialize(); }
  virtual void initialize() { 
    base::initialize();
    cout << "derived::initialize()" << endl; 
  }
};
int main() {
  derived h;
  return 0;
}


$ ./test
base::base()
base::initialize()       <<--- from base ctor, only base initialize is called
derived::derived()
base::initialize()
derived::initialize()

// SystemVerilog Version
module top;
  class base;
    function new;
      $display("base::base()"); 
      initialize();
    endfunction
    virtual function void initialize();
      $display("base::initialize()");
    endfunction
  endclass
  class derived extends base;
    function new;
      $display("derived::derived()"); 
      initialize();
    endfunction
    virtual function void initialize();
      super.initialize();
      $display("derived::initialize()");
    endfunction
  endclass
  
  derived h = new;
endmodule

Here is the result from your proposed behavior:

base::base()
base::initialize()
derived::initialize()    <<<--- THIS IS THE PROBLEM
derived::derived()
base::initialize()
derived::initialize()

ENDQUOTE

Steven Sharp
sharp@cadence.com


-- 
This message has been scanned for viruses and
dangerous content by MailScanner, and is
believed to be clean.
Received on Mon, 23 Apr 2007 17:30:53 -0400 (EDT)

This archive was generated by hypermail 2.1.8 : Mon Apr 23 2007 - 14:31:55 PDT