Using a view <https://nim-lang.github.io/Nim/manual_experimental.html#view-types> the generated code boils down to one pointer copy. import std / [macros, genasts] {.experimental: "views"} macro immut(n: typed) = if n.kind notin {nnkSym, nnkHiddenDeref}: error("Expected a variable", n) let (n, name) = if n.kind == nnkHiddenDeref: (n[0], ident($n[0])) else: (n, ident($n)) typ = block: let typ = n.getType if typ[0].eqident"var": typ[^1] else: typ result = genast(typ, name = ident(n.repr), n): let name: lent typ = n proc doThing(i: var int) = immut i echo i Run N_LIB_PRIVATE N_NIMCALL(void, doThing__immut_49)(NI* i) { NI* i_2; tyArray__nHXaesL0DJZHyVS07ARPRA T1_; i_2 = i; nimZeroMem((void*)T1_, sizeof(tyArray__nHXaesL0DJZHyVS07ARPRA)); T1_[0] = dollar___systemZdollars_3((*i_2)); echoBinSafe(T1_, 1); } Run
I had to google for this, but at least in C casting in this case probably doesn't have any runtime cost because the fundamental type isn't changing. It's hard to say for sure without looking at the assembly.