Thread overview
Class member function calls inside ctor and dtor
Jan 27, 2018
Johan Engelen
Jan 27, 2018
Thomas Mader
Jan 27, 2018
Johan Engelen
Jan 27, 2018
Jonathan M Davis
Jan 28, 2018
Jonathan M Davis
Jan 28, 2018
Shachar Shemesh
Jan 28, 2018
Timon Gehr
January 27, 2018
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

January 27, 2018
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?

January 27, 2018
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

January 27, 2018
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

January 27, 2018
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
January 27, 2018
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

January 28, 2018
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
January 28, 2018
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.