Thread overview
Subclass TcpSocket?
Mar 18
Enamisu
Mar 18
bauss
March 17

The presence of the "accepting" API in Socket seems to indicate that subclassing Socket/TcpSocket is intended to be supported. But I'm just not seeing my way through the twisty maze of pure and @nothrow barriers?

Andy

import std.socket : TcpSocket, Address, getAddress;

class WrappedTCP : TcpSocket {
    string label;

    this(string _label) nothrow {
        this.label = _label;
        super();
    }
    override WrappedTCP accepting() nothrow {
        return new WrappedTCP(this.label);
    }
}
void main() {
    import std.stdio : writeln;

    auto s = new WrappedTCP("My Label");
    writeln(s.label);
    s.listen(4);
    auto s2 = cast(WrappedTCP)(s.accept());
    writeln(s2.label);
}
March 18

On Monday, 17 March 2025 at 22:14:21 UTC, Andy Valencia wrote:

>

The presence of the "accepting" API in Socket seems to indicate that subclassing Socket/TcpSocket is intended to be supported. But I'm just not seeing my way through the twisty maze of pure and @nothrow barriers?

[...]

It seems to me that the accepting method should return TcpSocket, not WrappedTCP.

March 18
On Monday, March 17, 2025 4:14:21 PM MDT Andy Valencia via Digitalmars-d-learn wrote:
> The presence of the "accepting" API in Socket seems to indicate that subclassing Socket/TcpSocket is intended to be supported. But I'm just not seeing my way through the twisty maze of pure and @nothrow barriers?
>
> Andy
>
>      import std.socket : TcpSocket, Address, getAddress;
>
>      class WrappedTCP : TcpSocket {
>          string label;
>
>          this(string _label) nothrow {
>              this.label = _label;
>              super();
>          }
>          override WrappedTCP accepting() nothrow {
>              return new WrappedTCP(this.label);
>          }
>      }
>      void main() {
>          import std.stdio : writeln;
>
>          auto s = new WrappedTCP("My Label");
>          writeln(s.label);
>          s.listen(4);
>          auto s2 = cast(WrappedTCP)(s.accept());
>          writeln(s2.label);
>      }

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.

accepting is pure in the base class, so your implementation must be pure. If you can't do that, then you're out of luck. And given what you're trying to do, that means that your constructor must be pure, and looking at TcpSocket, that's not possible, because none of its constructors are pure.

If you gave up on deriving from TcpSocket and derived from Socket instead, you might be able to get it to work. TcpSocket doesn't have _any_ attributes on its constructors, so nothing that requires attributes is going to work with them - including accepting. Really, attributes are _not_ used well in std.socket, and it's clearly problematic for what you're trying to do. But std.socket is quite old and honestly isn't very good, so it doesn't surprise me particularly if you're having issues with it.

So, you can try subclassing Socket and making that work, but I don't know how easy that would be. I think that the classes in std.socket were intended to be extensible, but what's there right now isn't very good. It's a module that came from D1, so it predates all of the attributes, and the way that the attributes were tacked on later is clearly bad. So, if anything, it needs a complete redesign or replacement - which will happen in Phobos v3, but that's quite a ways off. So, if you can't make std.socket work the way that you need, you'll either need to find a solution on code.dlang.org or make your own using the underlying C calls.

- Jonathan M Davis



March 18

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:

class A
{
    this() {}
}

class B : A
{
    this() nothrow {
        try {
            super();
        } catch(Exception e) {}
    }
}

Not sure if it should...

-Steve

March 18

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:

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);
}
March 18
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