К изменениям лучше готовиться заранее, поэтому предлагаю посмотреть на то, что войдет в стандарт C++20, а именно на концепции.
Статус концепций
Сейчас концепции имеют статус технической спецификации(TS: technical specification): документ их описывающий ISO/IEC TS 19217:2015. Такие документы нужны, чтобы перед принятием нововведений в стандарт языка, эти нововведения были опробованы и скорректированы сообществом С++. Компилятор gcc поддерживает техническую спецификацию концепций в экспериментальном режиме с 2015 года.
Стоит заметить, что концепции из технической спецификации и концепции из текущего черновика С++20 различаются, но не сильно. В статье рассматривается вариант технической спецификации.
Теория
Шаблоны классов и функций могут быть связаны с ограничениями. Ограничения накладывают требования на аргументы шаблона. Концепции это именованные наборы таких ограничений. Каждая концепция является булевой функцией(предикатом), проверяющей эти ограничения. Проверка производится на этапе компиляции при инстацировании шаблона связанного с концепцией или ограничением. Если такая проверка не проходит, то компилятор укажет какой аргумент шаблона провалил проверку какого ограничения.
Практика
Теперь когда понятны смысл и назначение концепций можно рассмотреть синтаксис. Определения концепций имеют две формы: переменной и функции. Нас будет интересовать форма переменной. Она очень похожа на определение обычной шаблонной переменной, но с ключевым словом concept.
template<typename T>
concept bool MyConcept = /* ... */;
Вместо комментария нужно написать constexpr выражение, которое приводится к bool. Это выражение и есть ограничение на аргумента шаблона. Что бы ограничить шаблон концепцией, нужно вместо typename(или class) использовать её название.
Например, для целых чисел:
// (На момент написания статьи подсветка синтаксиса не работала для
// ключевых слов связанных с концепциями)
#include <type_traits>
template<typename T> // концепция целых чисел
concept bool MyIntegral = std::is_integral<T>::value;
//template <typename T>
template<MyIntegral T>
bool compare (T a, T b) {
return a < b;
}
void foo () {
compare (123u, 321u); /// OK
compare (1.0, 2.0); /** ОШИБКА: нарушение ограничений концепции MyIntegral
(std::is_integral<double>::value = false)
*/
}
Можно ставить более сложные ограничения, используя требование-выражение(requires-expression). Требование-выражение умеет проверять правильность(well-formed) выражения, возвращаемое значение выражения, наличие типов. Синтаксис хорошо разобран тут.
#include <unordered_set>
#include <vector>
template<typename T>
concept bool MyComparable = requires (T a, T b) {
a < b; /// Проверяем, что такое выражение правильно
{ a < b } -> bool; /// Проверяем, что сравнение приводится к типу bool
};
template<MyComparable T>
bool compare (T a, T b) {
return a < b;
}
void foo () {
std::vector<int> vecA = {1, 2, 3}, vecB = {1, 2, 4};
std::unordered_set<int> setA = {1, 2, 3}, setB = {1, 2, 4};
compare (vecA, vecB); /// OK
compare (setA, setB); /** Нарушение ограничений концепции MyComparable
std::unordered_set не имеет
операции сравнения.
требование ( a < b ) не выполнено.
*/
}
Сортировка
Как же концепции помогут в написании сортировки? Сам алгоритм останется неизменным, но шаблон сортировки можно улучшить с помощью концепций. Рассмотрим такой пример:
#include <algorithm>
struct NonComparable {};
int main () {
std::vector<NonComparable> vector = {{}, {}, {}, {}, {}, {}, {}, {}};
std::sort (vector.begin(), vector.end()); // Ошибка
}
Ошибка заключается, в том что у структуры NonComparable нет операции сравнения. Представляете как будет выглядеть ошибка компилятора? Если нет, то загляните под спойлер.
[username@localhost concepts]$ g++ -std=c++17 main.cpp
In file included from /opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_algobase.h:71:0,
from /opt/rh/devtoolset-7/root/usr/include/c++/7/vector:60,
from main.cpp:1:
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/predefined_ops.h: In instantiation of ‘constexpr bool __gnu_cxx::__ops::_Iter_less_iter::operator()(_Iterator1, _Iterator2) const [with _Iterator1 = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >; _Iterator2 = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >]’:
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_algo.h:81:17: required from ‘void std::__move_median_to_first(_Iterator, _Iterator, _Iterator, _Iterator, _Compare) [with _Iterator = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >; _Compare = __gnu_cxx::__ops::_Iter_less_iter]’
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_algo.h:1921:34: required from ‘_RandomAccessIterator std::__unguarded_partition_pivot(_RandomAccessIterator, _RandomAccessIterator, _Compare) [with _RandomAccessIterator = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >; _Compare = __gnu_cxx::__ops::_Iter_less_iter]’
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_algo.h:1953:38: required from ‘void std::__introsort_loop(_RandomAccessIterator, _RandomAccessIterator, _Size, _Compare) [with _RandomAccessIterator = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >; _Size = long int; _Compare = __gnu_cxx::__ops::_Iter_less_iter]’
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_algo.h:1968:25: required from ‘void std::__sort(_RandomAccessIterator, _RandomAccessIterator, _Compare) [with _RandomAccessIterator = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >; _Compare = __gnu_cxx::__ops::_Iter_less_iter]’
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_algo.h:4836:18: required from ‘void std::sort(_RAIter, _RAIter) [with _RAIter = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >]’
main.cpp:6:44: required from here
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/predefined_ops.h:43:23: error: no match for ‘operator<’ (operand types are ‘NonComparable’ and ‘NonComparable’)
{ return *__it1 < *__it2; }
~~~~~~~^~~~~~~~
In file included from /opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_algobase.h:67:0,
from /opt/rh/devtoolset-7/root/usr/include/c++/7/vector:60,
from main.cpp:1:
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_iterator.h:888:5: note: candidate: template<class _IteratorL, class _IteratorR, class _Container> bool __gnu_cxx::operator<(const __gnu_cxx::__normal_iterator<_IteratorL, _Container>&, const __gnu_cxx::__normal_iterator<_IteratorR, _Container>&)
operator<(const __normal_iterator<_IteratorL, _Container>& __lhs,
^~~~~~~~
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_iterator.h:888:5: note: template argument deduction/substitution failed:
In file included from /opt/rh/devtoolset-7/root/usr/include/c++/7/bits/stl_algobase.h:71:0,
from /opt/rh/devtoolset-7/root/usr/include/c++/7/vector:60,
from main.cpp:1:
/opt/rh/devtoolset-7/root/usr/include/c++/7/bits/predefined_ops.h:43:23: note: ‘NonComparable’ is not derived from ‘const __gnu_cxx::__normal_iterator<_IteratorL, _Container>’
{ return *__it1 < *__it2; }
~~~~~~~^~~~~~~~
и т.д.
Такая маленькая ошибка в коде и такая большая у компилятора. Абыдно, да!?
Такие ошибки можно сократить с помощью концепций, для этого напишем враппер их использующий. Сортировка принимает итераторы, поэтому нужно написать концепцию Сортируемый итератор. Для такого итератора, нужно несколько концепций поменьше. Например, сравнимый объект(приведен выше), обмениваемый объект:
template<typename T>
concept bool MySwappable = requires (T a, T b) {
std::swap(a, b); // Можно обменивать
};
перемещаемый объект
template<typename T>
concept bool MyMovable = requires (T a) {
T (std::move(a)); // Можно конструировать перемещением
a = std::move(a); // Можно присваивать перемещением
};
итератор случайного доступа
template<typename T>
concept bool MyRAIterator = requires (T it) {
typename T::value_type; // Есть тип на который указывает итератор
it++; // Можно работать как с Random Access итератором
it--;
it += 2;
it -= 2;
it = it + 2;
it = it - 2;
{ *it } -> typename T::value_type; // Можно разыменовать
};
Когда все простые концепции готовы, можно определить составную концепцию Сортируемого итератора:
template<typename T>
concept bool MySortableIterator =
MyRAIterator<T> && // Итератор случайного доступа
MyMovable<typename T::value_type> && // Перемещаемый объект
MyComparable<typename T::value_type> && // Сравнимый объект
MySwappable<typename T::value_type>; // Обмениваемый объект
С его помощью пишется враппер:
template<MySortableIterator T>
void conceptualSort (T begin, T end) {
std::sort (begin, end);
}
Если вызывать "концептуальную" сортировку с несравниваемым объектом,
struct NonComparable {};
int main () {
std::vector<NonComparable> vector = {{}, {}, {}, {}, {}, {}, {}, {}};
conceptualSort (vector.begin(), vector.end()); // Ошибка
}
то ошибка компиляции будет занимает всего 16 строк:
[markgrin@localhost concepts]$ g++ -std=c++17 -fconcepts main.cpp
main.cpp: In function ‘int main()’:
main.cpp:49:49: error: cannot call function ‘void conceptualSort(T, T) [with T = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >]’
conceptualSort (vector.begin(), vector.end());
^
main.cpp:41:6: note: constraints not satisfied
void conceptualSort (T begin, T end) {
^~~~~~~~~~~~~~
main.cpp:36:14: note: within ‘template<class T> concept const bool MySortableIterator<T> [with T = __gnu_cxx::__normal_iterator<NonComparable*, std::vector<NonComparable> >]’
concept bool MySortableIterator = MyRAIterator<T> && MyMovable<typename T::value_type> &&
^~~~~~~~~~~~~~~~~~
main.cpp:12:14: note: within ‘template<class T> concept const bool MyComparable<T> [with T = NonComparable]’
concept bool MyComparable = requires (T a, T b) {
^~~~~~~~~~~~
main.cpp:12:14: note: with ‘NonComparable a’
main.cpp:12:14: note: with ‘NonComparable b’
main.cpp:12:14: note: the required expression ‘(a < b)’ would be ill-formed
Конечно, первые разы все равно не очень просто понять в чем ошибка, но после нескольких "концептуальных" ошибок они начинают читаться за несколько секунд.
Заключение
Конечно, сокращение длины ошибок не единственное преимущество нововведения. Шаблоны станут безопаснее благодаря ограничениям. Код станет более читаемым благодаря именнованным концепциям(самые часто используемые войдут в библиотеку). В целом С++ расширится в своей функциональной(шаблонной) части.
ratijas
Что ещё осталось в мире, чего нет в крестах?
А, ну да. В нем всё ещё нет нормальных дженериков — только шаблоны а-ля банальный текстовый препроцессор.
Hokum
А в чем преимущество дженериков перед шаблонами? На мой взгляд шаблоны куда более мощный инструмент.
Да и все что описано в данное статье не столь изящно, но решается обычными шаблонами. Без условно концепции позволят делать все это куда более понятно и короче, так что я «за».
ratijas
Мощный, да. Как топор. И такой же примитивный. define тоже мощный — вон какие чудеса из чистой сишки творит.
PS за минус сорян, случайно вышло :c
Hokum
Мне на самом деле интересно в чем вы видите преимущества дженериков, в каких моментах в C++ они стали бы более удобным инструментом, не холивара ради? Ну и прежде хотелось бы понять в чем у них разница. Так как в моем понимании это в общем-то разные реализации одной концепции — обобщенного программирования.
И дженериками я столкнулся только однажды — при изучении Scala и ловил себя на мысле, что мне их мало. Основной неожиданностью для меня было, что дженерики в Scala не позволяют вызвать произвольный методы, что доступно при работе с шаблонами в C++. Задачи решить можно было, но это требовало задействование других механизмов языка. А если при использовании дженериков воспользоваться указанием, что он должен быть наследником какого-то класса, то тогда уже можно и без них обойтись.
Из основных плюсов — их можно экспортировать, но за счет этого они получаются очень ограниченными.
Макросы в C/C++ позволяют делать довольно хитрые и удобные вещи, только они все таки в стороне от шаблонов и дженериков. Вот они все таки ближе к определению текстовый процессор, чем шаблоны.
PsyHaSTe
Хотя бы в том, что дженерик всегда работает со своими типами корректно. Если вдруг вы подставили недопустимый тип Т, например вот так:
То у вас будет понятное сообщение об ошибке:
А не мешанина из кишков темплейта.
Не говоря про возможность собирать дженерик из типов динамически в рантайме.
Про «лучшесть» или «хужесть» дженериков в целом судить не берусь, но лично мне ими пользоваться удобнее.
Free_ze
PsyHaSTe
Ну да. Например, генерация имплементации интерфейса, который прилетает по сети. Ну и другие случаи, например см. статью
Free_ze
Печально, когда ничего не требуется генерить в рантайме. То есть почти всегда.
Это больше про рефлекшн вообщеmayorovp
Ну, в языках использующих JIT в конечном счете все равно код будет в рантайме генерироваться. Так что ничего печального в раскрытии обобщенных классов в рантайме нет.
Если же некоторый язык компилируется сразу в машинный код — не вижу принципиальных проблем собирать все сразу при компиляции.
Free_ze
mayorovp
Не могут. Компилятор C# не имеет генерировать нативный код в принципе, а в C++/CLI генериком может быть только управляемый класс…
Free_ze
Обратного не утверждалось. Однако как рантаймовые дженерики, так и компайлтаймовые шаблоны там есть, хотя вы их разделили лишь по признаку наличия JIT.
mayorovp
Нет, я их разделил по свойствам.
template в C++/CLI:
generic при этом:
Free_ze
mayorovp
Та часть языка C++/CLI в которой разрешены generic — компилируется в байт-код, а не в машинный.
Free_ze
Это не имеет значения, если рассматривать их как отдельные инструменты. Ничто не мешает сделать шаблоны поверх JIT-компилируемого кода.
А вот взаимодействие можно было бы построить через рефлекшн (опционально сохранять метаинформацию о шаблонах и позволять им работать как дженерики в рантайме).
mayorovp
Более того, в языке они есть.
Нельзя так просто взять и сделать из произвольного шаблона дженерик.
Free_ze
0xd34df00d
Вот ровно поэтому и непонятен профит дженериков в С++.
PsyHaSTe
В моем случае почему-то это почти всегда. Типичный пример — построение запроса, основанного на пользовательском вводе.
Рефлекшн без информации о дженериках ничего не смог бы сделать. Например я бы не смог реализовать такой метод:
Если бы у меня собственно не было MakeGenericMethod и interfaceMethod.ReturnType.GenericTypeArguments
0xd34df00d
Можно пример?
PsyHaSTe
Это не моя цитата.
0xd34df00d
Тьфу, Ctrl+C не нажался. Спасибо, обновил.
PsyHaSTe
Ответил ниже (про лямбду)
0xd34df00d
Интерфейс прилетает по сети? А как вы тогда в вызывающем коде статически типизированно собираетесь с ним работать?
PsyHaSTe
Имеется ввиду, что он деконструируется на некоторые известные части. Типичный пример, построить лямбду
x=>x.Name == "Alex" && x.Gender = Gender.M
на основании пользовательского текстового ввода.0xd34df00d
Так это ж каноничное expression templates.
PsyHaSTe
Можно пример?
0xd34df00d
Да, например. Надеюсь, я правильно понял ваш контекст, и аналогия верная.
PsyHaSTe
Тяжело читать плюсы. Насколько я понял, мы от пользователя получаем только параметры запроса, который выполняем. Я имел ввиду скорее генерацию самого запроса с нуля. У нас на проекте, например, была фильтрация, которая могла иметь произвольную вложенность. Всевозможные фильтры, объединяемые через И-ИЛИ. Весь фильтр целиком по сути имел свойства (де)сериализации из/в пользовательский ввод и умение выполняться в БД. Примера из этой системы не покажу, но например я класс, который генерирует объекты сравнения. Например, пишем так:
На выходе имеем объект типа
IComparer
с методомcompare
, который реализован какМожно ли тут сгенерировать нужный тип на этапе компиляции? Безусловно. Но только потому, что
ZComparer<Test>.New(t => t.A).Add(t => t.B).Add(t => t.C).Add(t => t.D);
мы знаем на этапе компиляции. Если же у нас немного больше динамикиТо сгенерировать нужный фильтр мы можем только в рантайме.
Чуть подробнее в исходниках и в тестах.
Код старый, так что возражения по оформлению и неоптимальности не принимаются, сам уже знаю :)
0xd34df00d
Этого тоже вполне можно добиться на плюсах.
PsyHaSTe
Возможно. Хотя я и не представляю, как именно, и ни разу не видел на практике. Просветите?
0xd34df00d
Каждый следующий
Add
принимает лямбду/указатель на поле/указатель на функцию и добавляет соответствующийstd::function
в список, по которому потом пробегает методCompareTo
в цикле. Можно это всё написать в виде кода, в принципе, но, надеюсь, и так примерная мысль понятна.PsyHaSTe
Но это упрощенный пример. Если довести до предела, то пользователь вводит в input корретный C++ код фильтрации сущностей, который транслируется в запрос в БД. Может это несколько надуманный пример, но он не сильно отличается от того, что я на реальном проекте видел.
0xd34df00d
А сущности-то определены на этапе компиляции?
PsyHaSTe
Ну, известно, что на вход фильтра подается объект типа T произвольной вложенности, а на выходе bool.
PsyHaSTe
Основное преимущество дженерика — он вещь в себе. Если вы написали дженерик и он компилируется — то скорее всего он написан правильно. Поэтому он будет корректно работать с любыми типами, которые подходят под ограничения (если они есть).
Темплейт же может таить в себе что угодно, и до момента инстанцирования сказать, рабочий ли он — невозможно.
Разные инструменты решают разные задачи, но в чем-то у них есть область пересечения. Дженерики позволяют решить задачу для произвольного типа Т, темплейты же позволяют писать факториалы времени выполнения, но никто не гарантирует того, что для любого возможного T реализация будет верна. Более того, это в общем случае неверно, ведь вместо типа может быть подставлено и число, и что угодно.
Free_ze
PsyHaSTe
Давайте так: дженерик без условий where на типах будет работать с ЛЮБЫМИ типами, всегда.
Могут ли такие гарантии быть у темплейтов?
Free_ze
Если говорить про .NET, то отсутствие
where
еще не означает отсутствие ограничений. Там все типы наследуют некую общую функциональность типаObject
, кроме того вы не сможете создать объект конструктором по умолчанию и прочие такие вещи.Да, грустно, что автоматически соответствие алгоритма ассертам не проверяется до момента инстанцирования. Но я не исключаю возможности таких проверок, когда появятся концепты. И что тогда?
PsyHaSTe
Ладно, перефразирую: чтобы понять, как будет работать дженерик с типом
Т
достаточно посмотреть на сигнатуру метода. Если же ошибка в темплейте, единственный способ посмотреть — залезть в него и посмотреть, как он используется внутри. Это совершенно разная сложность.Шарп/раст — не важно. Можно тип T никак не использовать, функционал "object" тут никак не задействован:
в интерфейсе я вижу:
я не знаю, что внутри метода, я вижу только сигнатуру, и точно знаю, что он отработает с любым типом.
Free_ze
Вы, как юзер, не знаете о том, используется он внутри или нет.
mayorovp
Насколько я понял, концепты указывают необходимое, но не достаточное условие работоспособности шаблона.
Free_ze
Достаточность — это разве не ограничение «сверху»? В этом случае по тем же принципам работают и дженерики.
mayorovp
В C# — нет, не по тем же. Нельзя обращаться к членам типа-параметра невыводимым из ограничений
Вот такой код не скомпилируется, будет ошибка что у типа A не виден метод Baz:
В то же время аналогичный код на шаблонах в C++ будет компилироваться без ошибок до тех пор пока шаблоном не попытаются воспользоваться.
Free_ze
Тогда наличие метода — это необходимое условие. Чем это будет хуже, чем:
?mayorovp
Вот смотрите, я пишу:
И этот код компилируется без ошибок. Вот в этом "без ошибок" и проблема: в реализации серьезная ошибка, но компилятор ее не видит.
Free_ze
Здесь нет ошибки. Это статический полиморфизм — очень мощная возможность C++.
В то же время, если мы хотим максимум безопасности, нам ничто не мешает задекларировать
Baz
в интерфейс. Тогда ситуация станет точно такой же, как у дженериков.mayorovp
Это называется не "статический полиморфизм", а "утиная типизация".
Нам мешает тот факт, что мы об этом забыли, а компилятор не напомнил. И теперь библиотека будет в нерабочем состоянии до первого багрепорта от возмущенных пользователей.
Free_ze
Что мешает вам здесь говорить об этом, как о статическом полиморфизме?
mayorovp
То, что это не статический полиморфизм.
Статический полиморфизм — это сама возможность инстанцировать шаблон конкретным типом без виртуальных вызовов в рантайме.
А доступ к методу Baz когда нигде не описано что такой метод у типа A есть — это именно что утиная типизация.
Free_ze
mayorovp
Где я это писал? Вы вообще отличаете утверждения "вызов a.Baz() — это не статический полиморфизм" и "у нас нет статического полиморфизма"?
Free_ze
Хорошо, тогда вы писали:
А как же сам шаблон? Гарантия того, что этот метод есть — это ошибка инстанцирования в противном случае.mayorovp
Вот и приходим к тому с чего начали — чтобы увидеть реальный контракт шаблона надо изучить всю его реализацию, а концепты указывают необходимое, но не достаточное условие.
Free_ze
Если весь контракт будет в концептах и других ограничениях, то это не потребуется.
mayorovp
… но вы никогда не сможете быть уверены в этом. Или вы живете в мире, где программисты никогда не ошибаются?
Free_ze
Возможно, в каком-нибудь C++23 сделают проверку на соответствие шаблона концепту. Хотелось бы верить) Но эти ошибки просто ловить и легко фиксить, так что не думаю, что это большая проблема.
mayorovp
Это не большая проблема только до тех пор пока обе части кода — объявление шаблона и его использование — пишутся одним разработчиком.
Free_ze
Повсеместное использование статических анализаторов — это реальность сегодняшнего дня. Подозреваю, что автоматизировать такую проверку будет не самой сложной задачей для них.
Antervis
назовите три примера ошибок в коде повсеместно используемых шаблонных библиотек
0xd34df00d
Не совсем то, но весьма распространённое отсутствие SFINAE friendliness, например.
0xd34df00d
Одно другому не мешает. Утиность типизации вообще можно рассматривать как деталь реализации.
PsyHaSTe
А мне ине не нужно знать.
В двух словах:
Free_ze
Какие вы видите оганичения возможности вычислить соответствие шаблона концепту на этапе компиляции? Аналогично тому, как компилятор контролирует дженерики.
PsyHaSTe
При чем тут дизайн языка? В любом языке зная тип
T
можно создать массивT[]
. Тут не используется ни одного метода или свойстваObject
.Насколько я знаю, темплейты вообще не компилируются, а инстанцируются по месту использования. И любой синтаксически верный код скомпилируется, даже если семантически там полный бред.
Free_ze
Хорошо, конкретизирую: на этапе статического анализа.
PsyHaSTe
Ну ок, допустим. Я не согласен, но допустим. Какая нам разница? Любой объект любого типа можно подставить вместо T? Можно. Что еще нужно?
Никакой статический анализ не скажет, что метода
fooasgjknasgh1htg781gh73
не существует ни у одного объекта в проекте, и этот темплейт обречен провалиться.Free_ze
where T: IFoo
и он точно так же повалится лишь тогда, когда выяснится, что пользовательский тип не имплементитIFoo
.Я о другом говорил: компилятор должен проверить, чтобы любой тип, подходящий под концепт, был валидным и для шаблона его использующего.
То есть нужен аналог сишарпового
where
, только с более широкими возможностями ограничений.mayorovp
Ничего подобного. Дженерик повалится тогда, когда окажется что в интерфейсе IFoo нет метода fooasgjknasgh1htg781gh73.
PsyHaSTe
В том, что будет ошибка "не реализован интерфейс IFoo", а не "не найден метод
fooasgjknasgh1htg781gh73
" Преимущество очевидно тогда, когда у вас темплейт вызывает другие темплейт-функции, и падает где-то в глубине нижних уровней из-за какого-то непонятного несоответствия.Free_ze
С этим я согласен отчасти, ибо грамотные статические проверки и это могут покрыть и дать более вразумительный текст ошибки. Да, это сложно писать. Но концепты это исправят. И что тогда?)
PsyHaSTe
Тогда станет чуть полегче. Но все равно гарантировать корректную работу для всех типов шаблон не может, просто потому, что он не является самостоятельной сущностью, а вычисляется при подстановке.
Antervis
Во-первых, существуют проверки в виде static_assert/enable_if/SFINAE — даже в с++14 можно проверить соответствие типа требованиям шаблона.
Во-вторых, функционал шаблона может зависеть от свойств типа-параметра. vector от unique_ptr не поддерживает копирование, т.к. unique_ptr не поддерживает копирование
В третьих, концепты и позволят накладывать все необходимые ограничения на тип. Ошибка возникнет в момент инстанцирования шаблона, и укажет именно на то требование к типу, которое не выполняется
как раз то о чем я говорил:
На enable_if сложнее, но тоже реализуемо
mayorovp
Так в том-то и проблема, что все требования к типу выполняются, но ошибка компиляции все равно возникнет. Потому что ошибка — в самом шаблоне.
Antervis
во-первых, возможно, этот шаблон собираются использовать только с типами, для которых определен T::baz(), отсутствующий в IFoo. Иначе метод можно реализовать и без шаблона.
во-вторых, посмотрите на это вот с какой стороны: за корректность библиотеки и проверку входных параметров/данных отвечает библиотека.
mayorovp
Отвечает-то библиотека, вот только сыпятся ошибка компиляции не на авторов библиотеки, а на ее пользователей почему-то. Это и неправильно.
0xd34df00d
Или потому, что
Кстати, в этом коде, возможно, UB. Я недостаточно language lawyer, чтобы об этом сказать.
Темплейты тайпчекаются в момент мономорфизации, это верно.
Hokum
Спасибо, я правильно понимаю, что это пример из Rust?
Я как-то не задумывался, что дженерики работают и в рантайме. Тогда для некоторых задач, они становятся удобнее шаблонов, в частности сохранения отношения наследования, или инвертирование его.
PsyHaSTe
Ну, в расте райнтаймовой поддержки на данный момент нет, т.к. раст все же обычно имеет зависимости на уровне исходников.
А вот в том же шарпе да, есть полная поддержка со стороны среды. И например
List<int>
иList<string>
там разные типы, и попытка засунуть одно в другое приведет к ошибке времени выполнения, даже если на этапе компиляции там все было ок.mayorovp
Что вы понимаете под «сохранением отношения наследования»?
Hokum
Пусть есть три класса: шаблонный класс Templ, класс Base и Derived, где является наследником B, то Templ будет являться наследником Templ. Т.е. Templ можно будет использовать везде, где ожидается Templ.
Такое поведение не может быть общим, но с дженериками это можно реализовать.
mayorovp
У вас угловые скобки потерялись...
Нет, в общем случае
Templ<Derived>
нельзя использовать в качествеTempl<Base>
, как и наоборот. Если бы было можно — это привело бы к многочисленным нарушениям LSP и просто ошибкам в рантайме.То, что вы просите, в языке C# работает только для обобщенных интерфейсов, которые отмечены как ковариантные (это накладывает на интерфейс дополнительные требования на этапе компиляции).
В Java ковариантных интерфейсов нет, но зато можно писать конструкции вида
Templ<? extends Base>
(такой класс "теряет" свои нековариантные методы) — и к вот такому типу действительно можно неявно привестиTemp<Derived>
.PS вот тут я совсем недавно пример приводил: https://habrahabr.ru/post/348286/#comment_10654282
Hokum
Про угловые скобки — спасибо, не заметил.
Да, я именно про ковариантность говорил. Это без условно накладывает определенные ограничения на интерфейс класса и не может быть обобщено на все шаблоны. Но если говорить о немутабельных коллекциях, то это несколько упрощает жизнь. В этом случае коллекция будет приводится к наиболее общему типу. Это могло бы выглядеть как-нибудь так:
Но больше пользы будет при работе в C++ со smart_ptr, в части возвращения значений.
Например такой вариант сейчас валиден:
То такой вариант уже не валиден:
Иногда такое удобно. Но для этого еще потребуется такая перегрузка:
mayorovp
А чем вас решения C# и Java не устраивают?
Hokum
Устраивают, наверно, я просто не пишу на этих языках. Это больше к мысли, чего не хватает шаблонам C++.
Antervis
Из вашего примера не понятно, какая именно из четырех ваших ошибок связана с недопониманием. Убедитесь, что Interface определен и имеет виртуальный деструктор.
Hokum
А причем тут недопонимание? И виртуальный деструктор тут совсем не причем. Речь идет о ковариантности, а не о том будет ли вызван деструктор наследников или нет.
Просто иногда удобно, когда в базовом классе метод возвращает, например, указатель на тип Q, а в классе наследнике метод перегружен и возвращает указатель на тип W — наследника Q. Таким образом когда работаешь с классом наследником, в той части программы, которая про него знает, можно работать с указателем на W, а общая часть, которая не знает о наследнике работает только с указателем на Q.
Но если речь заходит о возврате умного указателя, то код не скопилируется, так как нарушается ковариантность.
Antervis
я понял о чем вы. Чтобы с++ начал поддерживать подобное, в стандарте должно появиться понятие «ковариантности» как более жесткое отношение наследования, когда любой объект Derived класса является (бинарно) корректным экземпляром Base класса. А вот теперь вопрос — как это проверить? Чтобы Derived был ковариантен Base он должен как минимум не иметь дополнительных полей (должны быть одного размера) и переопределенного невиртуального деструктора. Указатели/ссылки/умные указатели по идее ковариантны, но с точки зрения корректности опять же всплывает требование на виртуальный ~Base() или не переопределенный ~Derived(). А как сформулировать правило в общем виде?
Hokum
Для указатели и ссылок это уже работает. В принципе можно было бы добавить оператор «повышения» класса, т.е. получения из наследника экземпляр родителя. Но это, конечно, не общий случай. Для более общего все должно стать «указателем». Тогда и рефлексию можно будет легко добавить и ко/ин-вариантность. Но это кардинально изменило бы язык. Так что как ввести это в C++ оставив его при этом C++ — затрудняюсь представить.
Antervis
Например вот такой случай:
DerivedContainer ковариантен BaseContainer, хотя не является (умным) указателем/ссылкой и даже его не наследует.
Нельзя «всё сделать указателем». Это 0-cost abstraction язык.
Hokum
В целом, да. Я про это и написал, что как такое ввести и оставив C++ в том понимании как он есть — я не представляю.
mayorovp
Вы немного путаетесь с терминологией. Ковариантность — это свойство параметризованного типа (шаблона или дженерика) в отношении одного из своих параметров, но никак не отношение двух классов.
Что же до вашего случая — тут как раз все очень просто:
Hokum
Спасибо за уточнение, про теминологию. Но в разрезе C++ это почти соответствует такому шаблонному коду:
И вроде они могли бы быть коварианты, но увы.
Можно даже было бы написать обощенный оператор приведения.
Но к сожалению наличие оператора приведения типа никак не поможет перегружать виртуальные метод с возвратом типа который отличается от того, который был указан при объявлении виртуального метода в базовом классе.
Это потребует хранение дополнительной информации о необходимых трансформациях возвращаемого значения или генерации некоторой хитрой обертки над таким методом, но все это будет приводить к путанице при последующем наследовании и перегрузке.
mayorovp
Не беспокойтесь, в C# и Java так тоже нельзя делать :-)
mayorovp
Теперь о недостатках вашего решения.
Вложенный контейнер работать не будет:
Cont<Derived1, Cont<Derived2, Derived3>>
нельзя привести кCont<Base1, Cont<Base2, Base3>>
;Hokum
Да и просто неявное приведение указателей не будет работать. Причем с п.1 еще можно попробовать побороться добавив еще несколько шаблонов, может быть, даже получится решить для общего случая, но боюсь, что в шаблонах можно будет утонуть. :) А вот с неявным приведением указателей сделать точно ничего не удастся.
iyemelyanov
Действительно на Rust-е это выглядит весьма естественно и вывод компилятора на порядок читаемей!
Antervis
вот как раз таки концепты позволяют решить в т.ч. и эту проблему.
BalinTomsk
Pабота с множествами.
auto ar[] = {1,4,7,9};
if( 4 in ar )
{
}
zorge_van_daar
Эээ, как бы
Free_ze
BalinTomsk
Вы отличаете языковую поддержку (о чем и сказано в статье) от кривых костылей библиотек?
К тому же я написал про array (если бы вы смогли понять что я написал в коде), а не неудачные примеры в STL.
Free_ze
Что вы понимаете под языковой поддержкой? Оператор «in»?
Нет, вы писали про множества) Если там будетstd::array
, то это мало что изменит.ЗЫ Стандарт С++ включает стандартную библиотеку, так что смело можете считать, что любые контейнеры и алгоритмы из нее — это и есть языковая поддержка.
Пожалуй, я бы еще boost сюда включил, но это уже будет не так честно.
Profi_GMan
Ээээх… Печаль, что всё это станет доступно с 20 стандартом…
Antervis
всё-таки «concept» практически всегда переводят как «концепт» а не «концепция».
Очень много реального сахара не описано (подробнее можно глянуть здесь). Пример:
Полная запись позволяет накладывать ограничения на функцию без объявления нового концепта (bb enable_if). Или, например:
В общем, всё самое интересное в статье опущено