Subclass TcpSocket?
Jonathan M Davis
newsgroup.d at jmdavisprog.com
Tue Mar 18 21:29:10 UTC 2025
On Tuesday, March 18, 2025 1:19:49 PM MDT bauss via Digitalmars-d-learn wrote:
> On Tuesday, 18 March 2025 at 18:04:12 UTC, Steven Schveighoffer
> wrote:
> > On Tuesday, 18 March 2025 at 07:42:37 UTC, Jonathan M Davis
> > wrote:
> >> The base class constructors are not nothrow, so WrappedTCP's
> >> constructor cannot be nothrow. There really isn't a way out of
> >> that, because if a constructor throws, the object's state is
> >> destroyed. So, catching and handling the Exception to make
> >> your function nothrow isn't really an option like it would be
> >> with many functions.
> >
> > FWIW, this does compile:
> >
> > ```d
> > class A
> > {
> > this() {}
> > }
> >
> > class B : A
> > {
> > this() nothrow {
> > try {
> > super();
> > } catch(Exception e) {}
> > }
> > }
> > ```
> > Not sure if it should...
> >
> > -Steve
>
> Interesting that it doesn't break anything really.
>
> ```
> import std;
>
> class A
> {
> this() { throw new Exception("test"); }
> }
>
> class B : A
> {
> int a;
>
> this() nothrow {
> try {
> super();
> } catch(Exception e) {}
> }
> }
>
>
> void main()
> {
> auto b = new B;
> b.a = 200;
>
> writeln(b.a);
> }
> ```
How bad it is depends on the type, but if an exception is thrown from a
constructor, then the object is not properly constructed, and its member
variables which were constructed are then destroyed.
So, if any members have destructors, they're going to be left in a destroyed
state, are there are no guarantees what that looks like. It's normally
supposed to be the case that you can't even access a destroyed object,
though given the mess that we're getting with move constructors and __rvalue
where objects can be destroyed multiple times, the compiler really should
enforce that a destroyed object is set to its init value (or just do it
itself and let the optimizer optimize it out if appropriate), but there is
no such guarantee at present.
It's even worse if a member is const or immutable, since then if it has a
destructor run and then you somehow access the object afterwards (as
Steven showed can be done in a derived class constructor right now), the
const or immutable object was mutated, which violates the guarantees that
const and immutable are supposed to have.
Member variable that weren't actually initialized in the constructor and
don't have constructors will have their init value after the constructor
throws, but if the type disables default initialization that could be
problematic, and if the class' logic assumes that the constructor succeeded
(which would be a pretty normal thing to do), then not having completed the
constructor could leave the object in an invalid state as far as its
internal logic goes.
And any member variables which were initialized and don't have destructors
will end up in whatever state they were in when the exception was thrown,
which again, could cause logic issues for the class.
In principle, when a constructor fails, the compiler is supposed to be
undoing the partial construction, and then the object is supposed to be
inaccessible - and for the most part, the language enforces that, because
other than in the constructor itself, you normally only have access to an
object once its constructor has completed. The primarily exception is
emplace, because you're constructing an object in memory that you control
rather than constructing it on the stack or asking the GC to allocate
memory, construct the object in it, and then give you access to that memory.
As for undoing the partial construction, the only thing that the compiler
normally _needs_ to do for correctness is then run the destructors of the
member variables, and the state of the object afterwards doesn't realy
matter, because it's inaccessible. So, it doesn't bother trying to put the
object in a sane state beyond doing that clean up.
So, it's definitely hole in the type system that a derived class constructor
can access catch an exception and continue construction.
And testing this a bit, I don't think that the members are even being
destroyed in the correct order. This code
```
import std.stdio;
struct S(string name)
{
int i;
~this()
{
writefln("destroyed %s", name);
i = 42;
}
}
class A
{
S!"s" s;
this()
{
writeln("begin A()");
throw new Exception("foo");
}
}
class B : A
{
S!"t" t;
this()
{
writeln("begin B()");
super();
writeln("end B()");
}
}
class C : B
{
S!"u" u;
this()
{
writeln("begin C()");
try
super();
catch(Exception e)
{}
writeln("end C()");
}
}
void main()
{
writeln("before");
auto obj = new C;
writefln("obj.s.i: %s", obj.s.i);
writefln("obj.t.i: %s", obj.t.i);
writefln("obj.u.i: %s", obj.u.i);
}
```
prints out
before
begin C()
begin B()
begin A()
destroyed s
destroyed t
end C()
obj.s.i: 42
obj.t.i: 42
obj.u.i: 0
So, as you can see, not only are destroyed member variables accessible, but
the members in A are destroyed before the members in B whereas destruction
should be going in the reverse order of construction.
- Jonathan M Davis
More information about the Digitalmars-d-learn
mailing list