Итак, что же такое «метакласс» с точки зрения Герба Саттера? Давайте вспомним наш С++ — самый прекрасный в мире язык программирования, в котором, однако,
А давайте на секунду представим, что мы сами можем вводить в язык новые сущности. Ну или пусть не прямо «сущности», а правила проверки и модификации классов.
Как это всё будет работать. Герб предлагает ввести понятие «метакласса», как набора правил и кода, которые будут выполняться на этапе компиляции и на основе которых компилятор будет проверять классы в коде и/или создавать новые классы на основе вышеупомянутых правил.
Например, нам хочется иметь в языке классический интерфейс. Что такое «интерфейс»? Например, стандарт языка С# отвечает на этот вопрос на 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, поскольку все эти вещи можно будет описать метаклассами
- Мы сможем генерировать код не внешними кодогенераторами и не тупыми дефайнами, а встроенным мощным фреймворком
- Мы сможем реализовывать на этапе компиляции даже такие сложные проверки, как «во всех ли методах класса, обращающихся к некоторой переменной мы используем критическую секцию, контролирующую доступ к ней?»
Мета-уровень — это очень круто! Правда?
Вот вам ещё на закуску видео, где Герб об этом рассказывает детальнее:
А вот онлайн-компилятор, в котором это всё даже можно попробовать.
Комментарии (82)
ababo
27.07.2017 18:42Не примут.
alexeykuzmin0
27.07.2017 18:49Почему вы так думаете? Уже есть Technical Specification, и clang поддерживает ее 4 версию.
Overlordff
27.07.2017 19:15Если я правильно понимаю, то интерфейсы, описанные в статье и близко не равны настоящим интерфейсам из С#. Ведь интерфейсы как набор из чисто виртуальных методов имеют оверхед времени выполнения, но в С# интерфейсы разруливаются на этапе компиляции.
mayorovp
27.07.2017 19:23+2И каким же образом интерфейсы могут разруливаться на этапе компиляции? Особенно вот в таком коде:
object a = ...; IFoo b = (IFoo)a; b.Bar();
CyberKastaneda
27.07.2017 19:49+1Незнаю как в C#, но в Java вообще все методы по дефолту виртуальны, JIT умеет делать оптимизацию «девиртуализация», но это JIT. Поэтому накладных расходов из-за использования интерфейсов нет. В C# скорее всего так же, но, как писал выше, я незнаю.
a-tk
27.07.2017 22:10Почти. Только по умолчанию методы не виртуальные. Девиртуализация работает для классов и методов, полученных из интерфейсов.
qw1
27.07.2017 23:41+1Компилятор C++ тоже умеет делать девиртуализацию, когда может определить, какой класс будет присутствовать по интерфейсной ссылке в данном месте программы, например в случае:
IFoo* a = new MyFoo(); a->Bar();
qw1
27.07.2017 23:47+1JIT умеет делать оптимизацию «девиртуализация», но это JIT
JIT в данном случае может сделать не больше, чем статический компилятор.
Runtime-статистика не поможет. Ведь, если в 1000 случаев по ссылке IFoo пришёл объект MyFoo, то это не гарантирует, что в 1001-й раз не придёт объект YourFoo.
Стандартный трюк JIT (сделаем простую проверку и, если угадали, запускаем оптимизированный под частый случай код, а если не угадали — универсальный, но медленный код) тут не поможет, т.к. проверка принадлежности объекта классу + невиртуальный вызов тяжелее, чем сразу виртуальный вызов без проверки.tmaxx
28.07.2017 10:53Вообще-то может из-за наличия class loaders. Объект YouFoo гарантированно не может придти если такой класс не загружен.
Я не уверен что все работает именно так, но при загрузке новых классов точно перекомпилируются некоторые методы. И логично при этом посчитать количество реализаций интерфейса и избавится от виртуального вызова (если 1) или заменить на switch-case (если мало).
qw1
28.07.2017 12:45То есть, компилятор проверяет условие, что интерфейс реализуется ровно одним загруженным классом, и у него нет наследников. Слишком жёсткие условия. Хотя, в java VM, где большинство методов виртуальные, эта оптимизация может очень часто выстрелить.
tmaxx
28.07.2017 21:33Да, только не весь интерфейс а его отдельные методы. Если конкретный метод имеет ровно одну загруженную реализацию, он будет девиртуализирован. А если реализация небольшая — ещё и заинлайнен. Сколько у интерфейса наследников неважно.
isotoxin
27.07.2017 22:15Вот этого мне в C++ очень нехватает. Иногда приходится городить многоэтажные конструкции из макросов и шаблонов, эксплуатировать в хвост и гриву препроцессор и постоянно натыкаться на несовместимости разных компиляторов. Было бы замечательно от всего этого избавиться.
Кстати, что-то похожее (я имею в виду мета-код) есть в языке Nemerle, который, к сожалению, так и не получил распространения (хотя может и к счастью).
daiver19
28.07.2017 05:07+1С одной стороны, круто.
С другой стороны, примеры натянуты. Профит от интерфейсов как отдельной сущности неясен, в том же шарпе это более безопасный механизм для множественного наследования, в С++ оно и так работает для чего угодно и искусственные ограничения особо ничего не изменят. В примере с точкой: во-первых, достаточно оператора <. Во-вторых, есть пары/tuple, которые хоть и грязновато, но делают то же самое. В-третьих, структуры часто не настолько тривиальны и вам может быть нужно, например, сравнивать только 2 поля из 3х или же сравнивать поля в разных направлениях итд. И самое страшное случится, когда это надо не на стадии дизайна структуры, а уже потом. И придется вам убирать этот ordered и писать весь код сравнения заново.
Так что хотелось бы увидеть примеры чего-то из Qt новыми средствами ну или вообще что-нибудь такое, что сейчас в языке сделать нельзя/делается крайне неудобно.Dark_Daiver
28.07.2017 05:48+2Поддерживаю. Лучше бы уж сразу дали работу с AST из cpp кода. Чтобы самому можно было написать аналог «deriving», например.
iCpu
28.07.2017 06:27А вы представьте, ведь так можно было бы определить
using byte = uint8_t; $class serializable { std::vector<byte> serialize() { std::vector<byte> ret; for (auto o : serializable.variables()) { if (o.is_class<serializable>::value) { ret.append(o.serialize()); } else { compiler.require(!std::is_trivial<typeof(o)>::value, "a value type must be either trivial or serializable"); ret.append(reinterpret_cast<byte*>(&o), sizeof(o)); } } return ret; } void unserialize(std::vector<byte> v, size_t &offset) { std::vector<byte> ret; for (auto o : serializable.variables()) { if (o.is_class<serializable>::value) { o.unserialize(v, offset); } else { compiler.require(!std::is_trivial<typeof(o)>::value, "a value type must be either trivial or serializable"); memcpy(&o, sizeof(o), v.data(), offset); offset += sizeof(o); } } } };
и просто прописывать
serializable Point3f { float x, y, z; }; serializable Mesh { Point3f a, b, c; Point3f texa, texb, texc; }; int main() { std::vector<byte> v = magic(of_puberty); size_t offset = 0; Mesh mesh; mesh.unserialize(v, offset); return 0; }
И от макроса Q_OBJECT можно было бы избавиться. А если можно будет определять мета-методы или мета-секции (или определять свои модификаторы аля __declspec), то даже не придётся париться по поводу синтаксиса сигналов и слотов — оно само собой разрешится.daiver19
28.07.2017 07:14Я собственно и попросил пример из Qt или чего-то еще, где это незаменимо. Я не говорю, что это не нужно, мне просто не понравились примеры из статьи.
По поводу вашего примера: выглядит прикольно, но на деле костыль, имхо. Самое важное — нельзя же сделать, например, ordered serializable? Во-вторых, неясно, как вы, например, контейнеры будете сериализовать? (я так понимаю — никак)iCpu
28.07.2017 09:12+1Извините, в силу объективных причин я несколько ограничен в ответах, их числе и частоте.
Dark_DaiverЛучше бы уж сразу дали работу с AST из cpp кода. Чтобы самому можно было написать аналог «deriving», например.
Собственно, что мета-уровень в описанном виде, что доступ к AST-дереву должны иметь какой-либо синтаксис. У нас есть два варианта: расширение синтаксиса шаблонов или новый. Оба варианта плохи. Введение нового синтаксиса даст ЧЕТВЁТРЫЙ язык программирования внутри плюсов, пополнив непосредственно плюсы, макросы и шаблоны. Встаёт вопрос, куда его впихнуть в этапе компиляции, к примеру, да и много чего по совместимости. Шаблоны уже сейчас выглядят ХТОНИЧЕСКОЙ МАГИЕЙ, которую можно написать лишь единожды, и их, наоборот, стоило бы разгрузить от ненужных нагромождений. Возможно, кстати, введение простого систаксиса доступа к AST параметрам без выведения кучи новых шаблонов дало бы нам вздохнуть спокойно (Variable.is_const() вместо std::is_const::value смотрится, имхо, куда плюсовее).
Что до trait'а — всё так. Можно было бы обойтись std::serialize<>, если бы.
daiver19Я собственно и попросил пример из Qt или чего-то еще, где это незаменимо. Я не говорю, что это не нужно, мне просто не понравились примеры из статьи.
Qt6, судя по рассказам разработчиков, уходит в шаблоны. В общем, для отказа от moc не хватает доступа к AST в любом виде.
Если не углубляться в реализацию, вот один из вариантов замены генерируемого moc'ом кода.struct qt_meta_stringdata<$object> { constexpr { for... (auto method : $object.methods()) { if (method.hasProperty("slot")) { QT_MOC_ADD_LITERAL(method.name()); } } } }; struct qt_slot_property { uint name, argc, parameters, tag, flags; }; struct qt_slot_properties<$object> { uint ammount; std::array<qt_slot_property,ammount> properties; constexpr { ammount = std::count_if< $object.methods(), [](auto& method)->bool{return method.hasProperty("slot");} >(); properties = std::make_array_if_impl< $object.methods(), [](auto& method) {return method.hasProperty("slot");}, [](auto& method) {return qt_slot_property prop(QT_MOC_GET_LITERAL_INDEX(method.name()),method.arg().size(), ...);} >(); } } }; $class object { /* ... */ static QMetaTypes qt_meta_types; static const qt_meta_stringdata<$object> qt_meta_stringdata_$$object; static const qt_slot_properties<$object> qt_meta_data_$$object; static const std::array <$object::method_type> qt_meta_vtable_$$object; void qt_static_metacall(object *_o, QMetaObject::Call _c, int _id, void **_a) { if (_c == QMetaObject::InvokeMetaMethod) { auto args = qt_make_arg_list(qt_meta_vtable_$object[_id], _a); qt_meta_vtable_$object[_id].invoke(args); } } constexpr { qt_meta_types.append(meta_type($object)); } };
tangro
28.07.2017 11:15Я собственно и попросил пример из Qt
Этот пример есть в документе по ссылке. Qt-код будет выглядеть вот так:
QClass MyClass { property<int> value { }; signal mySignal(); slot mySlot(); };
QClass, property, signal и slot будут определены разработчиками Qt как метаклассы со своими правилами генерации соответствующих методов и всё это будет компилится любым компилятором, без всякого moc.
Dark_Daiver
28.07.2017 07:22Согласен с предыдущим комментатором и добавлю, что (имхо) ваш пример хорошо бы смотрелся в виде «трейта» с автоматическим выводом реализации, чем в виде метакласса
Antervis
28.07.2017 09:21а если вдруг надо сериализовать поля не «в лоб»? Например, поле _с пишется в поток только если _b == true? А _d надо перед запихиванием разыменовать, потому что _d — указатель?
«Свои модификаторы аля __declspec» в языке уже есть, см. attribute sequence. По стандарту, компилятор обязан игнорировать атрибуты, которые он не поддерживает.
Это решение с изъянами, и оно не позволит в дальнейшем сделать что-то другое потому что «такой синтаксис уже используется»
Gorthauer87
28.07.2017 09:28+3В derive Rustа можно выводить несколько сущностей, лишь бы они не конфликтовали. А здесь только одну.
siexp
28.07.2017 11:19Разве это не будет просто синтаксическим сахаром для макросов? Как к примеру дебажить такие мета-классы?
tangro
28.07.2017 11:21Нет, не будет. В смысле — «как дебажить?». Метаклассы — объект этапа компиляции, на рантайме у вас будут обычные классы, с обычными методами. Ставьте себе бряки и дебажте. Даже вон тестовый онлайн-компилятор (ссылка в статье) позволяет посмотреть код сгенерированного по метаклассу реального класса.
orcy
28.07.2017 22:31+1Поскольку выполняются они на этапе компиляции, то и дебажить их надо на этапе компиляции. Видимо как с constexpr и шаблонами сейчас.
oleg-m1973
28.07.2017 11:21Да, штука полезная. Позволит уменьшить объём кода в разы. Тяжеловато, конечно, для понимания, по сути новый язык, но те же шаблоны тоже трудно дались в своё время.
pavlushk0
28.07.2017 11:21-1Я не профессиональный программист, я инженер, в круг интересов которого программирование входит, и для меня подобные вещи превращают программирование в шаманство. Я всегда относился к языку программирования как к инструменту вычислений (порой очень запутанных) и обслуживания этих вычислений. Последнее время, читая подобные статьи, не перестаю удивляться как далеко зашли требования профессионалов к ЯП, насколько сильно развилась мысль и какие конструкции в языках, необходимы для реализации этих требований.
DesertDragon
28.07.2017 15:14+3Как раз вот эти нововведения (рефлексия, метаклассы и compile-time programming), наконец-то, очень сильно упрощают программирование, и делают большую часть шаманства и шаблонной магии не нужными.
После того как я увидел как в компиляторе https://cppx.godbolt.org/ уже работает рефлексия:
struct { int x,y; } p; // получаем количество переменных в структуре переменной (без предкомпиляции, шаблонов и библиотек): сonstexpr size_t a = $decltype(p).variables().size(); // mov qword ptr [rbp - 32], 2
И сравнив с огородом шаблонов, которые практически невозможно отлаживать, в своем проекте, мне захотелось взять этот транк clang, и использовать в продуктиве, настолько это круто и просто.
Tiendil
28.07.2017 13:00-2Пожалуй это та фича, которая плюсы может похоронить — сложность возрастёт неимоверно. Особенно если студентота за метаклассы возьмётся с энтузиазмом, а она возьмётся. Код станет читать невохможно без полного изучения какого-нибудь нового boost.
Хотя сама идея метаклассов хорошая, но она больше для языков с duck typing. Для Python, например.
Точно не надо разрешать вводить новые «ключевые слова». Хотя бы так уже:
class MyClass: meta MyMetaClass {}
0xd34df00d
28.07.2017 20:17Этакий duck typing не обязательно означает динамическую типизацию, так что не только Python.
Antervis
28.07.2017 16:41+1Нельзя просто сказать «этот класс сравниваем» и чтобы компилятор сгенерировал оператор сравнения, ведь он может быть весьма нетривиальным. Простой пример:
class MyVector { // ... private: MyVectorPrivate *_d; }
Компилятор очевидно будет не прав, если сгенерирует оператор сравнения векторов как lhs._d == rhs._d.
Зато с концептами возможен путь от обратного: класс сравниваем (EquallyComparable), если определен оператор ==, возвращающий bool (requires (T a, T b) { {a == b} -> bool; };)
Приведу пример. С концептами/ренджами и uniform call syntax, вашему MyVector достаточно реализовать методы begin(), возвращающий указатель на начало и end(), возвращающий указатель за конец массива и о чудо! MyVector становится контейнером, для которого определен весь стандартный набор алгоритмов над диапазонами (например, sort, если тип, возвращаемый *begin(), поддерживает оператор <).
Есть еще propolsal по operator «spaceship» <=> — один метод, реализующий весь набор функций сравнения.iCpu
28.07.2017 18:29Ну ё-моё, я бы понял, если бы такие замечания делали в 97-ом году, но не в 17-ом же!
К примеру...$class comparable { friend bool operator == (const comparable& a, const comparable& b) { constexpr { for... (auto o : comparable.variables()) { compiler.require(o$.isPointer(), "Неоднозначное сравнение!"); -> { if (!(a.(o.name)$ == b.(o.name)$)) return false; } } } return true; } }; $class pointer_comparable : comparable { friend bool operator == (const pointer_comparable& a, const pointer_comparable& b) { constexpr { for... (auto o : pointer_comparable.variables()) { -> { if (!(a.(o.name)$ == b.(o.name)$)) return false; } } return true; } } }; $class value_comparable : comparable { friend bool operator == (const value_comparable& a, const value_comparable& b) { constexpr { for... (auto o : value_comparable.variables()) { if (!o.isPointer()) -> { if (!(a.(o.name)$ == b.(o.name)$)) return false; } else { if (!(*a.(o.name)$ == *b.(o.name)$)) return false; } } } return true; } }; $class no_pointer_comparable : comparable { friend bool operator == (const no_pointer_comparable& a, const no_pointer_comparable& b) { constexpr { for... (auto o : no_pointer_comparable.variables()) { if (!o.isPointer()) -> { if (!(a.(o.name)$ == b.(o.name)$)) return false; } } return true; } } }; template <value_comparable T> comparable TreeNode { using ParentNode = (TreeNode*).as(pointer_comparable); using ChildNode = (TreeNode*).as(no_pointer_comparable); ParentNode parent; std::set<ChildNode> children; T value; };
Antervis
28.07.2017 18:47+11. все ваши pointer_comparable/value_comparable не будут работать с большей частью классов даже из стандартной библиотеки. Домашнее задание вам: напишите четыре причины почему не заведется с std::string
2. для нетривиальной итерации да, придется определить итератор. Но зачем их разделять? Определил методы */+±- вуаля, у нас bidirectional_iterator. Определил методы +(Integral)/-(Integral) — вуаля, у нас random_access_iterator. В вашем случае всё равно придется определять операторы */+±- для итератора. В итоге всё то же самое, но с дополнительными действиями.Antervis
28.07.2017 18:54поправка: операторы * / ++ / --
iCpu
28.07.2017 19:471) Вы не путайте других и себя, мы находимся на этапе мета-компиляции, когда ещё даже шаблоны не раскрыты. std::string на этом этапе будет классом-контейнером. Так что operator== для него будет ровно однозначен, как и isPointer.
Если же вы имеете ввиду дуализм указателей, *pointer[], то это решается через
$class pointer { operator[]() = delete; } и $class array { operator*() = delete; }. Очень интересно, как вы реализуете это в рамках концептов и кто в этот раз придёт на зов рунических записей?
2) Из наличия оператора ++ не следует принадлежность к классу итераторов. Из наличия operator bool() не следует валидность при истинном значении. Бинарные операции не обязаны быть рефлексивными и транзитивными, да и геометрия не вписывалась всегда быть евклидовой. Поэтому ваш подход "если это выглядит и пахнет как банан, то это банан" заранее ущербен.
Мне не придётся ничего дописывать, так как у указателя, встроенного типа, уже есть операторы ++ и --, да и оператор * в наличии.Antervis
28.07.2017 20:26+1Так что operator== для него будет ровно однозначен, как и isPointer.
а теперь почитайте про SSO
Очень интересно, как вы реализуете это в рамках концептов и кто в этот раз придёт на зов рунических записей?
Здравый смысл. Если метод не определен вручную, то не придется его удалять. Помимо этого:
1. random access iterator должен обладать методом operator[].
2. ничто не мешает явно запретить методы для класса.
3. ничто не мешает поставить! перед условием концепта
4. операторы * / -> и прочие могут возвращать не указатель/ссылку на объект, а прокси-объекты. Что будете делать в таком случае?
Из наличия оператора ++ не следует принадлежность к классу итераторов
правильно. Для принадлежности к классу итераторов, оператор ++ должен не просто быть, а еще и возвращать итератор. Также должен присутствовать оператор *, возвращающий ссылку на объект. А чтобы нашего голубя за наличие перьев и клюва не приняли за утку, можно точно так же явно запретить ему крякать, UCS: MyVector {… sort() = delete;… };iCpu
28.07.2017 21:17SSO
Ещё раз, причём тут оно, если у меня даже шаблоны не раскрыты? Причём тут SSO, если у меня ещё std::basic_string в std::string не раскрыт?
1. random access iterator должен обладать методом operator[].
2. ничто не мешает явно запретить методы для класса.
3. ничто не мешает поставить! перед условием концепта
4. операторы * / -> и прочие могут возвращать не указатель/ссылку на объект, а прокси-объекты. Что будете делать в таком случае?
1. Вот ему этот оператор и отдадим.
2. Да, только вам нужно это делать вручную или наследоваться, наращивая дерево потомков, хотя можно определить метатип, где всё уже сделано.
3. Ставьте.
4. Я — ничего, потому как в таком случае это будет не указатель. А в общем случае — буду писаать решение. Возможно — используя шаблоны.
правильно.
#comment_10333414 второй спойлер.
Описанный вами подход: всё, что имеет операторы * и ++ нужной сигнатуры — итераторы. Описанный мной: всё, что подходит под критерии итераторов, может быть объявлено итераторами.
Если на итератерах разница не так очевидна, возьмите задачу сериализации. Мы можем объявить часть данных сериализуемыми и выполнять сохранение только этих данных. При этом, нам не придётся каждый раз беспокоиться, совпадут ли типы и везде ли выполнены изменения, нам даже метод (де-)сериализации объявлять не придётся. Для трейтов мы такого сделать не можем, нам в любом случае придётся обёртывать поля в некие структуры-контейнеры. Что обязательно скажется на семантике.Antervis
28.07.2017 22:06-1Причём тут SSO, если у меня ещё std::basic_string в std::string не раскрыт?
При том, что поведение всяких == для строк зависит от того, является ли строка emlaced, (это доп. проверка). При этом capacity строк может быть различной — от этого они менее равны не станут. И да: как вы явно дадите компилятору понять, что указатель на начало может быть nullptr и разыменовывать его не надо?
2. Да, только вам нужно это делать вручную или наследоваться, наращивая дерево потомков, хотя можно определить метатип, где всё уже сделано.
а как только надо сделать что-то чуть-чуть отличающееся, надо писать новый метатип, а потом на основе него тип? How about no
#comment_10333414 второй спойлер.
В вашем списке отсутствуют реализации методов begin() и end(). Оператор * итератора вашего списка вернет ноду, а не value списка. BackwardIterator является подмножеством ForwardIterator'а. Поверх этого, написать список вручную даже проще. А вдруг я хочу итератор в многомерном массиве?
Кстати, я уже говорил, что Ranges поддерживают диапазоны с разными типами begin() и end()?
возьмите задачу сериализации.
где вы сериализовали размер вектора? А где вы его десериализовали? Упс.
Я лишь пытаюсь сказать, что подгонка под ваши метаклассы в общем случае займет не меньше времени, чем написание всего класса с нуля. У вас возникает путаница с решениями, которые вы защищаетеiCpu
29.07.2017 06:33+1При том, что <...>. И да: как вы явно дадите компилятору понять, что указатель на начало может быть nullptr и разыменовывать его не надо?
Вы что, решили что я додумаюсь выводить operator== у std::string через метаклассы? Метаклассы не для этого. Вообще, зачем мне переписывать std::string, если у него уже определено всё интересующее меня поведение?
а как только надо сделать что-то чуть-чуть отличающееся, надо писать новый метатип, а потом на основе него тип? How about no
А сейчас с шаблонами разве не так?
В вашем списке отсутствуют реализации методов begin() и end().
Простите, что не написал реализацию всего std.core через метатипы. Что за детский сад?
где вы сериализовали размер вектора? А где вы его десериализовали? Упс.
Очень интересно, как вы сериализуете динамический массив на сырых указателях? (Ответ: никак, хотя и можно потрахаться со смещениями и сырым считыванием памяти.) А с контейнерами у меня всё будет просто:
template <class T> $class container { using Type = T; size_t size() const; iterator begin() const; iterator end() const; void append(const Type & t); }; /* Грубо, ибо синтаксиса ещё нет, да и мне влом снова писать сериализацию типов*/ $class serializable { constexpr { for... (auto v : serializable$.values()) { /* ... */ if (v.is(container) && v$.Type.is(serializable)) -> { v.name()$.size().serialize(); for (auto i = v.name()$.begin(); i != v.name()$.end(); ++i) (*i).serialize(); } /* ... */ auto size_type = serializable$.method("size").return_type().name(); -> { size_type$ sz = 0; sz.unserialize(buffer_reader); for (size_type$ i = 0; i != sz; ++i) { Type$ t; t.unserialize(); v.name$.append(t); } } } } };
И, заметьте, в кой-то веке запись мета-уровня выглядит единообразно с остальной программой. Никаких проваливающихся на дно #if !defined(QT_NO_UNSHARABLE_CONTAINERS), никаких уходящих в небо проверок на std::is_array::value, вся запись выглядит как один и тот же язык программирования.Antervis
29.07.2017 06:48std::array тоже контейнер, но при его сериализации не нужно записывать размер, т.к. он содержится в типе. И снова ваш serializeable не подойдет.
И, заметьте, в кой-то веке запись мета-уровня выглядит единообразно с остальной программой
посчитайте число спец. символов в вашем коде и в коде через шаблон.
Никаких проваливающихся на дно #if !defined(QT_NO_UNSHARABLE_CONTAINERS), никаких уходящих в небо проверок на std::is_array::value, вся запись выглядит как один и тот же язык программирования.
Где вы последний раз видели «проваливающиеся на дно» дефайны, кроме как для определения фич платформы/компилятора?
С концептами все std::is_array::value не нужны. Вы о них вообще читали? Они реализованы в gcc аж с шестой версии, сейчас head — восьмаяiCpu
29.07.2017 17:51+1std::array тоже контейнер, но при его сериализации не нужно записывать размер, т.к. он содержится в типе. И снова ваш serializeable не подойдет.
Сделайте это всё на шаблонах, потом поговорим.
посчитайте число спец. символов в вашем коде и в коде через шаблон.
Сначала расскажите, как в чисто процедурном языке появился функциональный метаязык и приведите аргументы, почему я в процедурном языке должен отказаться от процедурного метаязыка, оставаясь на функциональных шаблонах, от которых даже ярых хаскельщиков мутит. А уже потом мы с вами сигнумометрией позанимаемся.
Где вы последний раз видели «проваливающиеся на дно» дефайны, кроме как для определения фич платформы/компилятора?
Ну, чего далеко ходить? Концепты для отчаявшихся
Концепты, напомню, ещё не включены в стандарт, как и модули. И то, что они работают в gcc аж с шестой версии совершенно не гарантирует их работу в VC и Clang. И, тем более, не гарантирует их одинаковое ожидаемое поведение.0xd34df00d
29.07.2017 21:54Ну вот не надо, я люблю шаблоны, например.
Метаклассы, впрочем, тоже ничего так.iCpu
30.07.2017 07:21+1У меня к шаблонам простая претензия — они чрезвычайно сложно осваиваются. Мне, например, потребовался год практики и курс хаскеля, чтобы хоть немного выйти за пределы объявления контейнеров. Знаю ряд людей, которые на теме «Шаблоны в с++» перешли на Java или C#. И они мудры в своём решении.
Antervis
31.07.2017 07:44Сделайте это всё на шаблонах, потом поговорим.
Qt же сделали как-то сериализацию всех своих классов, на шаблонах. Даже без с++11.
Сначала расскажите, ...
то, что есть сейчас, не идеально — не спорю. Многие решения в с++ были приняты, скажем, неаккуратно. Но неужели это значит, что надо заменять недоделанное недоделанным? Беглый осмотр propolsal'а по метаклассам и я нахожу там около 4-5 изъянов.
Ну, чего далеко ходить? Концепты для отчаявшихся
С синтаксисом rvalue факап вышел, имо. Хотя, проблема в статье основана на том, что автор не знал про std::forward
И то, что они работают в gcc аж с шестой версии совершенно не гарантирует их работу в VC и Clang
корректную работу в VC вообще никто и ничто никогда не гарантирует, включая стандарт. Это касается не только концептов, ренджей и модулей (которые именно ms и продвигают), но и вот этих вот метаклассов, ведь interface — макрос VC для structiCpu
31.07.2017 09:01Qt же сделали как-то сериализацию всех своих классов, на шаблонах. Даже без с++11.
Автоматический вывод — никак. И не на шаблонах, на шаблонах только контейнеры — и то через жопу. Достаточно сказать, что простое объявление
вываливает ошибку компиляции. И нужно городить шаблон каста в базовый тип или определение оператора. Пример не засчитан.enum class myEnum: quint8{ ZERO = 0; }; myEnum my = myEnum::ZERO; QDataStream data; /* init QDataStream */ data << my;
Всё же, как будет выглядеть шаблон, который добавляет произвольному классу сериализацию? Ну?
то, что есть сейчас, не идеально — не спорю. Многие решения в с++ были приняты, скажем, неаккуратно. Но неужели это значит, что надо заменять недоделанное недоделанным? Беглый осмотр propolsal'а по метаклассам и я нахожу там около 4-5 изъянов.
Повторяю вопрос: почему я в процедурном языке программирования не имею права на долбаный процедурный метаязык? Никто же не призывает заменить шаблоны полностью, уж слишком прочно шаблоны вошли в нашу жизнь, да и назначение у них разное. Но можно предоставить программистам единообразный инструмент?
корректную работу в VC вообще никто и ничто никогда не гарантирует, включая стандарт.
Ну да, cl бастует, если функция не возвращает значение int foo(){}, хотя формально это UB. Отошли от стандарта, вай-вай! Сколько раз уже бывало, когда плюшки из экспериментального gcc после принятия нового стандарта приобретали совершенно обратный вид, так что код сливался в канализацию? Да что там gcc, у нас комитет с initialization_list до сих пор решиться не может, как он должен работать. Каждый пишет кто во что горазд, ну а виноват, естественно, мелкомяхк.Antervis
31.07.2017 11:03-1вываливает ошибку компиляции
enum class в общем и целом придуман сугубо для того, чтобы запретить неявный каст его значений в целочисленные типы.
Всё же, как будет выглядеть шаблон, который добавляет произвольному классу сериализацию? Ну?
Как будет выглядеть метакласс, который добавляет произвольному классу сериализацию? Никак, ведь все варианты не учтешь. То же самое и с шаблоном.
Повторяю вопрос: почему я в процедурном языке программирования не имею права на долбаный процедурный метаязык?
Я написал это раза три и повторю в четвертый: автоматическая генерация кода полезна лишь в очень узком диапазоне случаев, которые еще и тривиальны в реализации. Как только дело доходит до чего-то нетривиального, часть автогенерируемых методов не подходят.
Отошли от стандарта, вай-вай!
ознакомьтесь. Для справки: gcc и clang уже несколько месяцев имеют 100%-ую поддержку с++17iCpu
31.07.2017 12:13enum class в общем и целом придуман сугубо для того, чтобы запретить неявный каст его значений в целочисленные типы.
Отговорка.template <class Type> QDataStream & operator<< (QDataStream & stream, const Type & t) { return (stream << static_cast<typename std::underlying_type<Type>::type>(t)); } template <class Type> QDataStream & operator>>(QDataStream & stream, Type & t) { typename std::underlying_type<Type>::type temp; stream >> temp; t = static_cast<Type>(t); return stream; }
Пример.Antervis
31.07.2017 19:28а вдруг надо было сериализовать эти enum'ы текстовыми значениями?
Вот пример: попробуйте написать метакласс-сериализатор для классов: array, list, vector, tuple, string, pair, shared_ptr, optional и variant. С одним условием: нельзя использовать std::is_same и концепт Same. Надеюсь, где-то на 5% до вас дойдет что же я имел в виду.iCpu
31.07.2017 20:15+1А вдруг вам нужно посрать, а у вас трусы не спущены?
Мне интересно взглянуть на гения чистой красоты, который додумался сериализовать tuple, да и array тоже. Вас в них ничего не смущает? Нет?
Подсказка: анонимная, этапа компиляции, фиксированная. Нет? Не звонит колокольчик?
Но даже если такие господа есть, кто наложил такое ограничение?Antervis
31.07.2017 20:46ограничение тривиально. Существует тьма различных (но в то же время практически одинаковых) реализаций списка, вектора, кортежа и массива, вы всё равно не сможете перечислить их все. Метапрограммирование занимаете уходом от частного к общему. Продемонстрируйте же, как вы это делаете (спускаете трусы)
Подсказка: анонимная, этапа компиляции, фиксированная. Нет? Не звонит колокольчик?
еще раз. Вы сами сказали, что понадобится ОДИН метакласс serializable.0xd34df00d
31.07.2017 20:59+1Тот самый один метакласс нужен в тех случаях, когда ничего специального не нужно.
Вам никто не мешает написать свои собственные методы сериализации, если возникнет такая нужда, и весь остальной код продолжит работать как раньше (возможно, с перекомпиляцией).Antervis
02.08.2017 14:01Да даже не так. В принципе, метакласс может использоваться только для объявления методов класса, с реализацией, переложенной на пользователя. Примерно
так$Container MyContainer { $RandomAccessIterator iterator {}; }; // определение методов MyContainer begin(), end(), size(), ... // А также методов MyContainer::iterator ==, +(int), ++, --, -(int), *,...
0xd34df00d
03.08.2017 01:14А что такое комбинированный метакласс? :)
Я-то хотел бы видеть механизм, аналогичный то ли хаскелевским дженерикам, то ли Template Haskell. Когда вы можете написать
{-# LANGUAGE DeriveGeneric, DeriveAnyClass #-} import qualified Data.Aeson as A import GHC.Generics data MyComplexData = ... deriving (Generic, A.ToJSON, A.FromJSON)
Надо будет что-то более продвинутое — напишете ручками, в конце концов.
iCpu
03.08.2017 06:42+1Что до определения методов — не стоит даже пытаться, слишком много условностей.
Как будто голос из глубин 90-х! «Что до параметризации алгоритмов — не стоит даже пытаться, слишком много условностей. Даже в примере Страуструпа с quick_sort зачем-то добавлены макросы, хотя логики в этом нет.»
Antervis, вы привязываетесь к мелочам, не пытаясь увидеть главного. За деревьями леса не видите, как говорится. Вам дают синтаксис для определения поведения по умолчанию. Если сейчас вам дают написать class MyClass { MyClass() = default; };, то предполагается дать вам право определять это самое default. Именно в этом заключается предложение. Будет его синтаксис более процедурный или шаблонный — это уже четвёртый вопрос. А вы носитесь, как маленький мальчик «А у него там точка стоит!»
а как сделать комбинированный метакласс? Или там этого не было?
Было. Пункт 2.5. Предлагается вводить примерно так же, как сейчас со множественным наследованием. Слева направо, по очереди. Если есть конфликт — вываливаться на попытке использовать код с конфликтом или на попытке каста в каждый из базовых типов при компиляции. ($D:A,B,C{}; class d :$D{}; assert(d.is(A) && d.is(B) && d.is( C), «Metaclass composition conflict.») Учитывая, что на множественном наследовании это работает, нет причин паниковать.
iCpu
31.07.2017 21:19+1еще раз. Вы сами сказали, что понадобится ОДИН метакласс serializable.
Промотал назад, подумал, что меня переглючило. Нет, не меня.
Метапрограммирование занимаете уходом от частного к общему.
Лол, нет. Метопрограммирование занимается не индукцией, а генерацией. Не разработкой, а штамповкой. Есть разница между добавлением методов сериализации к кортежу и к 100500 структурам из тривиальных типов.
0xd34df00d, tuple, array и optional параметризуются своими типами на этапе раскрытия шаблонов. На выходе мы будем иметь структуру с методами доступа по индексу элемента, статический массив на стеке и структуру с флагом (в статической или динамической памяти). Если optional удобен, а array допустим, то идея хранения в tuple данных под сериализацию не особо ясна. Но кто я такой, чтобы судить? Важно то, что мы получим те же структуры, только через постель. Так в чём проблема? Только в интерпретации и привычках.0xd34df00d
31.07.2017 21:24На выходе мы будем иметь структуру с методами доступа по индексу элемента, статический массив на стеке и структуру с флагом (в статической или динамической памяти).
На выходе чего именно? Метакласса? Какая разница, если сериализовать/десериализовать он будет корректно, а о типобезопасности позаботится тайпчекер?
Если optional удобен, а array допустим, то идея хранения в tuple данных под сериализацию не особо ясна.
Что именно вы считаете неясным? Мне лично не нравится только анонимность туплов и, как следствие, эквивалентность тупла(Double, Double, Double)
, представляющего RGB-цвет и тупла(Double, Double, Double)
, представляющего координату в трехмерном пространстве.iCpu
31.07.2017 22:36+1На выходе чего именно? Метакласса? Какая разница, если сериализовать/десериализовать он будет корректно, а о типобезопасности позаботится тайпчекер?
Пёс знает, у нас ещё нет даже программы обещаний, только общее «а ведь неплохо было бы», а мы уже шкуру делим. Глупо это.
Что именно вы считаете неясным?
анонимность туплов и, как следствие, эквивалентность тупла (Double, Double, Double), представляющего RGB-цвет и тупла (Double, Double, Double), представляющего координату в трехмерном пространстве.
Сами же ответили. Туплы имеют смысл для сохранения временных данных в некотором куске алгоритма. Ну, например, промежуточные расчёты перехода с декартовых координат проекта в эллиптические спутников GPS\Глонасс. Они используются два раза: в первый и в последний. А когда мы храним те же цвета, последнее дело делать их анонимными. Должны быть именованные структуры. Если мы, конечно, не желаем быть проклятыми нашими последователями.
anz
28.07.2017 17:15Круто будет если примут. Использовал мета-классы (точнее мета-таблицы) в lua — это очень крутая штука в языке, позволяющая расширять возможности языка практически неограниченно. В общем, держу пальцы крестиком
fsmoke
29.07.2017 15:40+1Очень годное расширение. Мне, как олдовому крестовику, оно очевидно и понятно и приятно. Но есть один нюанс — уже сейчас ко мне приходят выпускники, которым тяжело начинать писать на крестах для энтерпрайза с учетом всех нововведений в язык. Мдя… язык был и так сложным, а становится с каждым годом всё сложнее — порог вхождения для неокрепших студенческих мозгов всё выше… как бы не скатится к тому, что через н лет люди просто будут пугаться влезать во всё это — типа «Язык программирования С++ в 10 томах. Том первый».
ПС
Уже много лет грызет меня эта мысль, что вот так вжух и в один прекрасный момент не смогу найти годных сотрудников крестовиков. Прям фобия.devalone
29.07.2017 21:25+3Ну хз, как по мне в C++11 многие вещи стали проще, те же смарт поинтеры, например. Другой вопрос в том, что вводя новые фичи не выпиливают старые, как например с синтаксисом enum class для обратной совместимости.
VioletGiraffe
OMG
То есть:
а) теперь надо знать этот мета-язык, который уже отдаляется от С++;
б) любой библиотеко-писатель сможет под себя таких правил наворотить, что знание всех деталей стандарта собственно языка С++ уже не поможет разобраться, что же делает (и даже что означает) этот код.
Я не против нововведений, но это как-то странно.
alexeykuzmin0
Да это же просто reflection времени компиляции, как во многих других языках. Не должно случиться ничего страшного
mayorovp
Больше похоже на кодогенерацию.
VioletGiraffe
Больше похоже на генерацию нового языка.
Vadem
Обычное метапрограммирование же. Как в Lisp или Nemerle.
0xd34df00d
Интересно, будет ли этот пропозал по мощности аналогичен template haskell?
Думаю, что нет.OlegMax
Хорошее вступление для постапокалиптического фильма-боевика.
Vjatcheslav3345
Видеоряд во вступлении должен иметь на правой части седого, морщинистого генерала или адмирала, на фоне тихоокеанского заката, со скептическим прищуром выслушивающего эти заверения, исходящие от бывшего аспирантика (слева) с факультета искусственного интеллекта боевых робототехнических систем...