Но по слухам, в более свежих версиях "детки" обзавелись шаблончиками.
Дженерики это всё таки не шаблоны. Я плотно не работал, но из того что знаю действительно более продвинутая версия у C# (чем у Java).
Но они всё равно не шаблоны. Если откинуть примитивные типы, то всё становится так же как в Java, дженерики - это НЕ кодогенерация, дженерики - это отличный синтаксический сахар над проверками типов времени компиляции. Но и только.
Это просто объяснить - все объекты в Java/C# являются наследниками общего предка Object.
В первых версиях языков (без дженериков) коллекции типа List хранили ссылки на Object и любой объект туда положенный доставался через метод Object Get( int i ) как Object и его надо было привести к тому типу который туда ложился, чтобы работать с ним дальше.
Так вот, дженерики это по большей части синтаксический сахар над тем, чтобы написать:
List<MyObject> list и при этом компилятор запретит вызывать Add не от MyObject или его наследника, а Get вернет сразу не Object, а MyObject, сам выполнить преобразование типа (причем даже не нужно делать проверки валидности на этом этапе, см. ниже).
Важно то, что List<MyOtherObject> даже не связанный с MyObject наследованием будет в рантайме пользоваться абсолютно тем же машинным кодом скомпилированной один раз коллекции List<T>, потому что все ссылки одинаковы, все есть наследники Object, а т.к. компилятор в компил-тайме запретил ложить в List<MyOtherObject> что либо иное, нежели MyOtherObject, то он может быть уверен, что там лежит именно оно, а Get даже никакой проверки типов делать не обязан - заранее известно что каст к MyOtherObject пройдёт без ошибок.
Причём из кода это действительно дико похоже и в объявлении и в использовании на std::list<T>, но суть сильно другая, до мощи кодогенерации шаблонов С++ тут как до луны пешком.
Например, т.к. дженериковый класс MyClass<T> ничего не знает о типе T, то он не может делать никаких предположений, кроме того что там лежит Object. Сам как таковой.
Чтобы заставить MyClass<T> "видеть" методы у шаблонотипа надо явно указать от кого он производится: class MyClass<T> where T : MyNode, только тогда дженериковый тип MyClass<T> сможет использовать методы из MyNode, но и только наследников от MyNode в него можно будет положить.
Как то так. В общем это не кодогенерация, но и то, что в своих рамках это очень мощный и нужный сахар - верно. Более того, там даже рождаются на стыке этих ограничений концепции неизвестные С++, причём невозможные в нём к воспроизведению. Например List<Parent> можно передать в метод принимающий List<Child>, если метод читает из списка, но нельзя - если пишет. И наоборот - в метод принимающий List<Child> можно передать List<Parent>, если метод пишет в список, но не читает.
На бытовом языке: в корзину фруктов всегда можно положить грушу, но из корзины фруктов не всегда можно вытащить грушу.
Зато из корзины груш всегда берешь фрукт, но не любой фрукт можно положить в корзину груш.