I'm trying to do something like this:
type
ThreadID* = distinct uint8
Msg*[T] = object
sender*: ThreadID
receiver*: ThreadID
previous: pointer#ptr[Msg[any]]
content*: T
let space = alloc0(sizeof(Msg[int]))
let m = cast[ptr[Msg[int]]](space)
#let p:ptr[Msg[any]] = m.previous
I would like to define "previous" as something meaning ptr[Msg[any]], so that a ptr to any concrete Msg[?] can be assigned to it. I'm sure this has been asked before, but no mater how I phrase it, nothing came up in the search.
The "ugly workaround" is to cast the pointer to some concrete version of Msg, for example Msg[int], and access the "non generic fields", but not content. OTOH, I'm not even sure if this is safe, or if the compiler could possibly choose a different "byte offset" for the "non generic fields", based on T.
@monster Why not use inheritance?
type
ThreadID* = distinct uint8
AnyMsg = object {.inheritable.}
sender*: ThreadID
receiver*: ThreadID
previous: ptr[AnyMsg]
Msg*[T] = object of AnyMsg
content*: T
let space = alloc0(sizeof(Msg[int]))
let m = cast[ptr[Msg[int]]](space)
let p: ptr[AnyMsg] = m.previous
Does the {.inheritable.} pragma convert an object into a pointer or keep it as a normal object with copy semantics?
Also, something to bear in mind, upon testing this it seems the type is erased when inserting into a seq: (Nim version 0.17.3 2017-10-13)
import typetraits
type
Obj1 = object {.inheritable.}
a: int
Obj2 = object of Obj1
b: float
var
x = newSeq[Obj1]()
x.add Obj1(a: 1)
x.add Obj2(a: 2, b: 3.5) # This is allowed
echo x[1].type.name # output Obj1, not Obj2
echo Obj2(x[1]) # This fails at run time with invalid object conversion, Obj2's extra fields are lost
Should the compiler warn that putting an Obj2 into a seq of Obj1 will drop it's extra data? Or is the extra data meant to be there but there's a bug somewhere?
With a ref, the type is obscured but internally preserved as expected:
import typetraits
type
ARef1 = ref object of RootObj
a: int
ARef2 = ref object of ARef1
b: float
var
y = newSeq[ARef1]()
y.add ARef1(a: 1)
y.add ARef2(a: 2, b: 4.5)
# output ARef1, not ARef2 (probably because 'type' is compiletime)
echo y[1].type.name
# displays data as ARef2 as expected, data is preserved - assuming you can cast appropriately at runtime
echo ARef2(y[1]).repr
Does the {.inheritable.} pragma convert an object into a pointer or keep it as a normal object with copy semantics?
No. It means that the object gains a type header that allows for dynamic dispatch, RTTI, and such.
Should the compiler warn that putting an Obj2 into a seq of Obj1 will drop it's extra data? Or is the extra data meant to be there but there's a bug somewhere?
The problem here is that copy semantics on value types with fixed size and actual polymorphism are incompatible; this is unavoidable. There's simply no place to put any extra fields, so the object is coerced to the supertype. If that didn't happen, methods for the subtype that accessed the missing extra fields would have undefined behavior.
Using pointers (whether ref, ptr, or even passing data as a var parameter) does not have that issue.
No. It means that the object gains a type header that allows for dynamic dispatch, RTTI, and such.
@Jehan Thank you for pointing that out! I thought the {.inheritable.} pragma was the solution to my question, but if it adds a "dynamic dispatch" pointer to the object, than I cannot use it. Sounds like it basically does the same thing as "object of RootObj"; my "messages" are going to be passed to other threads, serialized over the network, ... This just won't do. So I guess I'm back to "previous: pointer" :(
I want to not only send those messages between threads, but also across the network. Anything containing additional pointers would just make things more complicated. Atm, I'm trying something like this (not entirely sure if toMsgBasePtr() is safe):
type
QueueID* = distinct uint16
MsgBase* = object
previous: ptr[MsgBase]
sender*: QueueID
receiver*: QueueID
Msg*[T: not (ref|string|seq)] = object
base*: MsgBase
content*: T
converter toMsgBasePtr(msgPtr: ptr Msg): ptr MsgBase {.inline, noSideEffect.} =
cast[ptr MsgBase](msgPtr)
@Udiknedormin: I meant right now, since VTable are not available of course.
@monster: It is not safe as is, you must ensure your chain of Msg and MsgBase pointers have the same lifetime (by putting them in a seq for example). Otherwise if they are created in different functions, the previous MsgBase would not exist anymore.
Using concepts, doesn't work yet though because no VTable.
type
QueueID* = distinct uint16
MsgBase* = concept x
x.previous is ptr[MsgBase]
x.sender is QueueID
x.receiver is QueueID
Msg*[T: not (ref|string|seq)] = object
previous: ptr[MsgBase]
sender: QueueID
receiver: QueueID
content*: T
proc initMsg[T](previous: ptr[MsgBase], sender, receiver: QueueID, content: T): Msg[T] =
result.previous = previous
result.sender = sender
result.receiver = receiver
result.content = content
let a = initMsg(nil, 1.QueueID, 2.QueueID, 42)
echo a # Error: cannot instantiate: 'MsgBase'