|
C++ templates are weird. They are kind of like a mix between macros and the kind of generic types you would see in other languages. As a macro system, you can use templates to "generate code" but in a very limited way, it's nowhere near as expressive as a full macro system like you'd see in Lisp, Rust, or Haskell. As system for creating generic types, templates are too general-purpose... they give you a lot of power to underspecify things, and you can't typecheck a template until you instantiate it (like a dynamically typed program). It interacts in strange ways with operator overloading, namespace resolution, and other language features like constructors. There are all sorts of template helpers that are necessary but leave you scratching your head until you actually see them in use and find the corner case that they're needed for, like std::declval. There are also all the rules about ADL that interact with templates, and then the cherry on top is SFINAE which lets you control whether a problem with a template is an error or not. Simple templates are not too hard to grok, but writing actual generic types ends up being painful due to the proliferation of corner cases you have to consider, and there are a lot of things that you could do with a macro system that are frustrating and difficult with templates, but since they're "almost possible" you end up contorting the code a little bit and sprinkling on some ad-hoc helper definitions until it works. It's better than the C preprocessor but worse than almost everything else. Languages like Lisp have proper macros, so anything you want to do with a template you can just achieve by defining it in a macro... you have more rope to shoot yourself in the foot with, but the rules for Lisp macros are straightforward and easier to understand (you just write a function that spits out code, basically). Languages like C#, Java, Haskell, etc. have better generic types, so you can do something like say "this function takes an array of type T[], and T must implement the Widget interface, it will give you back a single T". In C++ you can accomplish this goal but you can't as easily encode the constraint that "T must be a Widget", you have to do something weird like put a static assert in the function body, or put a std::enable_if in the type, or accept that there will be template substitution errors with possibly long and difficult-to-read error messages. Mind you, this has really underscored (for me) the amazing developments in our understanding of language design over the past decades. C has preprocessor macros which are barely serviceable and impossible to use for anything complicated, C++ has templates which are more powerful but complicated, Java generics with type erasure are very clean but interact in subtle ways with e.g. covariance and contravariance, C# addresses the covariance and contravariance problems but interacts poorly with operator overloading. TBH I think Go made the right decision to keep both generics and operator overloading out of the core language at the beginning, since not having it at all is in many ways preferable to having a bad version. |