(для программистов)
Бьярн Струструп выпустил статью под названием
"Concept-Based Generic Programming in C++", о том, что такое концепты (нововведение в C++ последних лет) и как они помогают писать код с темплейтами еще лучше, чем раньше. Если в двух словах, концепты это способ формулировать требования к типам (например, "тип T обязан быть численным" или "обязан поддерживать операцию < и возвращать bool"), чтобы можно было эти требования включать в темплейты.
Струструп наглядно показывает на хорошо подобранных примерах, зачем это нужно и как помогает. После прочтения я уяснил для себя эту часть современного C++, которую раньше не понимал. Но вместе с тем кажется, что это только усиливает те тенденции в C++, которые мне не нравятся.
Кажется, что для Струструпа идеальное использование C++ выглядит так. Рядовой программист использует классы, функции итд., которые написали для него некие волшебники за кулисами. Для него все вылизано и сделано максимально простым: он объявляет переменные каких-то типов, делает с ними операции, которые удобно записываются как + или <<, и так далее. Если он что-то написал неправильно, то в 90% случаев это даже не скомпилируется, а если скомпилируется, то кинет исключение, но ему для этого ничего не нужно особенно делать. При этом волшебники за кулисами, чтобы все это сделать, написали тонны крайне нечитаемого и сложного кода, который делает головокружительную эквилибристику, жонглирует мета-программированием, использует всякие подспорные std::штуки и "двойные" ссылки и ключевое слово "requires", которые рядовой программист в обычной жизни вообще никогда не видит. Почему-то оба эти языка, тот, на котором пишет программист и волшебник за кулисами, называются C++.
С моей точки зрения это ужасно, с точки зрения Струструпа - красиво и правильно. Более того - неизбежно вытекает из твердых принципов, которые он повторяет в начале статьи (generality, uncompromised efficiency, statically type-safe interfaces - совместимость тут даже не упоминается, как само собой разумеющееся, видимо).
Например, он дает пример класса Span, который заключает в себе идею контейнера с прямым доступом по индексу, проверяющего, что индекс не выходит за пределы. Span можно инициализировать обычным массивом с размером, известным во время компиляции, или вектором с размером, доступным по size(), и он будет работать одинаково просто и удобно, и кидать исключение в случае слишком большого индекса. Если мы а) несогласны вносить проверку границ в сам язык (uncompromised efficiency), но при этом хотим дать к ней удобный доступ всем, кто хочет и не ограничиться одними массивами или векторами, а вообще для всего (generality), и при этом для разных видов Span автоматически компилировать разный код для доступа к реальным данным и проверки их длины (statically type-safe interfaces), то мы приходим к необходимости наворотить кучу сложной магии за кулисами, чтобы можно было написать:
double arr[10];
Span s(arr);
s[20] // throws
При этом Струструп отдельно с гордостью поясняет, что, казалось бы, надо писать Span<double> s(arr), потому что темплейт-классу Span необходимо знать тип элемента в контейнере, но пользуясь отдельным специальным магическим заворотом, можно сделать так, что эта информация выцепляется из arr статически. По-моему это только все ухудшает, потому что не понимаешь даже, что используешь темплейтный класс. По Струструпу, это правильно, мне и не нужно это понимать, мне надо пользоваться API, которые наваяли волшебники, и не отсвечивать.
Внутренности Span частично выглядят так:
template<class T>
class Span {
T* p;
unsigned n;
Span(std::ranges::continuous_range auto& s) : p{data(s)}, n{size(s)} {}
}
T.e. Span хранит указатель на начало данных, правильного типа, и их размер; но откуда он их берет? Для контейнера s он вызывает data(s) и size(s) - откуда они берутся?
data() и size() - темплейтные функции, которые по-разному написаны для массива и для вектора (итп.), сидят в std::range - то, что для них не указан std::range это еще один вид магии, так называемый ADL, крайне неприятный. А std::ranges::continuous_range это как раз и есть концепт, который говорит в своем определении, что тип обязан предоставить data() и size() - "предоставить" не как в ООП, не как методы в классе, а просто чтобы были, чтобы data(s) и size(s) компилировалось и давало правильный тип.
В общем, концепты - это мощный механизм для того, чтобы еще легче и круче было вносить в язык много фигни, которая происходит за кулисами, и которую рядовой программист по идее не должен даже замечать. Если вам интересно, почитайте, статья хорошо написана.
Отдельный интерес представляет седьмая глава, где Струструп рассказывает немного о том, как не только концепты, но и темплейты вообще появились в языке, как развивались и почему они выглядят так, а не иначе. Вот в этой цитате отчетливо ощущается раздражение дизайнера языка. Он говорит о том, что вообще хотел обойтись без ключевого слова "template":
I chose the < … > notation for type parameterization following some use in theory. Initially, I did not use the prefix template keyword: < … > was a suffix to the name they parameterized. However, people strongly insisted on having a prefix keyword to make templates stand out. That is typical, initially people ask for a LOUD syntax for novel constructs because they are seen as difficult or even dangerous. Later, the same people complain about verbosity.