The example you gave is the most trivial one possible. There is 0 reason to write that code over PrintAnything(item Stringer). Go doesn't even let you do the following:
auto foo(auto& x) { return x.y; }
The equivalent Go code would be
package main
import "fmt"
func foo[T any, V any](x T) V {
return x.y
}
type X struct {
y int
}
func main() {
xx := X{3}
fmt.Println(foo[*X, int](&xx))
}
which does not compile because T (i.e. any) does not contain a field called y. That is not duck typing, the Go compiler does not substitute T with *X in foo's definition like a C++ compiler would.
Not to mention Go's generics utterly lack metaprogramming too. I understand that's almost like a design decision, but regardless it's a big part of why people use templates in C++.
Interesting, thank you for the example. I'm mostly used to how Rust handles this, and in its approach individual items such as functions need to be "standalone sane".
func foo[T any, V any](x T) V {
return x.y
}
would also not fly there, because T and V are not usefully constrained to anything. Go is the same then. I prefer that model, as it makes local reasoning that much more robust. The C++ approach is surprising to me, never would have thought that's possible. It seems very magic.
Lots of C++ is driven by textual substitution, the same mess which drives C macros. So, not magic, but the resulting compiler diagnostics are famously terrible since a compiler has no idea why the substitution didn't work unless the person writing the failed substitution put a lot of work in to help a compiler understand where the problem is.
package main
import "fmt"
type Yer[T any] interface {
Y() T
}
func foo[V any, X Yer[V]](x X) V {
return x.Y()
}
type X struct {
y int
}
func (x X) Y() int { return x.y }
func main() {
xx := X{3}
fmt.Println(foo(&xx))
}
It is not equivalent because, per the monomorphization discussion above, putting an interface in there means that you incur the cost of a virtual function call. The C++ code will compile down to simply accessing a struct member once inlined while the Go code you wrote will emit a ton more instructions due to the interface overhead.
Depending on which implementation you use, the following may produce the same instructions as the example above. Go on, try it!
package main
import "fmt"
func foo(x *X) int {
return x.y
}
type X struct {
y int
}
func main() {
xx := X{3}
fmt.Println(foo(&xx))
}
Now, gc will give you two different sets of instructions from these two different programs. I expect that is what you are really trying and failing to say, but that is not something about Go. Go allows devirualizing and monomorphizing of the former program just fine. An implementation may choose not to, but the same can be said for C++. Correct me if I'm wrong, but from what I recall devirtualization/monomorphization is not a requirement of C++ any more than it is of Go. It is left to the discretion of the implementer.
Tried it out in godbolt, yes you are right that with the above example gc is able to realize that
func foo[V any, X Yer[V]](x X) V
can be called with only one type (X) and therefore manages to emit the same code in main_main_pc0. It all falls apart when you add a second struct which satisfies Yer [1], which leads the compiler to emit a virtual function table instead. You can see it in the following instructions in the code with a second implementation for Yer added:
Was it really necessary to try in gc...? We already talked about how it produces different instructions for the different programs. Nice of you to validate what I already said, I suppose, but this doesn't tell any of us anything we didn't already know.
The intent was for you to try it in other implementations to see how they optimize the code.
What are you referring to here? Code like
looks like type-safe duck typing to me.