
Кто убил класс? Судебный взгляд на забытое ключевое слово C ++
10 июля 2025 г.Занятия, вероятно, были первым, что добавлено в 1980 -х годах, отмечая рождение C ++. Если мы представляем себя археологами, изучающими древний C ++, одним из косвенных доказательств, подтверждающих теорию, будет «это» ключевое слово, которое все еще является указателем в C ++, предполагая, что оно было введено до ссылок!
МыОпубликовано и переведено эту статьюс разрешения владельца авторских прав. АвторКелбонПолем
Это не главное, хотя. С тех пор давайте посмотрим на эволюцию C ++: язык и его парадигмы, естественный выбор лучших практик и случайные «значительные открытия». Это поможет нам понять, как развивался язык, когда -то официально называемый «C с классами» (теперь это скорее мем).
В конце этой статьи (спойлер) мы постараемся превратить C ++ в функциональный язык на нескольких простых шагах.
Сначала мы рассмотрим базовое использование классов:
class Foo : public Bar { // inheritance
public:
int x;
};
// it's exactly the same, but struct
struct Foo : Bar {
int x;
};
Даже этот простой пример показывает, что ООП, инкапсуляция, наследование и другие связанные концепции были доминирующими парадигмами при введении классов. Было решено, что класс будет унаследован в частном порядке по умолчанию, как и его члены данных. Практический опыт показал это:
- Частное наследство-это чрезвычайно редкое существо, вряд ли когда-либо встречалось в реальном коде;
- У вас всегда есть что -то публичное, но не всегда что -то частное.
Первоначально, C-стильstruct
У меня не было возможностей класса - нет функций, конструкторов или деструкторов. Но сегодня единственная разница междуstruct
иclass
В C ++ сводится к этим двум параметрам по умолчанию. Это означает, что всякий раз, когда мы используемclass
В нашем коде мы, вероятно, добавляем еще одну дополнительную строку. Дающийstruct
Все эти возможности были только первым шагом от традиционных классов.
Ноclass
Ключевое слово имеет еще много определений! Давайте посмотрим на них всех!
В шаблоне:
template <class T> // same as template <typename T>
void foo() { }
Возможно, его единственная цель в 2K22 - путать читателя, хотя некоторые используют его ради сохранения целых три символа. Ну, давайте не будем судить их.
В шаблоне, но не так бесполезно (для декларирования параметров шаблона шаблона):
// A function that takes a template with
// one argument as a template argument
template <typename <typename> class T>
void foo() { }
// since C++17
template <class <typename> typename T>
void foo() { }
// it's funny, but we shouldn't do that
template <class <typename> class T> // compilation error
void foo() { }
В C ++ 17 эта функция устарела, так что теперь мы можем написатьtypename
без каких -либо проблем. Как видите, мы движемся все дальше и дальше отclass
...
Читатели знакомы с C ++, очевидно, помнятenum
сорт! Поскольку нет возможности его заменить, как мы можем его избежать?
Вы не поверите в это, но следующее работает:
enum struct Heh { a, b, c, d };
Итак, это то, что у нас есть: на данный момент нам не нужно использоватьclass
Ключевое слово в C ++, что смешно.
Но подождите, еще не все! Слава богу C ++ не привязана к какой -либо парадигме, поэтому смертьclass
почти ничего не меняется. Что происходило с другими ветвями программирования?
В середине девяностых мир C ++ внезапно стал свидетелем двух великих открытий: стандартная библиотека шаблонов (STL) и тип метапрограммирования.
Они оба были очень функциональными. Они оказались довольно удобными: использование шаблонов бесплатной функции вместо функций членов в алгоритмах STL приводит к большему удобству и гибкости. Аbegin
Вend
Вsize
, иswap
Функции особенно заслуживают внимания. Поскольку они не функционируют члены, их можно легко добавить к сторонним типам и работать с фундаментальными типами, такими как C-массивы, в коде шаблона.
Метапреграммирование шаблонов является чисто функциональным, потому что он не имеет глобального состояния или изменчивости, но имеет рекурсию и монады.
Функции и функции членов также кажутся чем -то устаревшим по сравнению с Lambdas (функциональные объекты). В конце концов, функция по сути является функциональным объектом без состояния. И функция -член является функциональным объектом без состояния, которое придерживается ссылки на его объявленный тип.
Кажется, что теперь мы накопили достаточно причин, чтобы превратить C ++ на функциональный язык ... Хорошо, давайте начнем!
Если мы думаем об этом, все, что нам не хватает,-это замена функций, функций членов и встроенного каррики, которые относительно легко реализовать в современном C ++.
Давайте владеем магическим персоналом и расстанемся в манере для получения металла:
// this type only stores other types
template <typename ...>
struct type_list;
// you can find its implementation at the link,
// the main feature is to take the function signature by type
template <typename T>
struct callable_traits;
Теперь давайте заявим о типе закрытия, который будет хранить любую лямбду и обеспечить необходимые операции во время компиляции:
template <typename F>
struct closure;
template <typename R, typename... Args, typename F>
struct closure<aa::type_list<R(Args...), F>> {
F f; // we store the lambda!
// We don't inherit here because it might be
// a pointer to a function!
// see below
};
Что здесь происходит? Есть только одинclosure
Специализация, где находится основная логика. Посмотрим ниже, какtype_list
С подписью функции и типа попадают туда.
Давайте перейдем к основной логике.
Во -первых, нам нужно научить лямбду, как называть ...
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)...);
}
Ладно, это было легко, теперь давайте добавим кадры:
// an auxiliary free function that we'll remove later on
template <typename Signature, typename T>
auto make_closure(T&& value) {
return closure<type_list<Signature, std::decay_t<T>>>(std::forward<T>(value));
}
// We learn to detect the first type in the parameter package
// and issue a "type-error" if there are 0 types
template <typename ...Args>
struct first : std::type_identity<std::false_type> {
};
template <typename First, typename ...Args>
struct first<First, Args...> : std::type_identity<First> {
};
// within 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...>{});
}
Эта часть требует немного большего объяснения ... так что, если нам дают один аргумент, и функция не может быть вызвана только одним, мы предполагаем, что это карри. Мы «на самом деле» принимаем тип, который указан сначала в подписи.
Мы возвращаем Lambda, которая занимает меньше одного типа и запомнила первый аргумент.
Наша Lambda сейчас готова. Окончательный штрих остается: что, если функция вызывается только с одним аргументом? Как мы это приводим? Вот где входит философия.
Что такое карри с одним аргументом, учитывая, что функциональным языкам не хватает глобального состояния? Ответ не очевиден, но это просто. Значение! Любой вызов такой функции - это просто значение полученного типа, и он всегда одинаково!
Таким образом, мы можем добавить актеров в результирующий тип, но только тогда, когда у нас есть 0 аргументов!
// in closure
operator R()
requires(sizeof...(Args) == 0) {
return (*this)();
}
Ждать! Разве мы не забываем что -то? Как пользователь должен использовать это? Им нужно указать тип, не так ли? C ++ позаботился об этом! CTAD (класс (хех) шаблон вычета аргумента) позволяет нам написать подсказку для компилятора, как вывести тип. Вот как это выглядит:
template <typename F>
closure(F&&) -> closure<type_list<
typename callable_traits<F>::func_type, std::decay_t<F>>>;
Наконец -то мы можем насладиться результатом:
// The replacement for global functions:
#define fn constexpr inline closure
void foo(int x, float y, double z) {
std::cout << x << y << z << '\n';
}
fn Foo = foo; // the lambda could be here, too
int main() {
// currying
Foo(10, 3.14f, 3.1); // just a normal call
Foo(10)(3.14f, 3.1); // currying by one argument and then calling
Foo(10)(3.14f)(3.1); // currying up to the end
// closure returning 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;
});
};
// First two arguments are for hmm, second two are for the closure it returns
hmm(3)(3.f)(5)("Hello world");
// we also support template lambdas/overloaded functions
// via this auxiliary function
auto x = make_closure<int(int, bool)>([](auto... args) {
(std::cout << ... << args);
return 42;
});
// This is certainly useful if you've ever tried to capture
// an overloaded function differently
auto overloaded = make_closure<int(float, bool)>(overloaded_foo);
}
Полный код со всеми перегрузками (для производительности)- Эта проблема решается в C ++ 23 с «выведением этого».
Версия сtype erasure
Для удобного использования во время выполнения впримерыПолем
Оригинал