www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Class member function calls inside ctor and dtor

reply Johan Engelen <j j.nl> writes:
I'm working on devirtualization and for that it's crucial to know 
some spec details (or define them in the spec if they aren't yet).

Currently, calling non-final member functions in the destructor 
is allowed and these indirect calls are to the possibly 
overridden functions of derived classes. That is, inside a base 
class constructor, the object's type is its final type (so 
possibly of a derived class). This is the same in the destructor. 
Thus, the object's dynamic type does not change during its 
lifetime.
This greatly simplifies devirtualization, and I want to verify 
that it is in the spec (I can't find it).

See this example program:
```
char glob;

class A {
     char c;

     this() { c = getType(); }
     ~this() { glob = getType(); }

     char getType() { return 'A'; }
}

class B : A {
     override char getType() { return 'B'; }
}

void main() {
     {
         scope b = new B();
         assert(b.c == 'B');
     }
     assert(glob == 'B');
}
```

My question: where can I find this in the spec, and where in the 
testsuite is this tested?

If it's not in the spec and/or not in the testsuite, I'll add it.

Thanks,
   Johan
Jan 27 2018
parent reply Thomas Mader <thomas.mader gmail.com> writes:
On Saturday, 27 January 2018 at 14:48:08 UTC, Johan Engelen wrote:
 I'm working on devirtualization and for that it's crucial to 
 know some spec details (or define them in the spec if they 
 aren't yet).

 Currently, calling non-final member functions in the destructor 
 is allowed and these indirect calls are to the possibly 
 overridden functions of derived classes. That is, inside a base 
 class constructor, the object's type is its final type (so 
 possibly of a derived class). This is the same in the 
 destructor. Thus, the object's dynamic type does not change 
 during its lifetime.
Can't answer your question but have a little question. How is the behavior different to the situation in C++? They argue that it's not good to call virtual methods in Con-/Destructors in https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-ctor-virtual So I guess it should better be not used in D as well?
Jan 27 2018
next sibling parent Johan Engelen <j j.nl> writes:
On Saturday, 27 January 2018 at 16:18:26 UTC, Thomas Mader wrote:
 Can't answer your question but have a little question.
 How is the behavior different to the situation in C++?
In C++, the dynamic type of an object changes during construction and destruction (e.g. base class ctor calls base class implementation of virtual functions). Because of that, it may be confusing to call virtual functions in the ctor/dtor, and people advice against it. In D, the situation is much more clear (imo). - Johan
Jan 27 2018
prev sibling parent reply Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Saturday, January 27, 2018 16:18:26 Thomas Mader via Digitalmars-d wrote:
 On Saturday, 27 January 2018 at 14:48:08 UTC, Johan Engelen wrote:
 I'm working on devirtualization and for that it's crucial to
 know some spec details (or define them in the spec if they
 aren't yet).

 Currently, calling non-final member functions in the destructor
 is allowed and these indirect calls are to the possibly
 overridden functions of derived classes. That is, inside a base
 class constructor, the object's type is its final type (so
 possibly of a derived class). This is the same in the
 destructor. Thus, the object's dynamic type does not change
 during its lifetime.
Can't answer your question but have a little question. How is the behavior different to the situation in C++? They argue that it's not good to call virtual methods in Con-/Destructors in https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-ctor-virtu al So I guess it should better be not used in D as well?
D solved that problem. In C++, when you're in the base class constructor, the object doesn't have its derived type yet. It's still just the base class. Each class level gets add as it's constructed (the same in reverse with the destructor). You don't have a full object until all constructors have been run, and once you start running destructors, you don't have a full class anymore either. In D, on the other hand, the object is initialized with its init value _before_ any constructors are run. So, it's a full object with a full type, and everything virtual is going to get the type right. - Jonathan M Davis
Jan 27 2018
parent reply Steven Schveighoffer <schveiguy yahoo.com> writes:
On 1/27/18 12:01 PM, Jonathan M Davis wrote:
 On Saturday, January 27, 2018 16:18:26 Thomas Mader via Digitalmars-d wrote:
 On Saturday, 27 January 2018 at 14:48:08 UTC, Johan Engelen wrote:
 I'm working on devirtualization and for that it's crucial to
 know some spec details (or define them in the spec if they
 aren't yet).

 Currently, calling non-final member functions in the destructor
 is allowed and these indirect calls are to the possibly
 overridden functions of derived classes. That is, inside a base
 class constructor, the object's type is its final type (so
 possibly of a derived class). This is the same in the
 destructor. Thus, the object's dynamic type does not change
 during its lifetime.
Can't answer your question but have a little question. How is the behavior different to the situation in C++? They argue that it's not good to call virtual methods in Con-/Destructors in https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-ctor-virtu al So I guess it should better be not used in D as well?
D solved that problem. In C++, when you're in the base class constructor, the object doesn't have its derived type yet. It's still just the base class. Each class level gets add as it's constructed (the same in reverse with the destructor). You don't have a full object until all constructors have been run, and once you start running destructors, you don't have a full class anymore either. In D, on the other hand, the object is initialized with its init value _before_ any constructors are run. So, it's a full object with a full type, and everything virtual is going to get the type right.
Well, a virtual function may expect that the ctor has been run, and expect that members are different from their init values. However, because you can initialize that data before calling the superclass' constructor, you can alleviate this problem as well. For instance, the invariant may be called when you call the virtual function: import std.stdio; class A { this() { writeln("A.ctor"); foo(); } void foo() { writeln("A.foo"); } } class B : A { int x; this() { writeln("B.ctor"); x = 1; // note the intialization before calling the base class super(); } invariant { writeln("invariant!"); assert(x == 1); } override void foo() { writeln("B.foo"); } } void main() { auto b = new B; } output: B.ctor A.ctor invariant! <- before calling foo B.foo invariant! <- after calling foo invariant! <- after constructors are done -Steve
Jan 27 2018
parent reply Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
On Saturday, January 27, 2018 19:42:26 Steven Schveighoffer via Digitalmars-
d wrote:
 Well, a virtual function may expect that the ctor has been run, and
 expect that members are different from their init values.
Yes, but you can have that problem even without getting inheritance involve. For instance, class C { immutable string s; this() { s = foo(); } string foo() { return s ~ "foo"; } } When foo is called from the constructor, s is null, whereas every time it's accessed after that, it's "foo", meaning that the first time, foo returns "foo" and all other times, it returns "foofoo". You can also do class C { immutable string s; this() { s = s ~ "foo"; } } which surprised me. I thought that the compiler prevented you from using an immutable variable before it was assigned in the constructor, but it doesn't. It actually can't if you call any member functions unless it required that all const and immutable members be initialized before calling other functions, but it could at least prevent it within the constructor. It doesn't though. So, you can do some weird stuff with structs or classes that have been initialized with their init values but not had all of their constructors run, but because D initializes the object with the init value first, at least you get something consistent out of the deal, and there are no problems with the wrong version of a virtual function being called, because the object was only partially constructed, whereas in C++, you can end up crashing the program due to stuff like calling an abstract function that's only defined in derived classes. - Jonathan M Davis
Jan 27 2018
next sibling parent Shachar Shemesh <shachar weka.io> writes:
On 28/01/18 03:13, Jonathan M Davis wrote:
 On Saturday, January 27, 2018 19:42:26 Steven Schveighoffer via Digitalmars-
 d wrote:
 Well, a virtual function may expect that the ctor has been run, and
 expect that members are different from their init values.
Yes, but you can have that problem even without getting inheritance involve.
Indeed. D's lack of proper definition of when underlying objects are initialized will strike you here as well. However Here, at least, you can view the problem locally. The problem is 100% contained in the constructor definition, and if it strikes you, you know where to look for it. With the inherited class case, that's not so simple. I can inherit your class, and then you can change your class' destructor definition, and I'll be caught completely off guard. A second point is that while the constructor may choose when to call the parent's constructor, the destructor has no such prerogative. Finally, even if you can control when your parent is destroyed, that doesn't mean there is anything you can do about it. If your class inherently needs a functioning parent in order to do its stuff, then you have no choice but to call the parent's super before doing anything else in the constructor. If the parent then chooses to call virtual functions, you might be facing a problem with no tools to resolve it. C++'s method of initializing parents is ugly as hell and a little confusing, but it is extremely clean and well defined. Both the compiler and the programmer know for sure what has been initialized, and no accidental calling of or relying on uninitialized members is possible. Shachar
Jan 27 2018
prev sibling parent Timon Gehr <timon.gehr gmx.ch> writes:
On 28.01.2018 02:13, Jonathan M Davis wrote:
 Yes, but you can have that problem even without getting inheritance involve.
 For instance,
 
 class C
 {
      immutable string s;
 
      this()
      {
          s = foo();
      }
 
      string foo()
      {
          return s ~ "foo";
      }
 }
 
 When foo is called from the constructor, s is null, whereas every time it's
 accessed after that, it's "foo", meaning that the first time, foo returns
 "foo" and all other times, it returns "foofoo". You can also do
 
 class C
 {
      immutable string s;
 
      this()
      {
          s = s ~ "foo";
      }
 }
 
 which surprised me. I thought that the compiler prevented you from using an
 immutable variable before it was assigned in the constructor, but it
 doesn't. It actually can't if you call any member functions unless it
 required that all const and immutable members be initialized before calling
 other functions, but it could at least prevent it within the constructor. It
 doesn't though.
At some point it will need to, as the current behavior can be used to violate type system guarantees.
Jan 28 2018