В этой статье мы поговорим о новом предложенном расширении языка С++ — метаклассах. Герб Саттер с коллегами работал над этим предложением около 2 лет и, наконец, этим летом представил его общественности.
Итак, что же такое «метакласс» с точки зрения Герба Саттера? Давайте вспомним наш С++ — самый прекрасный в мире язык программирования, в котором, однако, веками десятилетиями существуют примерно одни и те же сущности: переменные, функции, классы. Добавление чего-то фундаментально нового (вроде enum classes) занимает очень много времени и рассчитывать дождаться включения чего-то нужного вам здесь и сейчас в стандарт — не приходится. А ведь кое-чего и правда не хватает. Например, у нас всё ещё нет (да, наверное, и не будет) интерфейсов как таковых (приходится эмулировать их абстрактными классами с чисто виртуальными методами). Нет properties в полном их понимании, нет даже value-типов (чего-то такого, что можно было бы определить как набор переменных простых типов и сразу использовать во всяких там контейнерах/сортировках/словарях без определения для них разных там операций сравнения, копирования и хеширования). Да и вообще постоянно чего-то кому-то не хватает. Разработчикам Qt вот не хватает метаданных и кодогенерации, что заставляет их использовать moc. Разработчикам C++/CLI и C++/CX не хватило способов взаимодействия со сборщиком мусора и своими системами типов. Ну и т.д.
А давайте на секунду представим, что мы сами можем вводить в язык новые сущности. Ну или пусть не прямо «сущности», а правила проверки и модификации классов.
Как это всё будет работать. Герб предлагает ввести понятие «метакласса», как набора правил и кода, которые будут выполняться на этапе компиляции и на основе которых компилятор будет проверять классы в коде и/или создавать новые классы на основе вышеупомянутых правил.
Например, нам хочется иметь в языке классический интерфейс. Что такое «интерфейс»? Например, стандарт языка С# отвечает на этот вопрос на 18 страницах. И с этим есть целый ряд проблем:
- Никто их не читает
- Компилятор совершенно не гарантированно реализует именно то, что написано в тех 18 страницах текста
- У нас нет возможности проверить соответствие работы компилятора и текста на английском языке
- Для С++ пришлось бы написать такую же спецификацию и её реализацию в компиляторах (а зная С++ — так ещё и намного более сложную). А дальше см. пункты 1, 2 и 3.
Но, давайте скажем простыми словами, что такое «интерфейс» — это такой именованный набор публичных чисто-виртуальных методов, к которому в то же время не привязаны никакие приватные методы или члены данных. Всё! Да, может я сейчас упустил какую-то мелкую деталь из тех 18 страниц спецификации, но для 99.99% практического кода этого определения хватит. И вот для возможности описания в коде подобных определений и придуманы метаклассы.
Синтаксис ещё на этапе обсуждения, но вот примерно как может быть реализован метакласс «интерфейс»:
$class interface {
constexpr
{
compiler.require($interface.variables().empty(),
"Никаких данных-членов в интерфейсах!");
for (auto f : $interface.functions())
{
compiler.require(!f.is_copy() && !f.is_move(),
"Интерфейсы нельзя копировать или перемещать; используйте"
"virtual clone() вместо этого");
if (!f.has_access())
f.make_public(); // сделать все методы публичными!
compiler.require(f.is_public(), // проверить, что удалось
"interface functions must be public");
f.make_pure_virtual(); // сделать метод чисто виртуальным
}
}
// наш интерфейс в терминах С++ будет просто базовым классом,
// а значит ему нужен виртуальный деструктор
virtual ~interface() noexcept { }
};
Код интуитивно понятен — мы объявляем метакласс interface, в котором на этапе компиляции кода (блок constexpr) будут проведены определённые проверки и модификации конечного класса, который будет претендовать на то, чтобы считаться интерфейсом.
Применять это дело теперь можно вот так:
interface Shape
{
int area() const;
void scale_by(double factor);
};
Правда, очень похоже на C# или Java? При компиляции компилятор применит к Shape метакласс interface, что на выходе даст нам класс:
class Shape
{
public:
virtual int area() const =0;
virtual void scale_by(double factor) =0;
virtual ~Shape() noexcept { };
};
Плюс сгенерирует ошибку компиляции при попытке добавления данных-членов.
При этом обратите внимания, в полученном таким образом классе Shape нет больше никакой «мета-магии». Это просто класс, ровно такой же, как если бы он был написан руками — можно создавать его экземпляры, от него можно наследоваться и т.д.
Вот так мы смогли внести в язык новую сущность и использовать её, не прибегая к необходимости правок стандарта языка или компилятора.
Давайте теперь определим класс, который можно было бы использовать в упорядоченных контейнерах. Например, классическую точку для хранения в ordered-контейнере на практике приходится писать вот как-то так:
class Point
{
int x = 0;
int y = 0;
public:
Point() = default;
friend bool operator==(const Point& a, const Point& b)
{ return a.x == b.x && a.y == b.y; }
friend bool operator< (const Point& a, const Point& b)
{ return a.x < b.x || (a.x == b.x && a.y < b.y); }
friend bool operator!=(const Point& a, const Point& b) { return !(a == b); }
friend bool operator> (const Point& a, const Point& b) { return b < a; }
friend bool operator>=(const Point& a, const Point& b) { return !(a < b); }
friend bool operator<=(const Point& a, const Point& b) { return !(b < a); }
};
Но если на этапе компиляции у нас есть рефлексия, позволяющая перечислять данные-члены и добавлять в класс новые методы — мы можем вынести все эти сравнения в метакласс:
$class ordered {
constexpr {
if (! requires(ordered a) { a == a; }) ->
{
friend bool operator == (const ordered& a, const ordered& b)
{
constexpr
{
for (auto o : ordered.variables()) // for each member
-> { if (!(a.o.name$ == b.(o.name)$)) return false; }
}
return true;
}
}
if (! requires(ordered a) { a < a; }) ->
{
friend bool operator < (const ordered& a, const ordered& b)
{
for (auto o : ordered.variables()) ->
{
if (a.o.name$ < b.(o.name)$) return true;
if (b.(o.name$) < a.o.name$) return false; )
}
return false;
}
}
if (! requires(ordered a) { a != a; })
-> { friend bool operator != (const ordered& a, const ordered& b) { return !(a == b); } }
if (! requires(ordered a) { a > a; })
-> { friend bool operator > (const ordered& a, const ordered& b) { return b < a ; } }
if (! requires(ordered a) { a <= a; })
-> { friend bool operator <= (const ordered& a, const ordered& b) { return !(b < a); } }
if (! requires(ordered a) { a >= a; })
-> { friend bool operator >= (const ordered& a, const ordered& b) { return !(a < b); } }
}
};
Что? Выглядит сложно? Да, но вы не будете писать такой метакласс — он будет в стандартной библиотеке или в чём-то типа Boost. У себя в коде вы лишь определите точку, вот так:
ordered Point
{
int x;
int y;
};
И всё заработает!
Точно так же мы, наконец, сможем добиться того, чтобы вещи типа pair или tuple определялись тривиально:
template<class T1, class T2>
literal_value pair
{
T1 first;
T2 second;
};
Посмотрите, ради интереса, как банальная пара определена сейчас.
От открывающихся возможностей разбегаются глаза:
- Мы сможем явно определять в коде гайдлайны вроде «базовый класс должен всегда иметь чисто виртуальный деструктор» или "правило трёх"
- Мы сможем реализовать интерфейсы, value-типы, properties
- Мы сможем отказаться от Moc в Qt и от кастомных компиляторов для С++/CLI и C++/CX, поскольку все эти вещи можно будет описать метаклассами
- Мы сможем генерировать код не внешними кодогенераторами и не тупыми дефайнами, а встроенным мощным фреймворком
- Мы сможем реализовывать на этапе компиляции даже такие сложные проверки, как «во всех ли методах класса, обращающихся к некоторой переменной мы используем критическую секцию, контролирующую доступ к ней?»
Мета-уровень — это очень круто! Правда?
Вот вам ещё на закуску видео, где Герб об этом рассказывает детальнее:
А вот онлайн-компилятор, в котором это всё даже можно попробовать.
Автор: tangro