Основное отличие - в том, во что они превращаются после компиляции. Грубо говоря, сиплюснутые темплейты во время компиляции разворачиваются в новые классы, уникальные для каждого варианта параметров темплейта, в рантайме используется куча разных классов. Генериксы проще - на основе их параметров проверяется правильность их использования в коде, только на уровне компиляции, после чего вся инфа о параметрах генерик-класса стирается и в рантайме он представлен одним обобщённым классом.
Проблема в том, что ограничения могут поменяться, но интерфейс менять не есть хорошо. И ограничения на уровне интерфейса это жёстко, только классы или только интегральные типы.
Так или иначе ограничения в плюсах сделать можно, и хорошо, что это не на уровне интерфейса.
В том то и дело, что явно декларировать ограничения в самом интерфейсе это не очень удачно. Поменялись у нас ограничения, нужно менять интерфейс, а так «стало»/«не стало» компилиться, но интерфейс остался прежний.
>независимо от того, явно или неявно - но ограничения есть. ты считаешь что duck typing лучше явного контракта?
Контракт лучше подходит таким языкам как Java. Как понимаю в Java это сделано через интерфейс, в С++ такого явного понятия «интерфейс» нету.
можно пример, в котором ограничения могут поменяться?
В хорошо продуманном, отлаженном и проверенным временем интерфейсе такое вряд ли возможно. Но в какой-нибудь сторонней либе с пылу с жару, почему нет?
Герберт Шилдт «Java 2 v5.0 (Tiger). Новые возможности»
Отвратительный автор, после прочтения одного из его руководств по C++ обхожу стороной. Много неточностей, ошибок и не расставленных там где надо акцентов. ACCU большинство его книг не рекомендует к прочтению: http://accu.org/index.php?module=bookreviews&func=search - поиск по автору Herbert Schildt.
У Шилдта была пара приличных книг - «Теория и практика С++» и «Самоучитель С++». Всё остальное - редкостное гавно, особенно какие-то «советы/рецепты программирования от шилдта» или как-то так.
Навскидку - в с++ нельзя ограничить тип параметров шаблона.
И это хорошо.
Когда твой сосед попытается использовать твой шаблон с неподходящим типом и получит, в лучшем случае, километры говн в качестве сообщения об ошибке - будет плохо.
В чём разница между generics Явы и templates Ц++ ?
Темплейты - это такой препроцессор на стероидах. Можно руками (или автоматически) развернуть все темплейты, превратив код в плюсовый без темплейтов. Он вырастет в несколько раз, но работать будет точно так же. Собственно, компиляторы и должны это делать в качестве первой ступени обработки кода - либо притворяться, что делают. В результате, шаблон превращается в некоторое количество инстансов, каждый из которых уже точно знает, к чему он применён.
Дженерики - не такие. В них код дженерика не разворачивается, и до рантайма не имеет понятия, к чему именно будет применяться; однако, в заголовке дженерика указывается, какой интерфейс ОБЯЗАТЕЛЬНО будет поддерживать то, к чему он будет применён, и компилятор это гарантирует (ну, точнее, старается). Поэтому, в частности, дженерики позволяют писать код, где дженерик применяется к произвольному (не определённому в статике) количеству типов.
Пример (выморочный):
import java.io.*;
interface Foo {
Integer getNum();
}
class Empty implements Foo {
public Integer getNum() {
return 0;
}
}
class Bar<X extends Foo> implements Foo {
X Baz;
Bar(X _Baz){
Baz = _Baz;
}
public Integer getNum() {
return Baz.getNum() + 1;
};
};
public class Sample {
private static <X extends Foo> Integer quux(Integer n, X foo){
if (n == 0){
return foo.getNum();
} else {
return quux(n-1, new Bar<X>(foo));
}
}
public static void main(String []args){
try {
BufferedReader is = new BufferedReader(new InputStreamReader(System.in));
String line = is.readLine();
Integer val = Integer.parseInt(line);
System.out.println(quux(val, new Empty()));
} catch (NumberFormatException ex) {
System.err.println("Not a valid number");
} catch (IOException e) {
System.err.println("Unexpected IO ERROR");
}
}
}
В рантайме может быть создано значение типа Bar<Bar<Bar...<Empty>...>>, причём длина цепочки заранее не определена. Поэтому аналогичный пример на плюсах упадёт (попытается развернуть шаблон бесконечное число раз), а в жабе оно компилится и работает (потому что не пытается разворачивать вообще).
Лучше километровые матюки при неправильном использовании, чем бессмысленный запрет многих правильных использований.
Это каких? Единственное, что требуется при использовании шаблона - поддержка нужного интерфейса. Если у вас этого интерфейса нет, то использование по любому неправильное.
>Единственное, что требуется при использовании шаблона - поддержка нужного интерфейса
Да, в теории это замечательно. На практике же авторы дженериков выбирают избыточный интерфейс. Т.е. интерфейс требует методы a(), b() и c(), а реально нужны только a() и c(), но оптимальный интерфейс описывать лень. Соответственно юзерам дженериков приходится описывать этот нахрен не нужный b(). Для джавы тормознутость и многословность - норма жизни, а в c++ это неприемлимо.
Одна из причин выкидывания концетов из c++0x в том, что они потребуют просто астрономических объёмов писанины для правильного использования, а профита - шиш.
Никто не спорит, что на одном тьюринг-полном языке всегда можно сделать то, что делается на другом. Цимес в другом: концепты давали возможность НАЛОЖИТЬ ограничения, но одновременно давали очень мало возможностей ими ПОЛЬЗОВАТЬСЯ. Поэтому они, собственно, и не нужны.
Вот элементарное стирание типов по твоему примеру.
Конечно, более похоже на яву было бы *не* делать ErasedCons предком Cons (чтобы его можно было определить *после* определения Cons), но это требует библиотечки, которую до сих пор мне лень написать.
#include <iostream>
class ErasedCons
{
public:
virtual void print()=0;
};
class Nil: public ErasedCons
{
virtual void print() { std::cout << "Nil\n"; }
};
template<class T> class Cons: public ErasedCons
{
public:
Cons(T* tail, int payload): tail(tail), payload(payload) {}
virtual void print() { std::cout << "Cons: " << payload << ",\n"; tail->print(); }
private:
T* tail;
int payload;
};
template<class T> ErasedCons* cons(T* tail, int payload) { return new Cons<T>(tail, payload); }
ErasedCons* erased_cons(int i)
{
ErasedCons* result = new Nil();
for( int j=1; j<=i; j++) {
result = cons(result,j*j);
}
return result;
}
int main(int argc, char** argv)
{
int i;
std::cin >> i;
ErasedCons* ec = erased_cons(i);
ec->print();
return 0;
}
> Цимес в другом: концепты давали возможность НАЛОЖИТЬ ограничения, но одновременно давали очень мало возможностей ими ПОЛЬЗОВАТЬСЯ. Поэтому они, собственно, и не нужны.
Это я не понял.
Ладно, вот в моем примере все стирание типов свелось к ": public ErasedCons", и сами классы Cons и Nil *не* пришлось редактировать, но в более сложном случае может быть придется. хз.
все-таки пришлось редактировать вне функции print, а виртуальная функция print таки добавилась во внутрь, но этого можно избежать через библиотечку-виртуализатор
Поэтому аналогичный пример на плюсах упадёт (попытается развернуть шаблон бесконечное число раз), а в жабе оно компилится и работает (потому что не пытается разворачивать вообще).
Я тебе написал аналогичный пример, который не падает.
Цимес в другом: концепты давали возможность НАЛОЖИТЬ ограничения, но одновременно давали очень мало возможностей ими ПОЛЬЗОВАТЬСЯ. Поэтому они, собственно, и не нужны.
Это я не понял.
По-прежнему компилятор может отказаться работать с тем, что соответствует специфицированному интерфейсу (т.к. внутри шаблона всё равно может использоваться что угодно ещё).
По-прежнему невозможно отдельно скомпилить код шаблона и код, использующий шаблон.
По-прежнему при написании кода шаблона известные ограничения на его аргументы не страхуют программиста.
> По-прежнему компилятор может отказаться работать с тем, что соответствует специфицированному интерфейсу
Это более сложный вопрос, чем кажется с «теоретических» позиций — отнюдь не всегда полезно сначала специфицировать интерфейс, а затем проверять эту спецификацию.
Хотя иногда это надо.
По-прежнему невозможно отдельно скомпилить код шаблона и код, использующий шаблон.
Не, не аналогичный. Контекст был - разница между шаблонами и дженериками. То, что можно сделать как-то ещё, к вопросу не относится.
Относится и напрямую. Ты сначала делаешь дженериками явы то, что вообще не требует ни дженериков, ни шаблонов, а потом говоришь «а на шаблонах так нельзя».
Поставь уж задачу. Вот я например могу предложить что-то такое, (по идее из «равенство длин массивов»), но по-практичнее:
template<unsigned int n> class Array
{
public:
const unsigned int depth=n;
...
Array<n+1>* push(double value) { return new Array<n+1>(value, this);}
private:
double values[n];
};
теперь предположим, что мы хотим сделать ArrayOfAnySize, который был бы тем же Array, но со статически неизвесным n, и при этом хотим избежать копипаста в него методов из Array
возможно ArrayOfAnySize сделать предком всех Array<n>, но лучше и этого избежать — идеально было бы вообще не трогать код Array<n>
теперь предположим, что мы хотим сделать ArrayOfAnySize, который был бы тем же Array, но со статически *постоянным, но неизвесным* n, и при этом хотим избежать копипаста в него методов из Array
Ты сначала делаешь дженериками явы то, что вообще не требует ни дженериков, ни шаблонов, а потом говоришь «а на шаблонах так нельзя».
Да, естественно. Спросили про разницу между шаблонами и дженериками. Если переписать данный пример буква в букву на C++, используя шаблоны - он не заработает. Разница видна.
Могу привести и обратные примеры - то, что делается на шаблонах, но не переписывается напрямую в дженерики. Например, частичная специализация.