digitalmars.D - Feature: `static cast`
- Quirin Schroll (45/45) Jun 01 2023 In short, `static cast` would be exactly like C++’s `static_cast`
- Richard (Rikki) Andrew Cattermole (2/2) Jun 01 2023 Alternatively we could just make down casts (including truncation)
- Quirin Schroll (5/7) Jun 01 2023 Making down-casts without `null` checks unsafe wouldn’t solve the
- Paul Backus (14/28) Jun 02 2023 As you say, this is a bug. The cast in the example *should* work
- Nick Treleaven (8/17) Jun 03 2023 Essentially implemented that (but haven't fixed dmd etc yet with
- Quirin Schroll (135/165) Jun 05 2023 I don’t think `*cast(D*) &c` is good. I’d use `cast(D)
- ag0aep6g (3/5) Jun 05 2023 `cast(D*) &c` is the unambiguous type paint. `cast(D) cast(void*) c` can...
- Nick Treleaven (20/25) Jul 03 2023 I tried but it doesn't seem to work:
- Paul Backus (26/40) Jul 03 2023 It calls `opCast`, and then implicitly converts the result from
- Nick Treleaven (4/16) Jul 04 2023 Ah, thanks. So that implicit cast prevents the return type of
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](https://dlang.org/spec/expression.html#cast_class) 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: ```d 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)](https://issues.dlang.org/show_bug.cgi?id=21690) is solved.
Jun 01 2023
Alternatively we could just make down casts (including truncation) without a null/size check unsafe.
Jun 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.
Jun 01 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: ```d 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)](https://issues.dlang.org/show_bug.cgi?id=21690) 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.
Jun 02 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
Jun 03 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:Agreed.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: ```d 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)](https://issues.dlang.org/show_bug.cgi?id=21690) 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`).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. ```d // 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: ```cpp #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.
Jun 05 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`.
Jun 05 2023
On Tuesday, 6 June 2023 at 05:23:40 UTC, ag0aep6g wrote:On 05.06.23 18:06, Quirin Schroll wrote:`*cast(D*) &c` (though that requires an lvalue).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`.I tried but it doesn't seem to work: ```d 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*)`?
Jul 03 2023
On Monday, 3 July 2023 at 17:51:19 UTC, Nick Treleaven wrote:I tried but it doesn't seem to work: ```d 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`: ```d 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: ```d class C { int opCast(T)() if (is(T == void*)) => 42; } // Error: cannot implicitly convert expression // `c.opCast()` of type `int` to `void*` ```
Jul 03 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:Ah, thanks. So that implicit cast prevents the return type of opCast potentially being completely different from the `cast` type.I tried but it doesn't seem to work: ```d 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*`.
Jul 04 2023