Thread overview
Feature: `static cast`
Jun 01, 2023
Quirin Schroll
Jun 01, 2023
Quirin Schroll
Jun 02, 2023
Paul Backus
Jun 03, 2023
Nick Treleaven
Jun 05, 2023
Quirin Schroll
Jun 06, 2023
ag0aep6g
Jul 03, 2023
Nick Treleaven
Jul 03, 2023
Paul Backus
Jul 04, 2023
Nick Treleaven
June 01, 2023

In short, static cast would be exactly like C++’s static_cast when used with class-type pointers or references, however, D’s static cast cannot be used with anything else, i.e. D’s static cast can’t cast double to int.


Introduce static cast(Type) UnaryExpression with the following semantics: If Type and typeof(UnaryExpression) are class or interface types, it casts UnaryExpression to Type without checks; only the underlying pointer is adjusted if needed. It is invalid if there is no unique way to do it or if the types aren’t class or interface types in the first place.

There are two use-cases:

  1. It gives programmers the option to express: “I know by other means that the cast will succeed, so just do it.” If by the type system, there is no guarantee it will succeed, i.e. it’s a down-cast, that is un-@safe.
  2. It won’t trick unaware programmers into thinking that the following applies when it doesn’t: “Any casting of a class reference to a derived class reference is done with a runtime check to make sure it really is a downcast. null is the result if it isn’t.”

There’s at least once case where I got bitten by 2. and it cost me days to figure out that, contrary to the spec, casting between extern(C++) class references isn’t checked at runtime:

extern(C++) class C { void f() { } }
extern(C++) class D : C { }
void main() @safe
{
    assert(cast(D)(new C) is null); // fails
}

With extern(D), the assertion passes. With a static assert, it passes as well.

There are two issues with the cast in the code snippet:

  1. It is considered @safe when it’s not actually safe.
  2. Even if it weren’t safe, there should be an indication that the cast isn’t a safe, dynamically checked cast.

Introducing static cast can solve both of them. It wouldn’t be @safe in this case and it indicates that the cast isn’t dynamically checked. Also, in this case, it would be an error not to use it until issue 21690 (Unable to dynamic cast extern(C++) classes) is solved.

June 02, 2023
Alternatively we could just make down casts (including truncation) without a null/size check unsafe.

June 01, 2023

On Thursday, 1 June 2023 at 14:55:32 UTC, Richard (Rikki) Andrew Cattermole wrote:

>

Alternatively we could just make down casts (including truncation) without a null/size check unsafe.

Making down-casts without null checks unsafe wouldn’t solve the issue that null checks on extern(C++) class object down-casts don’t work properly. That would make the problem worse.

June 02, 2023

On Thursday, 1 June 2023 at 14:13:27 UTC, Quirin Schroll wrote:

>

There’s at least once case where I got bitten by 2. and it cost me days to figure out that, contrary to the spec, casting between extern(C++) class references isn’t checked at runtime:

extern(C++) class C { void f() { } }
extern(C++) class D : C { }
void main() @safe
{
    assert(cast(D)(new C) is null); // fails
}

[...] issue 21690 (Unable to dynamic cast extern(C++) classes) is solved.

As you say, this is a bug. The cast in the example should work the same way as dynamic_cast<D*>(new C()) in C++, but it doesn't, because the D compiler doesn't know how to access (or generate) the C++ RTTI.

Until the bug is fixed, casts like these should probably be a compile-time error ("Error: dynamic casting of C++ classes is not implemented"). If you want the current behavior, which is a reinterpreting cast, the normal way to write that in D is with a pointer cast like *cast(D*) &c (which is, appropriately, @system).

Of course, this would be a fairly disruptive breaking change, so it would require a deprecation period, but I think it would be worth it to disarm such an obvious footgun.

June 03, 2023

On Friday, 2 June 2023 at 17:53:41 UTC, Paul Backus wrote:

>

Until the bug is fixed, casts like these should probably be a compile-time error ("Error: dynamic casting of C++ classes is not implemented"). If you want the current behavior, which is a reinterpreting cast, the normal way to write that in D is with a pointer cast like *cast(D*) &c (which is, appropriately, @system).

Or cast(D)cast(void*)c.

>

Of course, this would be a fairly disruptive breaking change, so it would require a deprecation period, but I think it would be worth it to disarm such an obvious footgun.

Essentially implemented that (but haven't fixed dmd etc yet with workaround):
https://github.com/dlang/dmd/pull/15293

A less disruptive change could be just to mark a derived cast as unsafe:
https://github.com/dlang/dmd/pull/15294

June 05, 2023

On Friday, 2 June 2023 at 17:53:41 UTC, Paul Backus wrote:

>

On Thursday, 1 June 2023 at 14:13:27 UTC, Quirin Schroll wrote:

>

There’s at least once case where I got bitten by 2. and it cost me days to figure out that, contrary to the spec, casting between extern(C++) class references isn’t checked at runtime:

extern(C++) class C { void f() { } }
extern(C++) class D : C { }
void main() @safe
{
    assert(cast(D)(new C) is null); // fails
}

[...] issue 21690 (Unable to dynamic cast extern(C++) classes) is solved.

As you say, this is a bug. The cast in the example should work the same way as dynamic_cast<D*>(new C()) in C++, but it doesn't, because the D compiler doesn't know how to access (or generate) the C++ RTTI.

Until the bug is fixed, casts like these should probably be a compile-time error ("Error: dynamic casting of C++ classes is not implemented").

Agreed.

>

If you want the current behavior, which is a reinterpreting cast, the normal way to write that in D is with a pointer cast like *cast(D*) &c (which is, appropriately, @system).

I don’t think *cast(D*) &c is good. I’d use cast(D) cast(void*) c, making unambiguously clear I want a type paint. Also, one does not want a reinterpret_cast, one wants a static_cast. Only in specific cases, the reinterpret_cast happens to be the same as the static_cast.

Up until now, because the up-cast is a static_cast, I thought D did a static_cast for a down-cast as well, but looking like a dynamic_cast. That would be wrong, but only slightly wrong. That in fact it does a reinterpret_cast, is terribly wrong.

>

Of course, this would be a fairly disruptive breaking change, so it would require a deprecation period, but I think it would be worth it to disarm such an obvious footgun.

Yes. But it’s only surface-level. To reiterate, the fact that one direction is a static_cast while the other is a reinterpret_cast is a terrible footgun.

D doesn’t offer a static_cast, but it should, for two reasons:

  • One might want a static_cast instead of a dynamic_cast for performance reasons.
  • As long as D has no C++ RTTI solution, the best it should offer for a down-cast is a static_cast and that one shouldn’t syntactically look like a dynamic cast, in fact, if it were to work with extern(D) classes, it cannot look like a dynamic cast. A static_cast is only equivalent to a reinterpret_cast when using single-inheritance; to be precise, non-virtual single inheritance. C++ and D support multi-inheritance. D only supports interfaces, but it still counts, example below; also, interfaces are virtual inheritance.

For extern(C++) classes there is no down-cast except a reinterpret_cast and a reinterpret_cast does not do the Right Thing except when it does by happenstance. That is the problem.

For extern(D) classes, the dynamic_cast does the Right Thing. Occasionally, it’s only more expensive than it needs to be, but at least it’s there.

// compile with -dip1000
extern(C++) interface I1 { int x() const scope @nogc @safe; }
extern(C++) interface I2 { int y() const scope @nogc @safe; }
extern(C++) class C : I1, I2
{
    private int _x, _y;
    this(int x, int y) @nogc @safe pure { _x = x, _y = y; }
    override int x() const scope @nogc @safe => _x;
    override int y() const scope @nogc @safe => _y;
}

void main() @nogc @safe
{
    import std.stdio;

    scope C c = new C(1, 2);
    I1 i1 = cast(I1) c;
    I2 i2 = cast(I2) c;
    assert(i1.x == 1);
    assert(i2.y == 2);
    () @trusted
    {
        C c1 = cast(C) i1;
    	assert(c1.x == 1); // ok
        //assert(c1.y == 2); // fails??? (y is random garbage)

        C c2 = cast(C) i2;
        //assert(c2.x == 1); // fails (!)
    	assert(c2.x == 2); // passes (!)
        //assert(c2.y == 2); // sefault (!)
    }();
}
  • The first two asserts prove that the cast from C up to I1 and I2 is a static_cast. Variable i2 refers to the I2 subobject of c, not the I1 subobject as it would if it were a reinterpret_cast. The cast() is in fact unnecessary.
  • The @trusted asserts prove that the cast from I1 and I2 down to C is a reinterpret_cast. It’s unsafe of course, and it does not do the Right Thing as the second bunch of asserts shows: cast(C) i2 is a misaligned pointer, its .y component leads to a segmentation fault. How do I get from i2, knowing it’s in fact a C object, to its x component? With a static_cast - which D doesn’t have.
  • c, c1 and c2 point to different addresses. Were I1 not an interface but a class and its x() an abstract function, at least the random garbage wouldn’t occur. This is the special case where the reinterpret_cast happens to be the same as a static_cast.

This is how C++ does it:

#include <iostream>

struct B1 { int x; virtual ~B1() = default; B1(int x) : x{x} {} };
struct B2 { int y; virtual ~B2() = default; B2(int y) : y{y} {} };
struct D : B1, B2 { D(int x, int y) : B1{x}, B2{y} { } };

int main()
{
    D d{ 1, 2 };
    B1* b1s = static_cast<B1*>(&d);
    B2* b2s = static_cast<B2*>(&d);
    B1* b1r = reinterpret_cast<B1*>(&d);
    B2* b2r = reinterpret_cast<B2*>(&d);
    std::cout << b1s->x << std::endl; // 1
    std::cout << b2s->y << std::endl; // 2
    std::cout << b1r->x << std::endl; // 1 (reinterpret_cast is a type paint)
    std::cout << b2r->y << std::endl; // 1 (reinterpret_cast is a type paint)

    D* d1s = static_cast<D*>(b1s);
    D* d2s = static_cast<D*>(b2s);
    std::cout << d1s->x << std::endl; // 1
    std::cout << d2s->y << std::endl; // 2

    //B2* b2b1s = static_cast<B2*>(b1s); // error: static_cast cannot cast sideways
    //B1* b1b2s = static_cast<B1*>(b2s); // error: (same)

    B1* b2b1d = dynamic_cast<B1*>(b2s); // dynamic_cast can cast sideways
    B2* b1b2d = dynamic_cast<B2*>(b1s); // (same)
    std::cout << b2b1d->x << std::endl; // 1
    std::cout << b1b2d->y << std::endl; // 2

    B1* b2b1r = reinterpret_cast<B1*>(b2s); // reinterpret_cast is a type paint
    B2* b1b2r = reinterpret_cast<B2*>(b1s); // (same)
    std::cout << b2b1r->x << std::endl; // 2 (!)
    std::cout << b1b2r->y << std::endl; // 1 (!)
}

D is supposed to do a reinterpret_cast when pointers (usually void*) are involved. reinterpret_cast is inherently unsafe, in fact, it’s so unsafe, the C++ standard bans it from being used in constexpr.

The reinterpret_cast between B1 and D is fine because the B1 subobject has the same address as the D object.

The reinterpret_cast between B2 and D or B1 is fine only because B2 has the exact same layout as B1 and D starts with the B1 subobject. (Were B2 to contain a float instead of an int, it would be undefined behavior to access it.) As the example demonstrates, a reinterpret_cast is a type paint.

A static_cast gives you a pointer to the correct subobject; it’s not just a type paint, it does a pointer adjustment. For an up-cast, this is 100% safe (cf. the up-reinterpret_cast generally is not). For a down-cast, it’s not safe generally, but if the object actually is of the (derived) type the cast targets, the cast is defined behavior. A static_cast cannot do a sideways cast.

June 06, 2023
On 05.06.23 18:06, Quirin Schroll wrote:
> I don’t think `*cast(D*) &c` is good. I’d use `cast(D) cast(void*) c`, making unambiguously clear I want a type paint.

`cast(D*) &c` is the unambiguous type paint. `cast(D) cast(void*) c` can be intercepted by `opCast`.
July 03, 2023

On Tuesday, 6 June 2023 at 05:23:40 UTC, ag0aep6g wrote:

>

On 05.06.23 18:06, Quirin Schroll wrote:

>

I don’t think *cast(D*) &c is good. I’d use cast(D) cast(void*) c, making unambiguously clear I want a type paint.

cast(D*) &c is the unambiguous type paint.

*cast(D*) &c (though that requires an lvalue).

>

cast(D) cast(void*) c can be intercepted by opCast.

I tried but it doesn't seem to work:

class C
{
    int* opCast(T)()
    if (is(T == void*)) => new int(42);
}

void main()
{
    C c;
    auto p = cast(void*)c;
    pragma(msg, typeof(p)); // void*
}

Unless I've made some mistake above, I don't think intercepting cast(void*) should be supported. There's probably quite a lot of places where people use it to get the class instance pointer. Can we change the spec to ignore opCast!(void*)?

July 03, 2023

On Monday, 3 July 2023 at 17:51:19 UTC, Nick Treleaven wrote:

>

I tried but it doesn't seem to work:

class C
{
    int* opCast(T)()
    if (is(T == void*)) => new int(42);
}

void main()
{
    C c;
    auto p = cast(void*)c;
    pragma(msg, typeof(p)); // void*
}

It calls opCast, and then implicitly converts the result from int* to void*. You can see it if you add a side effect to opCast:

class C
{
    int* opCast(T)()
    if (is(T == void*))
    {
        import std.stdio;
        writeln("Called opCast"); // will be printed
        return new int(42);
    }
}

And if you change opCast to return an incompatible type like int, you will get a message about a failed implicit conversion:

class C
{
    int opCast(T)() if (is(T == void*)) => 42;
}
// Error: cannot implicitly convert expression
// `c.opCast()` of type `int` to `void*`
July 04, 2023

On Monday, 3 July 2023 at 18:15:43 UTC, Paul Backus wrote:

>

On Monday, 3 July 2023 at 17:51:19 UTC, Nick Treleaven wrote:

>

I tried but it doesn't seem to work:

class C
{
    int* opCast(T)()
    if (is(T == void*)) => new int(42);
}

It calls opCast, and then implicitly converts the result from int* to void*.

Ah, thanks. So that implicit cast prevents the return type of opCast potentially being completely different from the cast type.