Метаклассы в C++

в 14:38, , рубрики: c++, Блог компании Инфопульс Украина, Компиляторы, метаклассы, Программирование, Совершенный код

В этой статье мы поговорим о новом предложенном расширении языка С++ — метаклассах. Герб Саттер с коллегами работал над этим предложением около 2 лет и, наконец, этим летом представил его общественности.

Итак, что же такое «метакласс» с точки зрения Герба Саттера? Давайте вспомним наш С++ — самый прекрасный в мире язык программирования, в котором, однако, веками десятилетиями существуют примерно одни и те же сущности: переменные, функции, классы. Добавление чего-то фундаментально нового (вроде enum classes) занимает очень много времени и рассчитывать дождаться включения чего-то нужного вам здесь и сейчас в стандарт — не приходится. А ведь кое-чего и правда не хватает. Например, у нас всё ещё нет (да, наверное, и не будет) интерфейсов как таковых (приходится эмулировать их абстрактными классами с чисто виртуальными методами). Нет properties в полном их понимании, нет даже value-типов (чего-то такого, что можно было бы определить как набор переменных простых типов и сразу использовать во всяких там контейнерах/сортировках/словарях без определения для них разных там операций сравнения, копирования и хеширования). Да и вообще постоянно чего-то кому-то не хватает. Разработчикам Qt вот не хватает метаданных и кодогенерации, что заставляет их использовать moc. Разработчикам C++/CLI и C++/CX не хватило способов взаимодействия со сборщиком мусора и своими системами типов. Ну и т.д.

А давайте на секунду представим, что мы сами можем вводить в язык новые сущности. Ну или пусть не прямо «сущности», а правила проверки и модификации классов.

Как это всё будет работать. Герб предлагает ввести понятие «метакласса», как набора правил и кода, которые будут выполняться на этапе компиляции и на основе которых компилятор будет проверять классы в коде и/или создавать новые классы на основе вышеупомянутых правил.

Например, нам хочется иметь в языке классический интерфейс. Что такое «интерфейс»? Например, стандарт языка С# отвечает на этот вопрос на 18 страницах. И с этим есть целый ряд проблем:

  1. Никто их не читает
  2. Компилятор совершенно не гарантированно реализует именно то, что написано в тех 18 страницах текста
  3. У нас нет возможности проверить соответствие работы компилятора и текста на английском языке
  4. Для С++ пришлось бы написать такую же спецификацию и её реализацию в компиляторах (а зная С++ — так ещё и намного более сложную). А дальше см. пункты 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js