← Back to context

Comment by Merovius

2 days ago

> C++ templates are duck typed at compile time.

"Compile time" is not the right distinction. This is about "instantiation time". Go's implementation specifically allows to type-check the body and the call separately. That is, if you import a third-party package and call a generic function, all the type checker needs to look at to prove correctness is the signature of the function. It can ignore the body.

This is especially relevant, if you call a generic function from a generic function. For C++, proving that such a call is correct is, in general, NP-complete (it directly maps to the SAT problem, you need to prove that every solution to one arbitrary boolean formula satisfies a different boolean formula). So the designers made the conscious decision to just not do that, instead delaying that check to the point at which the concrete type used to instantiate the generic function is known (because checking that a specific assignment satisfies a boolean formula is trivial). But that also means that you have to (recursively) type-check a generic function again and again for every type argument provided, which can drive up compilation time.

A demonstration is this program, which makes gcc consume functionally infinite amount of memory and time: https://godbolt.org/z/crK89TW9G (clang is a little bit more clever, but can ultimately be defeated using a similar mechanism).

Avoiding these problems is a specific cause for a lot of the limitations with Go's generics.

> Look at Haskell type classes or Rust's traits for some classic examples of how to 'type' your generics. (And compare to what Go and C++ are doing.)

Yes, those are a different beasts altogether and the differences between what Go is doing and what Haskell and Rust are doing requires different explanations.

Though it's illustrative, because it turns out Rust also intentionally limited their generics implementation, to solve the kinds of performance problems Go is worried about. Specifically, Rust has the concept of "Dyn compatibility" (formerly "Object safety") which exists because otherwise Rusts goal of zero-cost abstractions would be broken. Haskell doesn't have this problem and will happily allow you to use the less efficient but more powerful types.

(All of this should have the caveat that I'm not an expert in or even a user of any of these languages. It's half-knowledge and I might be wrong or things might have changed since I last looked)