Что именно нового в концепциях c ++? В моем понимании они функционально равны использованию static_assert
, но «хорошим» способом, означающим, что ошибки компилятора будут более читабельными (как сказал Бьярн Страуступ, вы не получите 10 страниц или ошибок, а только одну).
По сути, правда ли, что все, что вы можете сделать с концепциями, которых вы также можете достичь, используя static_assert
?
Есть что-то, чего мне не хватает?
По сравнению с static_assert
s, концепции более мощные, потому что:
static_asserts
std::enable_if
(это невозможно только с static_asserts
)static_asserts
в каждой функции)Это может облегчить миры:
и быть строительным блоком для интересных парадигм.
Понятия выражают «классы» (не в термине C ++, а скорее как «группу») типов, которые удовлетворяют определенным требованиям. В качестве примера вы можете увидеть, что Swappable
Понятие выражают набор типов, которые:
std::swap
И вы можете легко увидеть, что, например, std::string
, std::vector
, std::deque
, int
и т.д. … удовлетворяют этому требованию и поэтому могут использоваться взаимозаменяемо в такой функции, как:
template<typename Swappable>
void func(const Swappable& a, const Swappable& b) {
std::swap(a, b);
}
Концепции всегда существовал в C ++, Фактическая функция, которая будет добавлена в (возможно, в ближайшем будущем), просто позволит вам выразить и применить их на языке.
Что касается лучшей диагностики, то сейчас нам просто нужно довериться комитету. Но на выходе они «гарантируют»
error: no matching function for call to 'sort(list<int>&)'
sort(l);
^
note: template constraints not satisfied because
note: `T' is not a/an `Sortable' type [with T = list<int>] since
note: `declval<T>()[n]' is not valid syntax
очень перспективно
Это правда, что вы можете добиться аналогичного результата, используя static_assert
с, но это потребует другого static_assert
с каждой функцией, и это может очень быстро утомить.
В качестве примера представьте, что вы должны обеспечить соблюдение требований, предъявляемых Container
концепция в 2-х функциях с параметром шаблона; вам нужно будет повторить их в обеих функциях:
template<typename C>
void func_a(...) {
static_assert(...);
static_assert(...);
// ...
}
template<typename C>
void func_b(...) {
static_assert(...);
static_assert(...);
// ...
}
В противном случае вы потеряли бы способность различать, какое требование не было выполнено.
Вместо этого вы можете просто определить концепцию и реализовать ее, просто написав:
template<Container C>
void func_a(...);
template<Container C>
void func_b(...);
Еще одна замечательная особенность, которая представлена, — это возможность перегрузки шаблонных функций на шаблонные ограничения. Да, это также возможно с std::enable_if
, но мы все знаем, как уродливо это может стать.
В качестве примера вы могли бы иметь функцию, которая работает на Container
и перегрузите его версией, которая лучше работает с SequenceContainer
s:
template<Container C>
int func(C& c);
template<SequenceContainer C>
int func(C& c);
Альтернатива, без понятий, была бы такой:
template<typename T>
std::enable_if<
Container<T>::value,
int
> func(T& c);
template<typename T>
std::enable_if<
SequenceContainer<T>::value,
int
> func(T& c);
Определенно уродливее и, возможно, более подвержен ошибкам.
Как вы видели в приведенных выше примерах, синтаксис определенно чище и более понятен в концепциях. Это может уменьшить объем кода, требуемого для выражения ограничений, и может улучшить читаемость.
Как было показано ранее, вы можете достичь приемлемого уровня с помощью чего-то вроде:
static_assert(Concept<T>::value);
но в этот момент вы потеряете большую диагностику различных static_assert
, С концепциями вам не нужен этот компромисс.
И, наконец, концепции имеют интересные сходства с другими функциональными парадигмами, такими как классы типов в Haskell. Например, они могут быть использованы для определения статические интерфейсы.
Например, давайте рассмотрим классический подход к (печально известному) интерфейсу игрового объекта:
struct Object {
// …
virtual update() = 0;
virtual draw() = 0;
virtual ~Object();
};
Затем, если у вас есть полиморфный std::vector
Из производных объектов вы можете сделать:
for (auto& o : objects) {
o.update();
o.draw();
}
Отлично, но если вы не хотите использовать множественные системы наследования или системы, основанные на компонентах, вы в значительной степени застряли с одним возможным интерфейсом на класс.
Но если вы действительно хотите статический полиморфизм (полиморфизм, который не тот в конце концов, вы можете определить Object
концепция, которая требует update
а также draw
функции-члены (и, возможно, другие).
В этот момент вы можете просто создать бесплатную функцию:
template<Object O>
void process(O& o) {
o.update();
o.draw();
}
И после этого вы можете определить другой интерфейс для ваших игровых объектов с другими требованиями. Прелесть этого подхода в том, что вы можете разрабатывать столько интерфейсов, сколько захотите, без
И все они проверяются и применяются во время компиляции.
Это просто глупый пример (и очень упрощенный), но концепции действительно открывают совершенно новый мир для шаблонов в C ++.
Если вы хотите больше информации, вы можете прочитайте эту хорошую статью на C ++ понятия против классов типа Haskell.
Других решений пока нет …