﻿# С\+\+ без классов?

Классы — это, скорее всего, первое, что добавил Страуструп в далёких 1980\-х, ознаменовав рождение С\+\+\. Если представить, что мы археологи древних плюсов, то косвенным подтверждением этого факта для нас будет this, который по\-прежнему в С\+\+ является указателем, а значит, скорее всего, он был добавлен до "изобретения" ссылок\!

![1259_cpp_no_classes_ru/image1.png](https://import.viva64.com/docx/blog/1259_cpp_no_classes_ru/image1.png)


> Мы опубликовали и перевели эту статью с разрешения правообладателя\\\. Автор статьи — \[Kelbon\]\(https://github\.com/kelbon/\)\\\.



Но речь не про это\. Пора окинуть взглядом пройденный с тех пор путь, изменение и языка, и парадигм, естественный отбор лучших практик, внезапные "великие открытия" и понять, к чему это всё привело язык, который когда\-то вполне официально назывался "C с классами" \(ныне мем\)\.

В конце \(СПОЙЛЕР\) мы попытаемся превратить С\+\+ в функциональный язык за несколько простых действий\.

Для начала рассмотрим базовое применение классов:

```cpp
class Foo : public Bar { // наследование
  public:
  int x;
};
// абсолютно то же самое, но struct
struct Foo : Bar {
  int x;
};
```

Уже на этом простом примере можно заметить, что во времена добавления классов господствовали ООП, инкапсуляция, наследование — всё такое\. Поэтому было принято решение, что класс по умолчанию приватно наследуется, и поля у него тоже по умолчанию все приватные\. Практика показала, что:

* приватное наследование — это чрезвычайно редкий зверь, практически не обитающий в реальном коде;
* у вас всегда есть что\-то публичное, но не всегда есть что\-то приватное\.

И если изначально сишный `struct` не обладал возможностями класса по добавлению методов, конструкторов и деструкторов, то на данный момент `struct` отличается от `class` исключительно этими двумя параметрами по умолчанию, а значит, каждое использование `class` в вашем коде, скорее всего, просто добавляет лишнюю строку\. Но добавление `struct` всех этих возможностей — лишь первый шаг на пути от классов\.

Но ведь у `class` есть ещё много значений\! Давайте посмотрим на них все\!

В шаблоне:

```cpp
template <class T> // same as template <typename T>
void foo() { }
```

Пожалуй, единственное применение этой возможности в 2к22 году — это запутывание читателя, хотя некоторые используют ради экономии аж 3\-х букв\. Не будем судить их\.

В шаблоне, но не так бесполезно \(для объявления шаблонных шаблонных параметров\):

```cpp
// Функция, которая в качестве шаблонного аргумента 
// принимает шаблон с одним аргументом
template <typename <typename> class T>
void foo() { }

// since C++17
template <class <typename> typename T>
void foo() { }

// забавно, но вот так нельзя
template <class <typename> class T> // ошибка компиляции
void foo() { }
```

В С\+\+17 эта возможность устарела и теперь можно писать `typename` без каких\-либо проблем\. Как видите, мы всё дальше уходим от `class`\.\.\.

Знающие С\+\+ читатели явно вспомнят, что есть же ещё класс `enum`\! Тут\-то уж точно никак его не заменить, как отвертеться?

Не поверите, но это работает:

```cpp
enum struct Heh { a, b, c, d };
```

Итого, что мы имеем: на данный момент в С\+\+ нет ни одной реальной необходимости использовать ключевое слово `class`, что забавно\.

Но ведь это ещё не всё\! Слава богам, что С\+\+ не был привязан ни к какой парадигме, и смерть `class` практически ничего не меняет\. Что же происходило с другими "отраслями" программирования?

В середине девяностых внезапно свершились сразу два великих открытия в плюсовом мире: стандартная библиотека шаблонов \(STL\) и метапрограммирование на типах\.

Оба открытия очень "функциональные"\.  Оказалось, что в STL\-алгоритмах гораздо удобнее и гибче использовать шаблоны свободных функций вместо методов\. Кроме того, стоит, конечно, выделить `begin` / `end` / `size` / `swap`, которые за счёт того, что не являются методами, свободно добавляются сторонним типам и работают в шаблонном коде на фундаментальные массивы, такие как в С\.

Метапрограммирование на шаблонах же является чистокровно функциональным, так как там по определению нет глобального состояния и мутабельности, зато есть рекурсия и монады\.

Функции и методы тоже кажутся чем\-то устаревшим, когда существуют лямбды \(функциональные объекты\)\. Ведь, по сути, функция — это функциональный объект без состояния\. А метод — это функциональный объект без состояния, принимающий к тому же ссылку на тип, в котором объявлен\.

Вот, кажется, мы и подошли к той точке, где накопилось достаточно поводов превратить С\+\+ в функциональный язык\.\.\. Ну что же, начнём\!

Если вдуматься, то всё, чего нам не хватает — замена функциям, методам и каррирование, встроенное в язык, что сравнительно просто реализовать на современном С\+\+\.

Возьмём волшебный жезл и мантию метамага:

```cpp
// всё, что делает этот тип, — хранит остальные типы
template <typename ...>
struct type_list;

// реализацию этого можно найти по ссылке,
// основной функционал — взятие сигнатуры функции по типу
template <typename T>
struct callable_traits;
```

Теперь, собственно, объявим тип замыкания, которое будет на компиляции хранить любую лямбду и давать необходимые нам операции:

```cpp
template <typename F>
struct closure;

template <typename R, typename... Args, typename F>
struct closure<aa::type_list<R(Args...), F>> {
  F f; // храним лямбду!
  // Не наследуемся, потому что это может быть указатель на функцию!
  // см. ниже
};
```

Что тут происходит? Есть только одна специализация `closure`, в которой находится основная логика\. Каким образом туда попадает `type_list` с сигнатурой функции и типом, мы рассмотрим ниже\.

Перейдём к основной логике\.

Итак, для начала нужно научить лямбду вызываться\.\.\.

```cpp
  R operator()(Args... args) {
    // static_cast, because Args... are independent template arguments here
    // (they're already known in the closure type)
    return f(static_cast<Args&&>(args)...);
  }
```

Ок, это было несложно, добавим же каррирование:

```cpp
// вспомогательная свободная функция, от которой мы позже избавимся
template <typename Signature, typename T>
auto make_closure(T&& value) {
  return closure<type_list<Signature, std::decay_t<T>>>(std::forward<T>(value));
}

// Учимся находить первый тип в паке параметров
// и выдавать "тип-ошибку", если типов 0
template <typename ...Args>
struct first : std::type_identity<std::false_type> {
};

template <typename First, typename ...Args>
struct first<First, Args...> : std::type_identity<First> {
};

  // внутри closure
  auto operator()(first_t<Args...> value) requires(sizeof...(Args) > 1)
  {
    return [&]<typename Head, typename ...Tail>(type_list<Head, Tail...>)
    {
      return make_closure<R(Tail...)>(
        std::bind_front(*this, static_cast<first_t<Args...>&&>(value))
      );
    }
    (type_list<Args...>{});
  }
```

Тут нужно немного больше объяснений\.\.\. Итак, мы считаем, что если нам дали один аргумент, и функция не вызывается с одним аргументом, то это каррирование\. Принимаем мы "реально" тот тип, который в сигнатуре указан первым\.

Возвращаем лямбду, которая принимает на один тип меньше и запомнила первый аргумент\.

В принципе наша лямбда уже готова\. Но остался последний штрих: что, если функция вызывается с одним аргументом? Как её каррировать? И тут на помощь приходит философия\.

Что есть каррированная функция с одним аргументом при учёте отсутствия глобального состояния в функциональных языках? Ответ неочевидный, но он прост\. Это значение\! Любой вызов такой функции просто является значением результирующего типа, и оно всегда одно и то же\!

Так что мы можем добавить оператор приведения к результирующему типу, но только для ситуации, когда аргументов 0\!

```cpp
  // в closure
  operator R()
      requires(sizeof...(Args) == 0) {
    return (*this)();
  }
```

Стоп\! А мы не забыли ничего? Как же пользователь будет пользоваться этим, нужно же указывать тип? С\+\+ об этом позаботился: CTAD \(class \(heh\) template argument deduction\) позволяет нам написать подсказку для компилятора, как выводить тип, выглядит она так:

```cpp
template <typename F>
closure(F&&) -> closure<type_list<
typename callable_traits<F>::func_type, std::decay_t<F>>>;
```

И наконец мы можем наслаждаться результатом работы:

```cpp
// Замена глобальным функциям:
#define fn constexpr inline closure

void foo(int x, float y, double z) {
  std::cout << x << y << z << '\n';
}

fn Foo = foo; // здесь могла бы быть и лямбда тоже

int main() {
  // каррирование
  Foo(10, 3.14f, 3.1); // просто вызов
  Foo(10)(3.14f, 3.1); // каррирование на 1 аргумент и потом вызов
  Foo(10)(3.14f)(3.1); // каррирование до конца

  // closure возвращающая closure
  closure hmm = [](int a, float b) {
    std::cout << a << '\t' << b;
    return closure([](int x, const char* str) {
      std::cout << x << '\t' << str;
      return 4;
    });
  };

  // Первые 2 аргумента для hmm, вторые 2 для возвращаемой ею closure
  hmm(3)(3.f)(5)("Hello world");

  // ну и мы поддерживаем шаблонные лямбды/перегруженные функции
  // через вот такую вспомогательную функцию
  auto x = make_closure<int(int, bool)>([](auto... args) {
    (std::cout << ... << args);
    return 42;
  });

  // Что, несомненно, удобно, если вы когда-то пробовали захватить по-другому
  // перегруженную функцию
  auto overloaded = make_closure<int(float, bool)>(overloaded_foo);
}
```

[Полный код со всеми перегрузками \(для производительности\)](https://godbolt.org/z/K56YW4Eca) — С\+\+23 deducing this решит эту проблему\.

Версия с `type erasure` для удобного рантайм использования находится в [examples](https://github.com/kelbon/AnyAny)\.