Комментарии 362
C++ он и есть от того С++, что к языку С добавили эти самые ++. А если хотите чистые ++, то тогда тут нужен другой язык.
Вот вам другое мнение для размышления: почему язык C++ развивается не согласованно со стандартами языка С? Как это подразумевалось при его создании чтобы расширить возможности С. А рождает вместо этого противоречивые костыли.
Этот язык программирования появился в начале 80-х годов ХХ столетия. Придумал его Бьерн Страуструп. Создавался С++ на базе существовавшего тогда С. Первоначально, Страуструп назвал новый язык «С с классами», так как в нем были реализованы классы. Помимо этого, в новый язык программирования были добавлены виртуальные функции, ссылки, константы и многое другое. Также здесь появился новый стиль комментирования (привычные многим //). В результате получившийся язык программирования перестал быть дополнением в С и стал самостоятельным.
С развивается?..
Вполне себе развивается, можете будущий C23 стандарт глянуть
там практически ничего не добавляется, а компилируется С++ код с С99 или чем нибудь таким
компилируется С++ код с С99 или чем нибудь таким
Не до конца понял что с чем компилируется. Если вы имели ввиду компилить C++ код под стандарт C99, то далеко не всякий C++ код будет компилиться таким образом.
там практически ничего не добавляется
Никто и не ожидает от небольшого низкоуровневого языка кардинальных изменений.
До стандарта C++17 совместимым с C++ стандартом был C98. Не C99.
Начиная со стандарта C++17 совместимым с C++ стандартом стал C11.
Стандарт C, при этом, и обновляется, и улучшается. Многие спорные практики из ранних стандартов C сейчас или утратили силу, или получили титул опциональных.
Зачем чинить инструмент, который не сломан? Как исправлять инструмент, который достаточно эффективно покрывает задачи своего применения?
С++ который лишён недостатков уже придумали.
Это Java(+Graalvm).
И да, union был в С не для того что написано в вашем примере, а для работы с аппаратурой, битовыми полями. Особенно нестандартных аппаратных приблуд, в которых половина слова биты значений другая половина -- короткие поля значений. Для того чтобы расписать регистр PSW на чтение/запись. И всё это без геморроя с масками и сдвигами.
Hidden text
На С/С++ с 1989 года, на Java 11/Graalvm - c 2020, и назад на плюсы ни ногой.
java это один большой недостаток, язык завязанный гвоздями на Object и ооп, которое устарело. Громадная куча бойлерплейта с плохой производительностью, не вижу смыла в 2022 году ориентироваться на это
В 2022 году не принято делать наброс без цифр, альтернатив и сравнений.
Чем вам помешал Object и чем плох ооп? Какие альтернативы?
Ну и на всякий случай, посчитайте свою зарплату за год, посчитайте сколько за нее можно купить на год железа в облаке и подумайте где на самом деле "тормоза" и стоимость
Ну, Object плох тем, что с ним ООП неполноценное :-) "Наследование убивает инкапсуляцию", как учит нас GoF, криво цитируя чужую статью :-)
задач, на которые объекты ложатся хорошо, мало.
Задач, которые хорошо ложатся на ФП, в реальном мире ещё меньше. И, что намного хуже, людей, на мозг которых нормально ложится ФП, вот прям совсем мало. Можете сравнить число резюме на С++/С#/Java/PHP и на вашем хаскеле + F#. А лидируют, внезапно, javascript и python, где большинство кандидатов пишут в старом-добром императивном стиле "пойди туда, возьми то", без всяких извратов с ООП и ФП...
Объекты прекрасно компонуются. Они для того и были придуманы - декомпозиция кода на объекты, чтобы компоновать уже их.
Абстракции для того и нужны, чтобы скрывать несущественные детали, в частности эффекты.
Книжек про монады, моноиды, функторы, эндофункторы и прочие фп паттерны - не меньше.
Было бы странно для компоновки придумывать абстракцию, для этого не годящуюся. Или вы считаете всех вокруг совсем идиотами?
Далеко не все эффекты являются существенными, не врите.
Что такое "уровень GoF"? Вы не поверите, но ООП паттерны - это тоже не более чем интерфейсы с правилами.
Спорить про удачность можно долго, но утверждать о некомпонуемости объектов - это либо дилетантизм, либо троллинг. Ни то, ни другое вас не красит.
Не получается, так как те два метода будут вызваны автоматически.
Куча непонятных символов. Можно было хотя бы словестно написать
Для развития педагогических навыков — какие символы тут непонятны?
Мне понятно, но подозреваю, что средний сишник может оказаться не готов к такому способу указания типов.
Открою страшную тайну, но стрелочки много где встречаются. Даже в математике. Выражение типа M a → (a → M b) → M b
вполне можно прочитать в логике предикатов, например, где они означают импликацию, и которая сишнику, по крайней мере теоретически, значительно ближе. Хотя он, конечно, прочитает какое-то странное "разыменование указателя".
Тут есть ещё другая проблема. Когда я вижу стрелочки и кванторы в математическом тексте - они читаются на вскидку. А когда в коде на незнакомом языке, это вызывает вопрос: "Чёрт возьми, что они в этом языке значат?!!"
Кроме того, символы, не вводимые с клавиатуры, - это триггер "Осторожно! Маргиналы!"
Это когда каждый пример разжёвывается на десяток страниц с кучей кода, мотивацией, и так далее.
Тут нужно бы делать скидку на уровень аудитории, для которой это все разжевывается.
Много слов нужно не потому, что ООП сложное или паттерны проектирования мудрены. А потому, что большая часть целевой аудитории таких книг, грубо говоря, виртуальный деструктор уяснить для себя не может.
отсылки к достаточно специфичным вещам, от оконных систем до всяких парсеров-компиляторов.
Что такого специфического, например, в оконных системах?
Виртуальный деструктор большинство из них при этом осилило.
Судя по тому, что вы пишете, вы только подтверждаете описанное в моем комментарии.
Design pattern это библиотека, которую невозможно написать, потому что язык не позволяет (c) не_мой
Про компоновку объектов можете почитать в этой статье. Там же есть целый параграф с описанием реактивного фабричного метода и других паттернов, которых вы не найдёте в GoF.
Можно аналогичные правила про фабричный метод, обсервер и адаптер?
А зачем? :) Мне вот обычному смертному и без этих правил удобно ими пользоваться.
Вот монады, хоть тыщу раз имеют эти правила, я так и не понял зачем нужны. Я уж не говорю о том, что монадой по итогу оказывается куча разных сущностей и их монадическая природа ваще не очевидна на первый взгляд. В то время как фабричный метод я объясню ребенку за 10 минут.
Это когда каждый пример разжёвывается на десяток страниц с кучей кода, мотивацией, и так далее.
Сколько статей про монады было на Хабре, но мир их так и не понял.
Как понять, когда у меня адаптер, а когда — бридж?
А зачем?
Как понять, когда фабричный метод применим?
Как на счёт "когда захочется"? :) С тем же успехом я могу спросить "когда применим нож?". Ответ: во многих ситуациях, в основном когда надо что-то порезать, но вообще можно и гвоздь забить, если сильно прижмёт.
Для того, чтобы один раз написать кучу разных абстракций.
Описание правильной применимости которых точно также займёт по целой книге. Просто никто эти книги не пишет. Не потому, что они не нужны, а просто потому, что ФП как таковое никто не использует.
Потому что их в основном пишут те, кто недавно открыл для себя теоркат и думает, что понял теоркатовое обоснование
Если уж те, кто что-то прочитал в теоркате, и получается что-то знают, не могут объяснить нормально (значит ничего сами не поняли), то я вижу очевидное противоречие, что монады и их применение - это просто.
Но вам же не нужно знать особенности теории рекурсивных функций, чтобы вполне себе писать на обычных императивных языках?
Как и вам не нужно знать паттернов ООП, чтобы успешно писать на ООП языках) Я вот вообще про существование этих паттернов узнал через два года коммерческой разработки. Как оказалось, 80% из них я просто переизобрел, а дизайн некоторых языков просто подталкивал ими пользоваться.
Можно пример книжек про монады, моноиды и эндофункторы примерно того же уровня, что GoF?
Пусть и не книжка и пусть наверно не настолько глубоко, но вот
https://stepik.org/course/693/promo
целый курс по монадам
Ага, поэтому по тому, как их компоновать, целые книжки пишут, и потом обсуждают, что за SOLID такой, что за билдер, что за визиторДа, и это всё из-за того, что сейчас заканчивается первая стадия ООП в духе С++ (слабо-инкапсулированные сильно-связанные объекты).
Зачем объекты для инкапсуляции состояния, когда состояние отлично инкапсулируется в виде глупых и не имеющих собственного поведения структур данных (у которых просто не экспортируются кишки), плюс функций, объявленных в том же модуле?и задам вопрос: что по вашему значит «собственное поведение» в этом контексте?
состояние отлично инкапсулируется в виде глупых и не имеющих собственного поведения структур данных (у которых просто не экспортируются кишки), плюс функций, объявленных в том же модуле?
Так это и есть объекты. Данные + методы. Разве что без ключевого слова class.
Как-то ваш синтез очень на процедурное программирование смахивает.
несущественные детали, в частности эффекты
После этих слов в ФПшном поезде начался сущий кошмар.
Потому что надо продукт делать, а не размышлять, какие стандарты и паттерны лучше или хуже. На пустых словах деньги бизнес не зарабатывает)
На лидирующем javascript как раз таки очень любят писать в функциональном стиле.
Ага, эмулируя объекты через грязные хуки.
Вам не нравится union - ну так получите вместо него принцип "всё является объектом".
К тому же, я так и не понял, в чем проблема с ООП и виртуальными методами
в том что они не нужны, реализуются на С++ удобно без ключевого слова virtual / override
На гитхабе с реализацией из статьи также описано какие проблемы уходят
Напишите пожалуйста как удобно без вирутальных методов реализовать нечто подобное
struct GraphicObject
{
virtual void draw() = 0;
};
std::vector<GraphicObject*> graphic_items;
....
for (auto item : graphic_items)
item->draw();
Думаю будет выглядеть как-то так
template <typename T>
struct GraphicObject {
static void do_invoke(const T& self) {
self.draw();
}
};
struct Circle {
...
void draw() {...}
};
struct Rectangle {
...
void draw() {...}
};
std::vector<aa::any_with<GraphicObject>> graphic_items;
graphic_items.push_back(Circle{});
graphic_items.push_back(Rectangle{});
for (auto& item : graphic_items) {
aa::invoke<GraphicObject>(item);
}
Но насколько это удобно, от меня пока ускользает. Особенно если нужно иметь более одного метода в интерфейсе. Например, GraphicObject::move.
Интересно конечно, но на каждый метод создавать класс обёртку это вообще не удобно, также какие проблемы уходят при этом решении, по-моему их становится только больше, получается если код практически полностью полагается на вызовы виртуальных методов везде придётся писать aa::invoke<method class>? К тому же судя по реализации any_with он работает только с movable классами. И давайте ещё немного немного усложним задачу.
struct Widget : GraphicObject
{
virtual void draw() override
{
...
}
};
struct Label : Widget
{
virtual void draw() override
{
Widget::draw();
...
}
};
draw можно будет поменять на event например, будет более логично.
в чём усложнение то задачи?
Из кода уходит явный контроль над памятью, наследования, невозможно сделать отсутствие виртуального деструктора и слайсинг, можно переиспользовать код в НЕ полиморфном контексте без каких либо проблем, можно делать дефолтные реализации методов свободными функциями на статическом полиморфизме... И многое другое
в чём усложнение то задачи?
Вызов метода предка.
По остальному, это всё и так есть, достаточно просто не использовать виртуальные методы, вы назовите какие проблемы уходят если убрать виртуальные методы. Потому что без виртуальных методов можно и сейчас писать, но если захочется полиморфизма придётся писать огромную кучу классов обёрток, при этом что делать если нужно лишь частично переписать класс предка, а часть реализаций оставить тех что были, копипастить их? А если требуется вызов метода предка + добавление своей реализации как в усложнённом примере что я кинул? То что вы предлагаете, будет лишь раздувать код в сотни раз, а больше кода больше ошибок, я уж молчу о всяких vasssist ах и решарперах они станут совсем бесполезными в данном случае, а ошибки компиляции будут ммм загляденье, время компиляции тоже наверно скажет спасибо.
move не нужно будет иметь потому что есть aa::move, то есть это ведёт себя как объект, а не как указатель на объект.
Так что будете писать a = b; а не a=b->copy() и прочая муть.
Методов может хоть 2 хоть 102, просто указываете. По неймингу понятно, что вы не совсем поняли суть, GraphicObject это не интерфейс целиком, а лишь один метод, из множества методов можно собрать интерфейс
Так что назовите его скажем Draw
и потом
using any_drawable = aa::any_with<Draw, aa::move, aa::copy>;(что вам там нужно то и берите)
Ну и в examples там показано, что можно в методы добавлять сразу интерфейс, чтобы писать не aa::invoke<Draw>(v), а v.draw();
move не нужно будет иметь потому что есть aa::move, то есть это ведёт себя как объект, а не как указатель на объект.
Да только там (судя по реализации) на каждый объект происходит по сути placment new и вызывается конструктор копирования\перемещения а если ещё и стандартного размера буфера не хватит то и выделение памяти для каждого any_with.
что вам там нужно то и берите
И на каждый писать класс обёртку? Отлично.
Ну и в examples там показано, что можно в методы добавлять сразу интерфейс, чтобы писать не aa::invoke<Draw>(v), а v.draw();
Да посмотрел, дикое извращение лишь бы не использовать vtable. Да и там по сути есть vtable только своя реализация.
Вам напомнить, что при использовании виртуальных функций обычно все объекты выделяются на куче? Тут у вас не будет выделения на куче в большинстве случаев, ведь есть оптимизация хранения, то есть аллокаций меньше, а не больше
И на каждый писать класс обёртку? Отлично.
Не на каждый класс, а на каждый полиморфный метод, при этом методы можно переиспользовать при создании других полиморфных интерфейсов
Да посмотрел, дикое извращение лишь бы не использовать vtable
Нигде не было заявлений об отсутствии vtable или о том, что я пытался её не использовать. Суть в отказе от ключевого слова virtual и проблем которые оно приносит
Вам напомнить, что при использовании виртуальных функций обычно все объекты выделяются на куче? Тут у вас не будет выделения на куче в большинстве случаев, ведь есть оптимизация хранения, то есть аллокаций меньше, а не больше
Вызов конструктора копирования\перемещения никуда при этом не девается. Всё равно оверхед больше чем при копировании указателя.
Не на каждый класс, а на каждый полиморфный метод, при этом методы можно переиспользовать при создании других полиморфных интерфейсов
Я это и имел ввиду, вы же понимаете насколько это неудобно?
Суть в отказе от ключевого слова virtual и проблем которые оно приносит
Честно говоря за 12 лет работы, ни разу не сталкивался с проблемами из-за virtual при том что работаю с графикой
вы можете сделать методы, которые будут принимать указатель и поставить размер буфера sizeof(void*), таким образом у вас никогда не будет аллокаций и копирование / мув будут происходить для указателя
Плюс в библиотеке ещё не документировано есть polymorphic_ptr<Methods...>, который отделяет полиморфность от типа и практически аналогичен void* для копирования(и принимания в интерфейс соответственно как в случае с виртуальными функциями)
Под GraphicObject::move имелся ввиду просто метод перемещения графического объекта, с move-семантикой это никак не связано.
А за разъяснение спасибо. Идея в целом понятна, просто городить для каждой функции отдельный класс со статическим методом многословно и не очень удобно.
На мой взгляд тему реализации динамического полиморфизма с семантикой значений хорошо раскрыл Louis Dionne в своей библиотеке https://github.com/ldionne/dyno. Но там тоже, без средств вменяемой кодогенрации, получается жутковато.
Получается вы создали псевдотаблицу виртуальных методов.
Самое интересное в оригинальных виртуальных методах это вызов базовым классом виртуального метода наследника.
Причём класс наследника может быть реализован позже разработки самого базового класса.
class Base {
public:
virtual void foo() { std::cout << "Base::foo" << std::endl; };
void bar() { std::cout << "Base::bar" << std::endl; }
void baz() { foo(); bar(); }
};
class AB : public Base {
public:
void foo() { std::cout << "AB::foo" << std::endl; }
void bar() { std::cout << "AB::bar" << std::endl; }
};
class DAB : public AB {
public:
void foo() { std::cout << "DAB::foo" << std::endl; }
void bar() { std::cout << "DAB::bar" << std::endl; }
};
int main( int argc, char * argv[], char * env ) {
DAB tAB;
tAB.baz();
Сейчас мой код обработки матриц на Java работает быстрее чем идентичная по коду реализация на C. Это факт. Не в разы, но на 10-15%. Оптимизатор работает лучше, ему не надо подсказывать или писать ассемблерный код.
А плохая производительность не из за Java, а из за навороченных библиотек не заточенных на производительность, но заточенных на предсказуемость и широчайший функционал.
Тут стоит заметить, что Ваши матрицы не единственное мерило. Расскажите как на jvm для linux в одном бинарнике сделать менеджер управляющий своими воркерам(процессы) с тредами, с возможностью оперативно уменьшать/увеличивать кол-во воркеров и с релоадом без разрыва соединений, на манер как это реализовано в nginx например. Отвечу сразу - никак, кроме запуска пачки jvm.
Вообще-же jvm в качестве ОС (а не поверх ОС) - весьма интересное решение.
Легко... Это называется GraalVM делаем бинарник, можно вообще в posix напрямую ходить. А можно ещё взять кваркус и vert.x под капотом и добавить реактивщины на все это, с воркерами, эвент лупами и прочим...
JVM в качестве ось - это глупость, а вот хождение прямо в ось(да даже в регистры процессора) уже реальность
Неа, не получится. В graalvm:
Directly using process related syscalls like clone, fork, vfork, etc. is not supported.
The exec function family is not supported.
Ну и про глупость - java me squawk, намекают как-бы, ну и есть те кто неплохо на этом зарабатывают: https://microej.com/
В graalVM вы можете на прямую ходить в Линукс и делать что хотите. POSIX доступен, ровно как на сях. Просто в доках этого нет ещё граалевских.
Просто повторюсь, я спокойно ддергал mmap и управлял регистрами прямо из java и Грааль.
То о чем вы говорите - это netty+vert.x+quarkus.... Просто никому не интересно 2й nginx писать... Зачем?
Вот как в graalvm обойдёте явно указанные ограничения, тогда и поговорим о доступности POSIX "ровно как на сях".
И с чего это никому не интересно писать второй nginx - куда ни посмотри, а на любом популярном языке не меньше десятка реализаций web-сервера или прокси или фреймворка.
А давайте поговорим...
import org.graalvm.nativeimage.c.function.CFunction;
import org.graalvm.nativeimage.c.function.CLibrary;
@CLibrary("c")
public class Sample {
@CFunction(transition = CFunction.Transition.NO_TRANSITION)
private static native int fork();
public static void main(String[] args) {
int result = fork();
System.out.println("result: " + result);
while(true) {
try {
Thread.sleep(1000);
System.out.println("process result = " + result);
} catch(Exception e) {}
}
}
}
Компиляем, запускаем
javac Sample.java
native-image Sample
./sample
и видим:
result: 533
result: 0
process result = 533
process result = 0
process result = 533
process result = 0
process result = 0
process result = 533
process result = 0
process result = 533
process result = 533
Заглядываем в top и видим:
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 892 484 416 S 0.0 0.0 0:00.13 init
8 root 20 0 892 88 20 S 0.0 0.0 0:00.00 init
9 root 20 0 892 88 20 S 0.0 0.0 0:00.12 init
10 sanders 20 0 10268 5384 3420 S 0.0 0.0 0:00.42 bash
531 sanders 20 0 98932 8860 5492 S 0.0 0.1 0:00.09 sample
533 sanders 20 0 98932 4320 936 S 0.0 0.0 0:00.00 sample
534 root 20 0 892 88 20 S 0.0 0.0 0:00.00 init
535 root 20 0 892 88 20 S 0.0 0.0 0:00.00 init
536 sanders 20 0 10032 5164 3424 S 0.0 0.0 0:00.09 bash
549 sanders 20 0 10868 3664 3152 R 0.0 0.0 0:00.02 top
Видимо из java сделать таки можно.
Соответственно ваше утверждение - является ложным
Update: Судя по всему вы в доки по llvm смотрели, а не про native компиляцию, отсюда и не верные выводы.
Да я смотрел доки по llvm. И да native-image позволяет сделать форк, оказывается в 2019 году свешилось, алилуя!
Однако:
$ time /usr/lib/jvm/java-16-graalvm/bin/javac Sample.java && time /usr/lib/jvm/java-16-graalvm/bin/native-image Sample
real 0m1.084s
user 0m1.730s
sys 0m0.191s
[sample:1115840] classlist: 1,441.24 ms, 0.96 GB
[sample:1115840] (cap): 840.95 ms, 0.96 GB
[sample:1115840] setup: 3,438.19 ms, 0.96 GB
[sample:1115840] (clinit): 374.36 ms, 1.74 GB
[sample:1115840] (typeflow): 6,396.07 ms, 1.74 GB
[sample:1115840] (objects): 5,129.39 ms, 1.74 GB
[sample:1115840] (features): 488.59 ms, 1.74 GB
[sample:1115840] analysis: 12,691.96 ms, 1.74 GB
[sample:1115840] universe: 1,009.62 ms, 1.74 GB
[sample:1115840] (parse): 868.96 ms, 1.77 GB
[sample:1115840] (inline): 1,460.33 ms, 2.33 GB
[sample:1115840] (compile): 13,060.04 ms, 2.38 GB
[sample:1115840] compile: 16,215.19 ms, 2.38 GB
[sample:1115840] image: 2,244.14 ms, 2.38 GB
[sample:1115840] write: 317.16 ms, 2.38 GB
[sample:1115840] [total]: 37,670.41 ms, 2.38 GB
# Printing build artifacts to: /home/rnz/experiments/graalvm/sample.build_artifacts.txt
real 0m38.611s
user 3m44.714s
sys 0m4.161
s
$ du -hs --apparent-size ./sample
11M ./sample
Тоже самое на С/С++:
$ cat sample.cpp
#include <iostream>
#include <chrono>
#include <thread>
#include <unistd.h>
using std::cout;
using std::endl;
int main()
{
pid_t rc;
rc = fork();
if (rc == 0) {
cout << "parent: " << getppid() << endl;
cout << "child: " << getpid() << endl;
}
while (true) {
std::this_thread::sleep_for((std::chrono::seconds(2)));
if (rc == 0) {
cout << "child: " << getpid() << endl;
}
}
return 0;
}
$ time g++ -o cppsample sample.cpp
real 0m0.525s
user 0m0.463s
sys 0m0.060s
$ du -hs --apparent-size cppsample
23K cppsample
$ strip cppsample && du -hs --apparent-size cppsample
15K cppsample
Ну и потреблянство сравним тоже:
$ pmap -x $(pgrep sample) | egrep 'Address|total'
Address Kbytes RSS Dirty Mode Mapping
total kB 21488 6712 1320
Address Kbytes RSS Dirty Mode Mapping
total kB 21488 4764 1320
$ pmap -x $(pgrep cppsample) | egrep 'Address|total'
Address Kbytes RSS Dirty Mode Mapping
total kB 5976 3060 192
Address Kbytes RSS Dirty Mode Mapping
total kB 5976 1804 200
В целом неплохо, на мышах. Но ждать ~4 минуты сборки для минимального приложения, да ещё ~2,4G ОЗУ отъело - так себе удовольствие (на Intel i5-8250U). Ну и судя по доке у native-image тоже хватает ограничений, типа невозможности подгрузить динамические штуки, есть какие-то проблемы с сериализацией, ну и если по коду попадаются какие-то особенные функции и методы, то всё равно надо положить рядом jvm.
Но ждать ~4 минуты сборки для минимального приложения
Ну давайте ближе к реальности. 4 минуты надо собирать в финале. Гонять можно в JVM. А так да, native сборка - это можно Войну и Мир прочитать.
Ну и судя по доке у native-image тоже хватает ограничений, типа невозможности подгрузить динамические штуки
Не не возможности подгрузить, а сделать дополнительное приседание. Которое кстати уже делается самим граалем через agent и генерирует эти приседания.
есть какие-то проблемы с сериализацией
Да нету их. Даже в 22.1 сериализацию лямбд прикрутили. Сейчас вот с ребятами из редхата(который Quarkus) делаем профайлер для реактивных приложений с разворачиванием лямд, эвент-лупов, воркинг пулов и всех методов в них.
ну и если по коду попадаются какие-то особенные функции и методы, то всё равно надо положить рядом jvm
Зачем? Собираете с флагом -static или -staticWithLibC и счастье вам.
Вообще, если костылей не хотите, то Quarkus вам в помощь. Туда уже многое затащили. По зависимостям, если не изменяет склероз - больше 1500 артифактов приходят. Буквально на прошлой неделе Алексей мерял.
Странно предъявлять языку, в основе которого лежит платформонезависимость - неумение работать напрямую с платформой.
При чем специально вводите доп. ограничения. Потому что сами заранее знаете, что просто с задачей "сделать, чтоб работало так же" язык справится, а ваша цель доказать, что язык фуфло, для чего вводите искусственные ограничения.
Уже снимают это ограничение. Java = Linux в современном мире в подавляющем количестве случаев. Хочешь платформонезависимость - она есть. Хочешь под платформу - тоже есть. Чуть выше можно увидеть пример в моем комментарии в ответ "что якобы нельзя, но можно"
Не "хочешь", а надо или не надо.
Вот "платформонезависимость" - не надо. Своей тонны java кода нет, что-бы пытаться не переписывая получить профит в производительности и эффективности. А начинать писать на java, то что можно эффективнее и оптимальнее реализовать на C/C++ - лишено смысла, кроме как в случае, если вокруг одни java-программисты и ты один со своим cpp.
Вот "платформонезависимость" - не надо.
Поясните мысль. В моей картине мира - вся "платформонезависимость" java - это Idea от JetBrains. Больше как на Linux серверах я яву даже близко последние лет 10 не вижу. То есть "платформонезависимость" - не более чем "мем из 90х". "платформонезависимость" есть, явы там нет.
А начинать писать на java, то что можно эффективнее и оптимальнее реализовать на C/C++
Я тут ниже начал тему раскрывать. Главный вопрос - "а что есть эффективность и оптимальность"? Я с огромным уважением отношусь к Джону Кармаку и его годам оптимизаций в ID. Но зачастую этот "эффективность" = "выдать быстрее заказчику" и "оптимальность" = "сделать дешевле". Иначе вы просто пролетите в тендере. Нахрен вы никому не нужны, если готовы за 5 лет круто, а не за год.
Далее - разговоры C++ или Java по моему вообще бессмысленные. Начнем с того, а есть ли у вас вообще java программисты, чтобы на ней писать (для С это тоже верно). Нет, так чего тут разговаривать.
А дальше мы начинаем упираться в задачу... Что лучше, ради "оптимальности" плодить дикие связки C++/JNI/Java или все же сделать на одном языке. Железо ныне сильно дешевле труда программиста, его можно увеличить в 2 раза часто за зарплату одного из них.
Короче говоря - борьба С++ или Ява (как и многое другое) это борьба остроконечников и тупоконечников, как в известной книге. Первична задача. Дальше классический треугольник: быстро-дешево-качественно. Наличие специалистов, риски, цена, сроки, стоимость и сложность багофикса и поддержки. А только потом средства его достижения, как раз где и находится язык.
Больше как на Linux серверах я яву даже близко последние лет 10 не вижу.
Андроид тихо засопел в уголке.
Слота то я и не приметил... Или забыл...
Правда не изучал онное, но если не изменяет склероз, то там по моему с 1.8 ещё не выбрались...
Кстати, разговаривал с соседями, это шишки SW в Samsung/Broadcom/Qualcomm - массово софт на golang с плюсов переводят.... Язык конечно хорошо, но стоимость разработки важнее
Про "плаформонезависимость" - непонял о чём вы, но я имел ввиду, "способность" програм для jvm работать на любой ОС где работает jvm без изменений под конкретнуюю ОС, на практие оно только на десктопах и годится.
Про эффективность, я вам уже привёл пример: ScyllaDB.
Про "есть/нет программисты" - неверно.
Есть бюджет, что-бы:
а) нанять программистов
б) оплатить эффективное решение.
Бюджетом можно распорядиться рационально, а можно разбазарить.
300 разрабов собравшихся вокруг биллинга - разбазаривание бюджета.
40 инстансов субд на java (в облаке-ли, on-premise-ли), там где справятся 3-5 инстансов субд на cpp - разбазаривание бюджета
И вопрос только в том, разберёт "заказчик музыки", что ему впарили "какофонию" или нет. Увы, но часто, выбор эффективного языка, вообще не стоит в начале, пишут не оценивая будущее TCO, в результате имеем кучу легаси с тремя сотнями разрабов плящущими вокруг и доказывающими что "хуяк-хуяк и в продакшин" это time-to-market.
Больше как на Linux серверах я яву даже близко последние лет 10 не вижу
посмотрел, из установленных пакетов у меня от jre зависят dbeaver и sweethome3d. пусть это и linux, но, всё-таки, десктоп.
из установленного не из репозитория, помимо idea, пользуюсь программками для управления kvm (подключение к видеовыходу удалённого сервера). в свежих серверах помимо java-приложения есть html5, но есть и старые сервера, и внешние kvm, где только через jnlp.
а ещё у нас кассиры на кассах работают с java-приложением. да, там тоже linux, но, судя по документации, работа под windows тоже поддерживается, как бэкенда, так и фронтенда.
это то, что пришло в голову за пять минут.
Во-первых, я предъявлял не языку, а jvm. Во-вторых, эта кроссплатформенность вообще не упёрлась.
И предлагая задачу, я намекал на то, что jvm не позволит сделать ряд вещей, хотя как оказалось с 2019 года есть возможность запилить native приложение на java (graalvm), однако выглядит это всё равно как срещивание ужа и ежа, и со значительным оверхедом, хотя в целом неплохо в сравнении c jvm.
В общем я о том, что когда нужна реальная производительность и эффективность, то выбрав java с jvm, можно долго ломать копья и всё равно не достич приемлемых показателей, и на этом фоне кроссплатформенность по типу jvm идёт лесом. Показательными примером является Cassandra vs ScyllaDB
Когда речь заходит про "реальную производительнось" не следует забывать про тип задач и количество программистов в нем. Если это какое то небольшое приложение - да, вы совершенно правы. А если это скажем риалтайм биллинг, который пишут 300 человек, то производительнось встаёт на 2й план после стоимости программистов. То есть своей задачи свое решение. Тут и производительность и time to market и главное цена. Одна и та же система, но сделанная "по дзену" но в 100 раз дороже никому на рынке не будет нужна
Та же система "по дзену", а не как обычно "кто во что горазд, а потом скручиваем изолентой" потребовала бы не более 30 человек, в 10 раз меньше времени на разработку и в 10 раз меньше серверов.
Назовите мне хоть одну IT компанию, у которой 30 сеньеров сидят ровно на 5й точки и ждут проекта такого объема на кассандре, вместо того чтобы не испытывать недостаток ресурсов, нанимать народ и прочее-прочее прочее. В идеальном придуманном мире - это возможно. В реальном - в крайне редких и исключительных случаях.
Извините, но кто мешает сделать JVM реализацию не для ОС а для железа? Никто не мешает. https://ru.wikipedia.org/wiki/JavaOS
Просто это очень неоптимально с точки зрения разработки -- малейшее изменение платформы и нужно переписывать JVM. С точки зрения программы на Java -- вообще ничего не поменяется, и ваши низкоуровневые задачи управления процессами и другими ресурсами будут отражены в абстракциях пакетов.
Да никто не мешает, пусть кому надо тот и делает. Мне не надо.
"малейшее изменение платформы и нужно переписывать JVM" - ну как-бы с ОС именно так. Ведь основная фишка jvm: абстрагировать, вируализировать, эмулировать - ну и логично тогда уже сразу вместо ОС применять jvm.
А её ещё попробуй напиши.
В том то и дело. Что на С нужно самому извращаться с платформой делая код на переносимом С оптимальным по производительности, тогда как на Java все извращения и оптимизации делает разработчик JVM.
И я не уверен что я это сделаю лучше, т.к. JVM лучше знает как расположить в памяти массивы, оптимизировать обращения с учётом кэширования и ещё 100500 зависимостей.
И да, я не фанат Java, и долго писал на С/С++ ассемблере, и считал что это самые быстрые реализации, но Graal меня удивил.
Чистая Java конечно медленнее чем С на 15-20%.
Код вычислительный без выделения памяти и классов.
Я сомневаюсь что ваши версии переплюнут тот же https://bitbucket.org/blaze-lib/blaze/src
Чтобы писать действительно быстрый код без знания платформы пока не обойтись.
Ну и стоимость JVM тоже забывать не надо, не всегда есть возможность или резон держать лишнюю память и прогревать виртуальную машинку.
Вы явно что-то не то на С написали. Покажите ваш код)
Не знаю как, но Graal оптимизирует такой код лучше чем С.
а давайте ради интереса проверим.
Скиньте полный класс для теста + как его компилять. А я перепишу "в лоб" на яву и компильну в бинарник под graal.
Соответственно обменяемся исходниками и проверим на разных конфигурациях.
https://github.com/Kotofay/JavaVsC
Test time : 1538
Result : -1.0, -2.696002453891236
Тут все ждали код на сях, а тут java :) завтра под Граалем запущу, интересно цифры будет в jvm vs native увидеть
Обновил
Test time : 1517
Result : -1.000000 -2.696002
Это в JVM режиме?
Майкрософт реализует clock_t
как long
, 32-разрядное целое число со знаком, а макрос CLOCKS_PER_SEC
определяется как 1000. Времена 18.2 cps давно прошли ;)
printf( "Test time : %lf\n", ( end - start ) / (double)CLOCKS_PER_SEC * 1000. );
Test time : 1535.000000
Result : -1.000000 -2.696002
Код на С упрощён, я не стал приводить реальную реализацию. И там присутствуют некоторые неприятные для С вещи.
массивы в Java проверяют выход за границы.
массивы переданные в функцию не имеют заранее известной размерности aka [ 32 ][ 32 ] и компилятор не может вставить константы смещений в код вычисления индекса как это делает компилятор С
подозреваю, что отсутствует выделение памяти как в clone() перед каждым вызовом, а не просто копирование memcpy
Думаю, что введение этих ограничений приведёт к падению производительности.
graal мне ставить лень, но
Чистая Java, как у вас, медленнее graalvm.
[...] которые jit вполне может вырезать.
Не может. Это нарушает контракт обращения к массиву по индексу.
Опции JVM: -XX:+UseG1GC
Если он может себе доказать, что индексов больше 31 не бывает
Я уже написал, это не реальный код а кусочек, и n там вполне меняется, как и размеры массивов.
Поэтому никто ничего заранее доказать не может.
Однако со всеми оптимизациями Java справляется с кодом не хуже С++. А если сделать код С++ исключительно надёжным то и превосходит.
А какой кодогенератор у вас там используется? clang-based или что ещё?
Clang:
Test time : 1237.000000
Result : -1.000000 -2.696002
[...] которые jit вполне может вырезать.
Добавил туда же реализацию на С++: std::vector, с проверкой на выход за границы и отсутствием явного размера:
PS: std::array не подходит, поскольку требует явного указания размера.
Отказываемся от проверки на границы -- падает надёжность и применимость, начинаются попытки оптимизировать внешний код, часто недоступный для ребилда. Вот поэтому я бросил С++.
sanders@ubuntu:/mnt/d/work/JavaVsC$ java --version
openjdk 17.0.3 2022-04-19
OpenJDK Runtime Environment GraalVM CE 22.1.0 (build 17.0.3+7-jvmci-22.1-b06)
OpenJDK 64-Bit Server VM GraalVM CE 22.1.0 (build 17.0.3+7-jvmci-22.1-b06, mixed mode, sharing)
sanders@ubuntu:/mnt/d/work/JavaVsC$ java Main
Test time : 1427
Result : -1.0, -2.696002453891236
sanders@ubuntu:/mnt/d/work/JavaVsC$ native-image Main
sanders@ubuntu:/mnt/d/work/JavaVsC$ ./main
Test time : 1883
Result : -1.0, -2.696002453891236
sanders@ubuntu:/mnt/d/work/JavaVsC$ clang --version
clang version 10.0.0-4ubuntu1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
sanders@ubuntu:/mnt/d/work/JavaVsC$ clang -O3 -march=native MainC.c -o main && ./main
MainC.c:6:8: error: unknown type name 'bool'
static bool lu_factor( double a[ 32 ][ 32 ], int n, int ipvt[46] ) {
native - оказался медленнее. Почему - надо изучать.
си - не компильнулся :(
Чистая Java, как у вас, медленнее graalvm.
Это спорное утверждение. Например в graalCE - SerialGC только. А вот в EE - уже пожирнее + всякие фишки:
GraalVM Enterprise only:
--pgo
: a comma-separated list of files from which to read the data collected for profile-guided optimization of AOT compiled code (reads from default.iprof if nothing is specified).
В "чистой java" еще несколько штук. Тут от задач и тюнинга зависит
JS это хорошо, но код надо вызвать 1М раз, при этом нужно массивы копировать. Псевдокод:
a = A
b = B
ipvt = IPVT
call lu_solve( a, n, ipvt, b )
Автоматика говорит, что достаточно и пары тысяч вызовов.
А копировать массивы тут не нужно. Вам тоже советую убрать лишнее копирование.
А копировать массивы тут не нужно.
В оригинальном коде требуется брать как бы "чистые" массивы и "портить" их этим вызовом.
Поэтому копирование для точного воспроизведения нагрузки нужно.
Автоматика говорит, что достаточно и пары тысяч вызовов.
Ну это же тестирование производительности. В предоставленных сниппетах должно быть вычисленно 1М вызовов.
То есть вы не понимаете, что делает этот код и какие массивы он "портит"?
Это вы похоже не понимаете для чего был написан этот пример.
Ваш опыт оптимизации примеров оставьте при себе, спасибо.
Ну так разъясните, какая сакральная цель копировать массив, чтобы передать в функцию, которая его не меняет.
Т.е. смысл вызывать функцию, результаты которой никому не нужны вас не смутил. Ок.
Копирование массивов нужно для приближения теста к реальным условиям вызова данного кода.
Тестируется не код а скорость вычислений, в них входит не только манипуляция с матрицами но и скорость копирования массивных блоков памяти.
Даже не сомневаюсь, что в ваших реалиях массивные блоки копируются по поводу и без.
Либо раскомментируйте `lu_factor`, который меняет остальные массивы, либо уберите их копирование, раз уж мы тут говорим про эффективность и реализм. А тестировать заведомо бестолковый код - не имеет смысла.
А если прогревать JVM?
Добавил много итераций:
Hidden text
$ java Main
iteration : 0
Test time : 1403
Result : -1.0, -2.696002453891236
iteration : 1
Test time : 1273
Result : -1.0, -2.696002453891236
iteration : 2
Test time : 1253
Result : -1.0, -2.696002453891236
iteration : 3
Test time : 1212
Result : -1.0, -2.696002453891236
iteration : 4
Test time : 1323
Result : -1.0, -2.696002453891236
Процентов на 10 лучше, и то не всегда. Дисперсии не измерял.
А покажите ключи оптимизации?
Например без --ffast-math внезапно векторизации флоат операций не произойдет.
с плохой производительностью
Какие языки из индекса-TIOBE (но можете взять и любой другой топ-20 популярных ЯП на свой вкус) обладают хорошей производительностью? Хочу понять на чём имеет смысл писать в 2022 году.
"Набор классических ошибочных стереотипов о Java начинающего программиста" из палаты мер и весов прямо-таки.
Сэр, мне кажется, но вы просто не умеете готовить эти ЯП (C++, Java). Тут у меня два соображения:
1. Ваши примеры "плохого С++" выглядят как примеры "не целевого использования". Вы просто пытаетесь писать простые вещи с использованием сложного функционала. так делать нетсмысла.
2. Обратите внимание какие ЯП используются на топовых соревнованиях по спортивному программированию. Там производительность крайне важна! И организаторы выбрали C++ и Java. Вам не кажется, что люди, которые годами учились писать высокоскоростной код несогласны с тем, что Java имеет плохую производительность? Можете лучше (производительнее)? Покажите на каком-нибудь top coder.
Как человек который участвовал в топовых соревнованиях по спортивному программированию, хочу сказать что Java используется исключительно для реализации задач на длинную арифметику. В каждой команде есть как минимум 1 человек, который даже если и не умеет полноценно в Java, то всё равно может написать короткую функцию, которая условно просуммирует BinInteger в цикле и потом его выведет.
Ну и да, в спортивном программировании не так давно (хотя уже пожалуй давно) паскаль отменили. Общество консервативное достаточно.
А что касается плохой производительности, то она вообще ни разу не в том что арифметические операции делаются дольше, а в реальном бизнесовом коде. Например там где вы поюзаете дженерик с вызовом делегата и нарожаете кучу объектов, С++ всё это будет темплейт с указателем на функцию, который заинлайнится и обойдётся без аллокаций вообще.
Тот же C# сейчас сильно упарывается что бы завезти много фич для повышения комфорта низкоуровневого программирования, но проблема в том что весь язык (как и Java), с ног до головы спроектирован в пользу удобства. И для оптимизации нужно отдельно изощряться, там где в С++ всё происходит само собой.
А что вы не можете сделать с битами в обычной структуре? Причем тут юнион?
union bsshort {
unsigned short val;
struct {
#ifdef LITTLE
char l;
char h;
#else
char h;
char l;
#endif
};
char bytes[2];
}
Например...
и что мешает вам использовать в этой ситуации типа похожий на span, который даёт вам доступ кроссплатформенно к младшему байту при обращении по индексу [0] ? Неужели легче везде расставлять ифдефы?
type puning трюков с union нарушает active member rule
А я всю жизнь думал, что тип union придуман ИМЕННО для таких трюков.
Давно не лазил в стандарт, но там везде в таких случаях были исключения для POS (plain old struct), так что мне кажется что конкретно этот bsshort по прежнему совершенно легален и определен. Могу быть не прав конечно, но ооочень удивлюсь.
Java + GraalVM = golang
В java тоже есть эти устаревшие массивы, местами несовместимые с generic'ами, да ещё и индексируются они только int'ом, а не long'ом. Если нужны структуры с 2^31 элементами и более, их только руками реализовывать.
Долго думал, так и не придумал.... Покажите пожалуйста use cases где нужны массивы с 2^31 в реальной жизни 99,9% программистов.
а) воксельные модели NxNxN, где N чуть более 1000.
б) файлы размером чуть более 2ГБ (имеют или не имеют отношения к п. а).
в) 2d изображения немногим больше 500 мегапикселей (не сталкивался).
А теперь попробуйте найти кейс, когда это не потоковое действие, а надо обязательно целиком в память засосать, чтобы начать работу. Получим ровно что я написал про 0,01% случаев, когда это надо.
Но даже тут, можно обойтись другими средствами, а не массивом.
Ээ.. Случайный доступ к таким данным?
Конечно, можно обойтись системой кэширования или многомерными ссылочными массивами, но скорость несколько уменьшится. Было бы проще просто работать с обычными массивами, железо-то позволяет. Вон, в .net массивы индексируются long, да и во многих других языках. Особенно обидно, когда файл (массив) не так уж и велик, но слегка в 2^31 не влезает. И довольно неприятно, когда у пользователя (условно) при N=1290 программа ещё работает, а при N=1291 перестаёт. Приходится предусматривать эту возможность, писать странный код.
Такие объемы вы уже вряд ли в памяти будете хранить. Ибо и памяти на пару объектов хватит только и врятли "просто случайный доступ" это бизнес кейс.
У вас уже он где то будет валятся - а дальше что-то типа https://docs.oracle.com/javase/7/docs/api/java/nio/channels/FileChannel.html#map(java.nio.channels.FileChannel.MapMode, long, long)
И вперёд...
Действительно, случайный доступ к огромному массиву не необходим бизнесу, но такая фича делает некоторые вещи проще, а некоторые алгоритмы (выполняемые бизнес-задачи) - возможными. В любом случае, у меня уже есть пример этакой системы кэширования, которая работает уже много лет.
Mmap на jvm, помнится, когда-то приводил к утечкам виртуальной памяти. На таких размерах файлов и все 256ТБ виртуальной памяти съесть недолго. Пока наиболее интересный вариант - MemorySegment, наиболее неприхотливый - кастомные структуры из ссылочных массивов.
Вот просто по памяти:
sun.misc.Unsafe.allocateMemory(long bytes)
jdk.incubator.foreign.MemorySegment (эта часть уже стабильна)
Если говорим про GraalVM:
org.graalvm.nativeimage.UnmanagedMemory.malloc
Ну и в native можно прямо в POSIX:
@CFunction(transition = CFunction.Transition.NO_TRANSITION)
public static native PointerBase mmap(PointerBase addr, long len, int prot, int flags, int fd, long off);
К сожалению, все эти решения требуют ручного управления памятью и имеют несколько нестандартный синтаксис. Что-то вроде С++ получается.
Именно так... для не стандартных кейсов не стандартные решения...
Меня печалит, что кейс "доступ к элементу массива (списка)" норовит стать нестандартным и невозможным (без написания адского кода), когда массив занимает всего 10-20 процентов от доступной памяти. Это искусственное сейчас ограничение. Да, раньше int хватало всем для индексации, но теперь-то long хватает всем.
Это как раз то что называют:
"Что только люди не делают, что бы не учить C++" ©
А давайте перефразирую: Работаешь с 64-х битными массивами - учи С++ :)
В реальности, по моему скромному мнению, массивы только в нише специфичных кейсов остались. 90% народа работает с листами, мапами, сетами и прочими обертками
А листы/мапы/сеты умеют в размер больше чем 2^31?
Я вполне могу представить, что на C++ кто то захочет массив/вектор булов размером больше чем на 500Мб. Но так же не вижу причин оправдываться на вопрос что в Java это нельзя сделать из коробки.
А описанные изощрения хоть и позволяют достичь этого, но прям явно сигналят что для данной задачи нужен другой язык. Ну или для данного языка другой подход — не делать так.
Поэтому тезис имхо очень подходит)
Ключевое написанное вами - "Я вполне могу представить".
Ровно так же можете представить миллионы(а не несколько) задач где поможет рефлексия или аннотации. И все написанное вами выше сразу теряет смысл.
Но нет же. Конечно у рефлекшна есть ряд задач, которые он решает более удобно, с этим никто и не спорит. Речь про то что у каждого языка есть свои преимущества и недостатки. Но сделать из Java замену C++ не получится даже близко. Как и из C++ замену Java.
Кстати про рефлекшн и аннотации (да и про GC). Тот же UE4 вполне успешно сделал их реализацию поверх всего этого поверх C++. Причём с предметной спецификой, когда не всё попадает в GC и рефлекшн. Так что это всё тоже можно сделать с определённым извратом.
Ну раз "Но нет же" и "у каждого языка свои преимущества", то это не согласуется с вашей фразой с которой начался этот разговор - "Это как раз то что называют:
"Что только люди не делают, что бы не учить C++" ©"
То есть я написал как решить эту задачу на Яве. Всего то показал опции.
Согласуется, мой тезис был в том что если язык не предоставляет удобного способа решения какой то проблемы, в данном контексте — работы с большими массивами, то и не надо использовать его для этих задач. Вместо того что бы придумать костыли как это решить, лучше просто взять подходящий инструмент, например С++.
"Но нет же" — потому что я совсем не согласен с тем что наличие в Java рефлексии хоть каким либо образом обесценивает возможность работы в С++ с большими массивами.
Если мы говорим, про "сферический длинный массив в вакууме", то вы безусловно правы.
Однако я не встречал компании которые пишут исключительно длинные массивы. То есть это какая то из задач. Если у нас компания и так пишет на С++, то нам нечего обсуждать.
А вот если мы говорим про Яву, то давайте попробуем вместе, в этой компании Ява программистов написать массив на сях в решение которое уже пишет куча программистов.
То есть мы найдем сишниеа, возьмём на работу, выгоним нахрен через 3 месяца, если так как он не нужен больше или найдем контактора.
Дальше напишем кучу JNI, который надо будет так же как то поддерживать... Ну то есть "костыль на java"и ваш "просто взять с++" начинают играть совершенно новыми красками...
По этому я и написал про наиболее подходящий под задачу язык. Если задача например обработка изображений или рендер, то там всё не обойдётся только лишь длинными массивами. Там и множество других проблем всплывёт.
А вот если делать сервер, то Java/C# на порядок более подходящие языки. И даже если появится такая задача с массивами то можно её решить более простым способом, например сделав абстракцию с несколько массивами внутри, а не делать платформозависимый код.
Собственно я пока что не встречал компаний/проектов, где пишут исключительно на одном языке. Из того что я вижу, в рамках одного проекта вполне себе успешно сочетаются C#/C++/Java/Python/Powershell и местами немного Rust/Go.
И да, union был в С не для того что написано в вашем примере, а для работы с аппаратурой, битовыми полями
Сомневаюсь, что это так, ибо "C99 6.7.2.1-11:An implementation may allocate
any addressable storage unit large enough to hold a bit- field. If
enough space remains, a bit-field that immediately follows another
bit-field in a structure shall be packed into adjacent bits of the same
unit. If insufficient space remains, whether a bit-field that does not
fit is put into the next unit or overlaps adjacent units is
implementation-defined"
Проще говоря, компилятор может раскидать битовые поля, так как посчитает нужным, что не очень подходит для работы с аппаратурой. Драйвера, которые мне приходилось видеть никогда не используют битовые поля в структурах. Они используют маски и смещения.
великолепном hole C++
Великолепная опечатка. Дыра++
C++ это феномен нашего времени – популярный язык, который позволяет писать вот реально «как хочешь», когда все остальные языки пытаются диктовать какую-то одну парадигму и строят все вокруг нее.
Потому я бы в C++ только добавлял – рефлексию там (и статическую и динамическую конечно), аннотации к полям и типам, деривативы, новую версию макросов (не удаляя старой конечно), процедурные макросы и так далее.
Вам не приходилось, получать код от других программистов, которые пишут "как хотят"? Поделитесь впечатлениями?
Из-за такого подхода язык получился безумно раздутым и на его изучение уходит полжизни. При том, что ни в одном реальном проекте все его фичи никогда не используются.
который позволяет писать вот реально «как хочешь»
В итоге даже Страуструп оценивает свое знание плюсов на 7 из 10, потому что знать этот язык более менее полно нереально. И это создатель языка, который посвятил ему десятилетия своей жизни, причем до тонны новомодных нововведений. Как теперь угадывать, какую часть C++ у которого только список терминов занимает страниц 20 A4 мелким шрифтом нужно учить, чтобы гарантированно понимать код который пишет хотя бы условное большинство - хз.
О, ООП опять хоронят.
10 - ключевое слово final (запрет наследоваться от типа) - не имеет ни одного известного мне полезного применения, ломает обобщённый код, вердикт - удалить
Отличный довод, просто великолепный. И не поспоришь ведь.
покажите пожалуйста полезное применение... Может это?
struct example final {
virtual void foo() = 0;
};
Если мне не изменяет память, то прежде чем кейворд auto поменял своё значение, была просмотрена куча кода на предмет его использования. Немного разнится с личным мнением Васи из интернетов.
final позволяет компилятору делать оптимизации, вызов методов происходит непостредственно, без использования vtable, если переменная типа который final
ну во-первых в статье также предлагается выкинуть virtual, во вторых целое ключевое слово для какой-то возможности оптимизации, когда компилятор итак в состоянии понять, что наследников у типа нет?
Компилятор способен понять в пределах одной единицы трансляции, если же класс уже скомпилирован, то понять будет ли он в будущем унаследован в другой единице трансляции компилятор не может)
Захочет - сможет. Особенно учитывая тот факт, что девиртуализировать нужно по месту применения, то есть компилятор видит что создан конкретный тип и можно не делать вызов через vtalbe
целое ключевое слово
final
-- не ключевое слово, иначе этот код не компилировался бы:
#include <cstdlib>
#include <iostream>
int main() {
struct final final {
virtual ~final() final {
std::cout << __func__ << std::endl;
}
virtual operator int() final {
return EXIT_SUCCESS;
}
} final;
return final;
}
это называется контекстно зависимое ключевое слово
это называется контекстно зависимое ключевое слово
Ключевые слова не могут быть использованы в качестве идентификаторов.
Я использовал здесь final
два раза в качестве идентификатора (имя структуры и имя переменной) безо всяких проблем, и ни один из 4-х компиляторов даже не пикнул: https://godbolt.org/z/rKh169Wb3
Если же прочитать нечто, достаточно близкое к оригиналу:
https://en.cppreference.com/w/cpp/keyword
То выяснится, что final
среди ключевых слов отсутствует.
Зато он присутствует среди идентификаторов с особым смыслом/значением, который (смысл, то бишь, или значение) они (идентификаторы) имеют лишь в определённых контекстах.
В другом месте https://en.cppreference.com/w/cpp/language/final он описывается как спецификатор.
И нигде он не описывается как ключевое слово.
Специально глянул драфт 2015-го года -- там final
описывается в главе 2.10, которая называется Identifiers.
Вот пункт 2 оттуда:
2 The identifiers in Table 2 have a special meaning when appearing in a certain context. When referred to in the grammar, these identifiers are used explicitly rather than using the identifier grammar production. Unless otherwise specified, any ambiguity as to whether a given identifier has a special meaning is resolved to interpret the token as a regular identifier.
Table 2 — Identifiers with special meaning
override final
Думаю, излишне будет уточнять, что Стандарт имеет силу аксиомы, то есть, он -- абсолютен, и если он указывает, что это -- identifier with special meaning, а не ключевое слово, значит, это абсолютно именно так и есть.
Напрашивается вопрос: зачем выдумывать про ключевые слова?
Прежде чем будет достигнута программистская зрелость, неизбежно придётся неоднократно взбираться на пик глупости кривой Даннинга-Крюгера.
То есть, само по себе, хоть это и неприятно, -- это необходимая издержка.
Но в дальнейшем -- всё определяется выбором, который делает человек, оказавшись там.
Выборе, сделанном для того, чтобы научиться не попадать туда больше, или для того, чтобы во чтобы то ни стало каждый раз "казаться правым".
В данный момент вы прилагаете существенные усилия не только для того, чтобы там, на пике глупости, удержаться, но и пытаетесь подпрыгнуть ещё выше, и всё -- для того, чтобы "казаться правым".
То, что вы не проверили, является ли final
ключевым словом, или проверили, но специально продолжаете его так называть, как раз и показывает, что выбор вы пока делаете не в сторону зрелости.
По этой же причине вы не воспринимаете даже здравые аргументы ваших оппонентов и отвергаете их только потому, что они не совпадают с вашими.
Стараетесь отталкиваться не от того, что есть, не от реальности, а от того, что вам хочется, и всё ради того, чтобы "казаться правым".
Этот путь отнюдь не способствует достижению программистской зрелости.
Но возможность изменить свой выбор всегда остаётся.
Ответ на этот вопрос содержится в первом предложении моего ответа, на который вы ответили.
С примерами у вас -- не очень.
С примерами контекстно-зависимых ключевых слов, которые могут быть использованы в качестве идентификаторов.
Именно ключевых слов, то есть, таких идентификаторов, в отношении которых в Стандарте прямо и недвусмысленно заявлено, что они "reserved for use as keywords" и/или "unconditionally treated as keywords".
https://godbolt.org/z/K1ErnMMxe
https://eel.is/c++draft/gram#key-1.sentence-1
New context-dependent keywords are introduced into a program by typedef, namespace, class, enumeration, and template declarations.
Следует прочесть немного выше о том, что мы вообще читаем:
https://eel.is/c++draft/gram#general-1.sentence-1
https://eel.is/c++draft/gram#general-1.sentence-2
А именно:
This summary of C++ grammar is intended to be an aid to comprehension. It is not an exact statement of the language.
Перевод отсюда:
Это краткое изложение грамматики C++ предназначено для облегчения понимания. Это не точное определение языка.
Суть той части документа, на которую вы сослались:
Это не точное определение языка.
И конкретно тот пункт, ссылку на который вы привели, противоречит следующей главе, находящейся в той части документа, которая является точным определением языка: https://eel.is/c++draft/lex.key
class a { public: void foo() {} };
int main() {
a a;
a.foo();
};
Поясните, пожалуйста, что именно демонстрирует данный код.
Вы ведь понимаете, что это — вопрос определений, и что любой человек, примерно представляющий суть разговора, может понять, что «контекстно-зависимое ключевое слово» и «контекстно-зависимый идентификатор» — это одно и то же?
Человек, примерно представляющий суть того, что из себя представляет ключевое слово, понимает, что ключевое слово "изымает" данный идентификатор из тех возможных, которые могут быть использованы в программе для именования сущностей, в отличие от контекстно-зависимого идентификатора.
Проще говоря, переменную с именем class
невозможно завести в принципе, потому что class
-- это ключевое слово, и все такие идентификаторы в программе трактуются именно как ключевое слово class
, и -- никак иначе, а переменную с именем final
-- очень даже возможно, как раз по причине того, что final
не является ключевым словом.
Более того, в Стандарте прямо указано, что в случае неопределённости, как интерпретировать контекстно-зависимый идентификатор, когда он может быть проинтерпретирован и так, и этак, выбор однозначно должен делаться в пользу обычного идентификатора, то есть, без специального значения (смысла).
Отсюда, в частности, следует, что невозможно определить безымянную структуру так, чтобы она стала final
, потому что неопределённость, как интерпретировать final
в этом контексте, разрешается, в соответствии со Стандартом, в пользу обычного идентификатора, а не в пользу специального значения (смысла) final
, и поэтому следующий код успешно компилируется:
auto
fun() {
struct final {
} const s;
return s;
}
struct s: decltype(fun()) {
};
Более того, я бы сказал, что «контекстно-зависимый идентификатор» субъективно имеет меньше смысла, чем «контекстно-зависимое ключевое слово», ну да ладно.
Если что-то является ключевым словом, то основная семантика данного факта такова: данный идентификатор "изымается" из всего множества возможных идентификаторов, доступных для использования в качестве имён сущностей и становится недоступным для этой цели.
Для final
-- это не так. Он -- доступен, причём, всегда.
В этом и была суть моего изначального замечания, потому что я полагал, что автор имеет ввиду именно "изъятие" идентификатора final
из множества возможных имён для именования сущностей в программе.
А поделитесь плз ссылками на секту хоронителей ООП. Хочется поднять настроение.
final - пожалуй соглашусь с автором. Ещё с лохматых годов, со времён 90х и delphi, накрывался на финальные классы, которые приводили к дикому количеству копипасты. Реальных же случаев, когда без этого никак, на вскидку не припомню
Автор описал язык ++ , без С. Имхо это офигенно само по себе.
Убрать union
, typedef
, массивы, void
, int
, макросы. И прощайте все заголовочные файлы C. Будем экспортировать нативные функции, как это делает C#.
Некоторые пункты отдают легким троллингом. Я бы еще предложил оставить в функциях не более 1 аргумента, а вызов функций с N аргументами имитировать в виде шаблонной магии.
Реальные проблемы в C++ выглядят не так. Вот так выглядят "классы" проблем, после фикса которых можно получить "C++ мечты":
Вопрос о сломе ABI и всего веера положительных и отрицательных эффектов из-за этого.
Бедность стандартной библиотеки по сравнению с буквально всеми другими языками (тут ABI ни при чем).
Поехавший нейминг и правила, например овер9000 значений слова
static
при разных обстоятельствах; или что anonymous namespace наделяет все символы внутри себя internal linkage-ом (как до такого додумались?).Медленная компиляция по разным причинам.
Отсутствие общепринятого менеджера пакетов/библиотек (тут могут быть разные мнения, но это тоже "класс" проблем)
Достаточно медленное развитие языка в целом - те же концепты пытались родить с нулевых годов, а также медленная поддержка этого в компиляторах.
Отсутствие встроенных линтеров и чекеров (которых овер9000, но ими пользуются не только лишь все, а только самые продвинутые).
А "фиксы", которые удаляют ООП и т.д. - это не решение всех проблем C++, а описание какого-то нового языка.
Не вопрос стандарта языка, аби хранят реализации, а в стандарте такого понятия нет
Зато есть куча другого кода вне стандарта
Предложенное статье сокращает количество правил. Значительно
Достаточно быстрая компиляция, относительно возможностей языка. У раста в разы медленнее
Cmake
Последние 12 лет очень бурное развитие
Они не должны быть встроенными, потому что IDE и спелчекеры не должны являтся частью языка. Как и система сборки и документирования. Сторонние инструменты есть
Cmake это даже не решение, а само по себе проблема с синтаксисом из 70ых и без нормальной документации.
cmake это и не менеджер пакетов, это в первую очередь система сборки
Не вопрос стандарта языка, аби хранят реализации, а в стандарте такого понятия нет
Ситуация из разряда "жопа есть, а слова нету". Почему нельзя удалить из стандарта ставший ненужным с C++11 метод push_back? (А точнее, переписать его код вместо создания нового метода emplace_back
)
Потому что ABI сломается. Хотя, казалось бы, про ABI в стандарте ничего не написано.
Я бы, кстати, еще AST (Abstract Syntax Tree) в стандарт "C++ мечты" легализовал, чтобы люди могли писать тулзы для исходного кода без привязки к внутренностям компилятора.
Зато есть куча другого кода вне стандарта
95% людей устроило бы наличие любой (даже не самой быстрой) json-либы (как в питоне). Если бы была нужна какая-то мега-быстрая json-либа (в realtime-приложениях), то там уже могут быть варианты, но свои трейд-оффы есть везде.
Предложенное статье сокращает количество правил. Значительно
Предложенное в статье главным образом создает какой-то совершенно другой язык.
Достаточно быстрая компиляция, относительно возможностей языка.
Я имел в виду "класс" проблем, которые именно вызывают медленную компиляцию. Например, из-за некоторых свойств препроцессора тулза include-what-you-use в 10 раз толще, чем нужно быть, и все равно работает не совсем как следует. Значит, нужно порезать препроцессор. Просто не стал все это расписывать, иначе комментарий получился бы длиннее статьи.
Cmake
Многие считают это худшей системой сборки ever. Если приводить все аргументы, выйдет тоже немаленький текст, но я читал бомбежку на CMake (сам его редко использую) - меня впечатлило.
Они не должны быть встроенными, потому что IDE и спелчекеры не должны являтся частью языка. Как и система сборки и документирования. Сторонние инструменты есть
95% людей были бы согласны на любой нормальный спеллчекер в "C++ мечты". Да и что значит "не должны", "не могут". C#/Java/Python/Rust - у них все есть и они все могут.
ставший ненужным с C++11
Не понял, почему ненужным? Он позволяет создать объект на месте, но есть много случаев когда объект создается, с ним производятся манипуляции, и только потом он добавляется в вектор
STL всосалась в стандарт когда JSON еще никому не нужен был, если вообще существовал. А теперь поздно что-то кардинально менять. Но вот оформить пропозал по включению JSON поддержки сейчас, никто не запрещает. Вот только С++ используется там, где текстовый, несамодокументированный формат передачи небольших, непотоковых данных немного редко применим. Так что вряд ли быстро примут в стандарт. Ну и без вменяемой рефлексии, это все будет полу-мерой без решения реальной проблемы, как получить по RPC готовый объект ничего о нем зная, или с динамическими полями.
Убирание push_back сломает не аби совместимость, а интерфейс совместимость))
Да и бывают ситуации когда он нужен
Насчёт стандартизации AST - я бы хотел стандартизацию интерфейса модуля. Кстати именно модули решат проблему инклудов...
А CMake обладает плохим синтаксисом, но в целом удобно, желаю всем на него перейти, чтобы подключать либы было удобнее(а синтаксис можно обернуть в программу на питоне по генерации скриптов CMake...)
Ну и что-что, а json либ громадное множество, а мне не хотелось бы ничего включающего в себя даже в названии JAVA SCRIPT в стандарте С++
а json либ громадное множество
Что является проблемой само по себе. Т.к. в проекте легко может появиться несколько библиотек, каждая их которых тянет разные либы для json. Это из той же серии, как раньше каждый фреймворк пилил свой класс строк, а потом их нужно было по пять раз переводить между собой на ровном месте.
синтаксис можно обернуть в программу на питоне по генерации скриптов CMake...
Уже обернули. Система сборки SCons это питоновский скрипт с многопоточным билдом объектных файлов и кешем компиляции (в модули до сих пор не может)
Поздравляю, вы изобрели язык D.
приятно, что человек думает самостоятельно, но конечно проще и чище делать new from scratch, совет простой - забыть "С++" и описать свой "D"
С помощью union можно сделать например так
typedef struct {
union
{
unsigned int address;
unsigned char bytes[4];
}
} ipv4_address;
И как в том же Java так же сделать? Возможно какие-то нормальные обходные пути и есть. Но я в Java максимум быдлокодер, таких не знаю, поэтому приходилось делать громоздкую конструкцию, где я при замене 32 битного значения вручную менял и массив и наоборот. А тут всё быстро и автоматически.
Естественно с первого раза конструкция не взлетела пришлось потом тратить время на вылавливание ошибок.
Вы написали UB. Если я правильно понимаю как вы это собираетесь использовать.
В Джаве так сделать нельзя и слава богу.
А зачем это делать в java или любом другом языке? Разделяем на разные типы и логически всем понятно кто-что-зачем-куда
а ещё это можно сделать без помощи union
(у вас int на другой платформе может быть 2 байта...)
И как это сделать в Java без помощи юнион?
У нас на порт приходит пакет 10 байтов. На самом деле же это числа по 2 или 4 байта + заголовок и контрольная сумма.
Есть конечно дуболомный вариант. Что если не самый старший байт <0 , то добавляем 256. И потом ещё умножаем на 256 в зависимости от того какой разряд. Но по мне , подход так себе.
----
Про два байта вы правы, такой код не универсальный, для универсальности надо делать костыли.
Почему бы не поправить стандарт?
Не увидел там ничего про "нельзя", только "в общем случае небезопасно", но этот общий случай проявляется лишь в вариантах (когда мы засовываем в него что попало), а там никому не надо читать не тот тип, что записан. Во всех остальных применениях юниона (структурирование буфера) там всё вполне безопасно.
В вашей цитате нет ничего про "нельзя", да и быть не может, ибо это основная причина существования union вообще. Структурирование буфера - оно не про "сложные типы".
Рано или поздно вам надоест писать одни и те же касты во всех местах работы с полями и вы напишете класс *Union, который это инкапсулирует в себя, и получите те же самые юнионы, только с кучей кода и надеждой, что компилятор всё соптимизирует как надо.
Ближайшее, что есть для подробного в Java, это ByteBuffer.
Но позволяет читать один и тот же массив данных и как int и как byte
Спасибо , может мне пригодится. Интересно, как он внутри работает. В коде ByteBuffer.java описываются getFloat() и putFloat(float f) но самого кода , как это работает нет.
Единственное ,нашёл в модуле bits что-то вроде такого
static private int makeInt(byte b3, byte b2, byte b1, byte b0) { return (((b3 ) << 24) | ((b2 & 0xff) << 16) | ((b1 & 0xff) << 8) | ((b0 & 0xff) ));}
Примерно это работает вот так:
SocketChannel channel = ...
int bytes = ( volts.length + currents.length + resistances.length + 1 ) * Double.BYTES;
bytes += Integer.BYTES * 7;
ByteBuffer bbw = ByteBuffer.allocateDirect( bytes );
bbw.order( ByteOrder.nativeOrder() );
bbw.putDouble( t );
bbw.putInt( callIndex ); // 0 - startIteration, 1 - doStep
bbw.putInt( inCount );
bbw.putInt( outCount );
bbw.putInt( volts.length );
bbw.putInt( currents.length );
bbw.putInt( resistances.length );
bbw.putInt( flags );
bbw.asDoubleBuffer()
.put( volts )
.put( currents )
.put( resistances );
bbw.rewind();
try {
int sended = 0;
while ( bbw.hasRemaining() ) {
sended += channel.write( bbw );
}
} catch ( Exception x ) {
disconnect();
return;
}
Самое зло с union - Это вызов его из Java. :) Год назад неделю промучался, чтобы вот это:
#define I2C_SMBUS_BLOCK_MAX 32 /* As specified in SMBus standard */
#define I2C_SMBUS_I2C_BLOCK_MAX 32 /* Not specified but we use same structure */
typedef union i2c_smbus_data
{
uint8_t byte ;
uint16_t word ;
uint8_t block [I2C_SMBUS_BLOCK_MAX + 2] ; // block [0] is used for length + one more for PEC
} i2c_smbus_data;
typedef struct i2c_smbus_ioctl_data
{
char read_write ;
uint8_t command ;
int size ;
union i2c_smbus_data *data ;
} i2c_smbus_ioctl_data;
в ioctl передать из java... Неделя в GDB и профайлера и вот счастье... Которое закончилось ровно 2 дня назад. Тот же самый код - уже не работает :)
Ждет нас опять чудное сравнение байтов в памяти...
Тоже игрался долго.
Без "ByteOrder.nativeOrder()
"ничего не работало.
Код выше обменивался с программой на С по сети.
#pragma pack(1)
struct PICHeader {
double time;
uint32_t callIndex; // 0 - startIteration, 1 - doStep
uint32_t inCount;
uint32_t outCount;
uint32_t volts;
uint32_t currents;
uint32_t resistances;
uint32_t VccClk;
};
Попробую, спасибо.
Вопрос: как человек более близкий к ось и сям - не подскажите, есть ли лёгкий способ трэйсить что в ioctl пришло (на примере этих структур хотя бы)
На реальных данных наверно долго будете искать ошибку.
Нужно написать тестовый пример, и заполнить известными данными. Потом вывести в лог побайтно и смотреть где компилятор вставил дырки для выравнивания.
Если используете MSVC то упаковка структуры с выравниванием до байта делается #pragma pack(1).
Если вы используете GCC/Clang то:
Использование
__attribute__((aligned(1)))
сообщает компилятору, чтобы начать каждый элемент структуры на следующей границе байта, но не говорит, сколько места он может положить в конце. Это означает, что компилятору разрешено округлятьstruct
до кратного размера машинного слова для лучшего использования в массивах и тому подобное.__attribute__((packed))
сообщает компилятору, что он вообще не использует никаких дополнений, даже в концеstruct
.
struct foo {
uint8_t bar;
uint8_t baz;
} __attribute__((packed));
А потом можно перекомпилировать этот код на машине с другим порядком байт в слове и "happy debugging, suckers"
Вы бы хоть uint32_t
и uint8_t
написали, что ли...
Хорошая статья. И провокационная, и по делу. Спасибо.
Про 1-8 пункты помечтать не вредно. Вот как Комитет разрешит ломать обратную совместимость, так можно и предложения официально оформлять.
На текущий момент, мне вот крайне зашел Rust как язык, отринувший старье без душевных терзаний за это. Сразу оговорюсь, в Rust, на мой взгляд, крайне сырые и неудобные правила именования стандартных сущностей, репозиторий пакетов как огромная свалка сломанных и заброшенных библиотек, захвативших очевидные названия. Однако, логика языка, поддержка разных парадигм и встроенный инструментарий, готовность быть языком общего назначения — мне понравилась.
Пункт 9. С++ он все таки объектный язык. Из-за нам тут машет Smalltalk, который ввёл моду на ООП, но шила в мешке не утаишь, и перегруженную грамматику языка все-таки как-то надо реализовывать, ключевое слово class для объявления объекта в целом оправдано. struct и class вдвоем избыточны да. Haskell вот вводит data, type и, внезапно, newtype слова. Ну что вообще не лучше class/struct и using/typedef. А Хаскелл трудно упрекнуть в неточности.
Пункт 10. С++ — язык общего назначения, в том числе и для хардкорных, узко-специфичных библиотек и фреймворков. И, теоретически, фреймворк может заставить пользователя не расширять какие-то иерархии. ADL опять же. Пересечение со статической типизацией и мета-программированием конечно возможно, но теоретическая необходимость в Стандарте С++ во многих местах прослеживается. Правило 3(5 с недавнего времени) тоже не в каждой базе кода встретишь, но мы же не удаляем из языка методы-операторы присвоения и равенства.
Пункт 11. Виртуальные методы — центр всей объектной модели объектно-ориентированного языка общего назначения. Rust предлагает динамические трейты, Java — вообще вся насквозь по умолчанию виртуальные методы использует. Я не углубился в реализацию предложенной библиотеки, и могу ошибиться в суждении, но только виртуальные методы на уровне языка умеют пересекать границы бинарника без рекомпиляции. Все шаблонные башни и чернокнижные преобразования, сломаются, если потребуется передать новые типы, неизвестные в момент создания библиотеки. (Этот факт, кстати, вызывает у меня несказанное удивление до сих пор. Не имея стандартного ABI С++ позволяет передавать указатели на интерфейсы через границы бинарников и это чаще работает, чем не работает. Хотя в принципе не должно работать вообще, по аналогии с проблемой манглинга имен и передачи STL сущностей через границу бинарника.)
Пункт 12. С++ — объектно-ориентированный. Опять. Метод, это не столько еще один блок кода, который вызывается вокруг некоторого инстанса. (Тут нам передает привет Go, где указатель на метод и на функции — одно и то же, а получатель вызова по указателю и по значению — тема для продвинутых и особо не афишируется). В С++ метод — это не столько операция над аргументами, сколько операция над объектом в первую очередь. В идеальном ООП мире не существует c = a.add(b), однако, шаблоны Visitor или Builder полагаются на то, что методы выполняют не только работу над аргументами, но и над внутренним устройством типа. Ну и RAII без методов было бы ущербным. Нам опять передает привет Go, где потоко-безопасный множественный доступ в коллекцию из разных функцию — антипаттерн в пользу каналов или унылого бойлерплейта с мутексом и кастомным типом вокруг каждой коллекции. То, что метод в С++ неотъемлемая часть типа, это и ограничение и преимущество. И просто так избавляться от в целом невредного инструмента ну как-то импульсивно. ADL вот только для функций используется, и это прекрасно. Хочешь применить алгоритм к аргументам — используешь свободные функции, хочешь запускать сложную машинерию скрыто от глаз вызывающего кода(и для хитрого продакшен кода, и для тестов, почему нет) — методы наше все.
Так методы фактически остаются, но понятие метод и указатель на метод исчезает, deducing this показывает что это вполне реальность
Насчёт пункта 11 - не то чтобы это должно было волновать С++ программистов, ведь длл вообще не существуют для С++. Но библиотека их(надеюсь) поддерживает, правда с дефайном (#define AA_DLL_COMPATIBLE ), можете посмотреть как оно реализовано(в стандартной библиотеке +- также работает междллное взаимодействие, насколько я знаю)
в Rust, на мой взгляд, крайне сырые и неудобные правила именования стандартных сущностей <...>
Можете пояснить, что вы имеете в виду?
C++ STL использует snake_case и преимущественно полные или достаточно длинные идентификаторы: std::shared_ptr; std::swap; std::in_place_index_t; std::format_to_n; std::unordered_multiset; std::condition_variable.
Комбинация типов через метапрограммирование хоть и монструозна, но псевдонимится через using, оставляя основное определение многословным, но однозначным. std::shared_ptr<std::variant<std::vector<std::string>, std::map<std::byte, std::string>>> не оставляет разночтений и места для предположений.
Теперь смотрим в Rust, где хоть и есть стандарт по именованию, но он не помогает читать чужой код.
Типы именуем CamelCase, функции и модули — snake_case.
Комбинированное внесение в область видимости заставляет спотыкаться:
use datafusion::logical_plan::{DFField, DFSChema, lit_timestamp_nano};
use std::path::{Path} дублирование разным регистром выглядит подозрительно избыточно, хоть и теоретически обоснованно, потому что модули и тип это разные сущности.
use std::sync::Arc — причем здесь потокобезопасная дуга? Нет, Atomically Reference Counted. Теперь мы подготовлены к std::sync::Condvar, std::path::PathBuf;
std::sync::mpsc::channel — Multi-producer, single-consumer FIFO queue communication primitive.
std::rc::Rc мы уже понимаем как Reference Counter; но std::rc::Weak вообще прилагательное. И мы додумываем, что это про слабый счетчик ссылок.
To/From, Into_ типы, трейты и вспомогательные функции тоже не самодостаточно именнованы, чтобы понимать о чем речь. Какое правило по превращение одного в другое: B = A.into() или B = B.from(A), и прочие противоречивости.
От аббревиатур в CamelCase к Rust сущностям.
std::boxed::Box, что по сути является указателем на динамически выделенную в куче память. И ничего общего с боксингом/анбоксингом значений в Java, или двумерной фигурой с 4 углами.
И std::borrow::Cow, который не про одолжить корову у соседа, и даже не про одолжение Copy-On-Write.
The type Cow is a smart pointer providing clone-on-write functionality: it can enclose and provide immutable access to borrowed data, and clone the data lazily when mutation or ownership is required.
std::cell::{RefCell, RefMut, Cell}; без подглядывания в референс мануал напрашиваются какие-то клетки или ячейки, а ведь это A mutable memory location. И это совсем не про memory buffer.
Может у меня глаз замылился, всё-таки пишу уже несколько лет на расте, но честно говоря не припоминаю, чтобы даже на заре знакомства с языком эти "неочевидности" вызывали вопрос. Хотя первое время "ломка" и была из-за того, что многое хотелось писать как привык в С++. Но да, люди разные, так что аргументировать личным опытом — такое себе. Теперь мне даже интересно у многих ли людей такие же вопросы возникают...
Комбинированное внесение в область видимости заставляет спотыкаться:
А мне наоборот нравится, что сразу видно где тип, где функция, где переменная, а где константа.
To/From, Into_ типы, трейты и вспомогательные функции тоже не самодостаточно именнованы, чтобы понимать о чем речь. Какое правило по превращение одного в другое: B = A.into() или B = B.from(A), и прочие противоречивости.
Вот это не очень понял. Почему From
и Into
именованы не самодостаточно, если всё что они делают — это просто преобразуют из одного типа в другой. Как-то даже затрудняюсь придумать как название можно расширить чтобы сделать "более очевидным".
Зачем два? Потому что много где не надо явно указывать целевой тип — он будет известен из контекста и достаточно просто а.into()
, а там где нужно указать тип — там удобнее X::from
. В доке, вроде как, вполне внятно это описывается, а так же какой из трейтов надо реализовывать чтобы автоматически получить реализацию второго.
From и Into зачастую реализуются для разных трейтов. И для сложных случаев, таких как datafusion библиотека, каждый тип может на себе иметь по десятку реализаций. Автоматический вывод всего конечно прекрасен, и C++ auto и type deduction не зря вводит и расширяет. Но в Rust иногда попадается f.into().into() что просто взрывает мозг. Но мои претензии все равно остаются сугубо субъективными и обусловленными С++ деформацией сознания. Был бы рад изучить популярным языком написанные статьи с предпосылками именования в Rust. Не верю, что придумав грамотные концепции и принципы были упущены правила именования. Скорее, я смотрю на них не стой стороны.
Про virtual не понял. Вы хотите как в java, где все, что не помечено как final или private, по факту virtual? Или вообще vtable выпилить из языка? Если vtable убирать, то я против. Это очень мощная фича языка и я даже не представляю, как писать эффективный код без этого.
Посмотрите на примеры с гитхаба в том же пункте. Нет, мне не нужны везде virutal, наоборот, отделение типа и его полиморфизма
template<typename T>
struct Say {
static void do_invoke(const T& self, std::ostream& out) {
self.say(out);
}
};
using any_animal = aa::any_with<Say>;
struct Cat {
int field;
void say(std::ostream& out) const {
out << "Meow\n";
}
};
struct Dog {
std::string field;
void say(std::ostream& out) const {
out << "Woof!\n";
}
};
int main() {
any_animal Pet = Cat{};
}
Как вы можете видеть здесь any_animal - тип созданный библиотекой, он может хранить любой другой тип с требуемыми методами( в данном случае Say)
Вот только эта либа желает динамическую аллокацию памяти на КАЖДЫЙ чих. И да, таблицы виртуальных функций там всё ещё есть, только теперь девиртуализация с эти и чудом невозможна, любые оптимизации с вызовом final невозможны. Если для вас всё это ок, то может вам и плюсы не нужны? Ну пишите вы на питоне, зачем мучаться
Что вы несёте? Где там нужна аллокация на каждый чих? Может вы посмотрите как используют СЕЙЧАС виртуалньые функции, когда каждый объект выделен в куче?
А в библиотеке как раз этот недостаток убирается и можно как изменить аллокатор так и просто ничего не делая получать бонусы от small object optimization
Пожалуйста, если вы не разбираетесь в вопросе - не делайте таких уверенных утверждений
P.S. оптимизации очевидно всё ещё возможны, потому что это всё ещё код, который можно оптимизировать. Даже больше - языковой vtable оптимизируется менее охотно, ведь он языковой и гарантируется чем-то, плюс там часто куча лишней информации,раздувающей бинарник, в либе этого уже нет, только нужное
Правильно ли я понял, что автор библиотеки реализовал виртуальные методы на шаблонах, со своей собственной vtable? И что в итоге получили? Усложнение синтаксиса. Ну да ладно, привыкнуть можно. К генерации столь-же оптимального кода, как сейчас на встроенном vtable тоже есть вопросы. Я бы не стал тут полагаться на магию оптимизатора. А вот умеет ли библиотека (не сильно ковырялся в ней) делать множественное виртуальное наследование? Да, это спорная фича и вызывает у многих вопросы, но в моей практике множественное виртуальное наследование довольно часто оказывается самым удобным, быстрым и красивым решением задачи.
Так что все равно нет.
Однажды автор познакомится с Rust и будет ему счастье. Ну серьезно, для меня C++ это прежде всего ООП язык с упором на скорость и удобство разработки (в последних стандартах), мне нравится на нем программировать, но я делаю это без использования каких-либо сложных концепций (ламер на C++, использующий только умные указатели вне функций)
Нет, раст мне не нравится, могли бы и лучше сделать, учитывая что не было легаси, аби и прочих штук...
аби
А у кого оно было?
Надо себя заставлять. Через пару месяцев разработки на Расте вы к С++ не вернётесь. Просто надо научить мозг новым концепциям.
А можете кратко описать чем плох Rust или дать ссылки на что-нибудь почитать об этом? Было бы интересно, спасибо
Осталось уговорить ребят из C-- все это сделать
Следующий код полностью заменяет юнион, не имеет никакого оверхеда относительно него и имеет более понятный пользователю интерфейс (emplace / destroy)
Как вы лихо запихали placement new
и reinterpret_cast
в constexpr-контекcт. Можете продемонстрировать как вы замените вот такой код с помощью вашего union_t
?
struct T {
union {
int n;
float f;
};
constexpr T(int a) : n(a) {}
constexpr T(float b) : f(b) {}
};
int main() {
constepxr T a = 1;
constexpr T b = 0.5f;
static_assert(a.n == 1);
static_assert(b.f == 0.5f);
}
И реальный кейс: как вы планируете реализовать стандартный std::optional
без union или std::variant
?
Как же их заменить? Рекусивным(или через множественное наследование) туплом с элементами одного типа(да, это было очевидно)))
Вы же это несерьезно, правда?
https://cppinsights.io/s/62550a23 и вот это у вас будет в каждом юните трансляции?
И реальный кейс: как вы планируете реализовать стандартный
std::optional
без union илиstd::variant
?
Как массив байт с правильным алигментом, прямо в статье код делающий всё что вам нужно. Если вы не знаете способа - не значит что его нет
Хорошо. Можете, пожалуйста, на https://godbolt.org привести код инициализации и использования вашего union_t в constexpr-контексте. Не важно каким компилятором и стандартом вы это сделаете?
Как массив байт с правильным алигментом, прямо в статье код делающий всё что вам нужно
И было бы здорово посмотреть, думаю не только мне, как вы с помощью массива байт реализуется ваш fucking::optional<T>
как literal type
, что требует стандарт.
Для optional и variant вообще то исключения в стандарте на использование в constexpr контексте. Никаким языковым способом не реализуемо на данный момент сделать их constexpr. Только внутренняя реализация компилятора может это позволить
Если бы это было возможно, то был бы разрешён reinterpret_cast и bit_cast указателей в constexpr контексте, но это запрещено
Никаким языковым способом не реализуемо на данный момент сделать их constexpr.
Это реализуется за счет union.
Заменить это всё можно одним фундаментальным типом byte и указателями, действительно: с помощью byte и системы типов С++ можно создавать любые типы, в том числе аналогичные int, double, float, bool и т.д. из фундаментального набора. Тут мы убиваем сразу несколько зайцев - нет больше исключений для фундаментальных типов в разрешении перегрузки, нет исключений в шаблонном коде для наследования( от фундаментальных типов нельзя наследоваться) ну и другие более мелкие исключения для подобных типов уходят в прошлое
И одним из этих убитых "зайцев" будет видимо constexpr.
нет не реализуемо, просто создать переменную в юнионе - бесполезно. Пользоваться юнионом на компиляции вы не сможете (так как на компиляции undefined behavior это ill formed программа)
Вы всерьёз смотрите на то, что выплёвывает cppinsights? Это не то что реально происходит, а лишь какая то демонстрация непонятная
https://godbolt.org/z/oW4zEc7hK
И во вторых, если в ЯЗЫКЕ нет массива, это не значит что РЕАЛИЗАЦИЯ(например от msvc) не сможет его сделать на built-in'ах, при этом ничего не потеряв вы убрали лишнюю вещь из языка. Перечитайте и подумайте над этим
если в ЯЗЫКЕ нет массива, это не значит что РЕАЛИЗАЦИЯ(например от msvc) не сможет его сделать на built-in'ах
То есть Вы предлагаете рассчитывать на нестандартные вендорские расширения (в ещё большей степени, чем это происходит сейчас)?
Вот вам очень похожий случай:
template <class _Ty, _Ty _Size>
using make_integer_sequence = __make_integer_seq<integer_sequence, _Ty, _Size>;
Никаких расширений. Вы не знаете как это реализовано. На уровне языка нет понятия index_sequence. Реализуется только через рекурсию внутри языка. Но как вы видите компилятор делает реализацию не через рекурсию, а через встроенный в компилятор механизм.
И в языке нет лишних сущностей и вы ничего не потеряли как пользователь
Если этого нет на уровне языка, то, вообще говоря, потерял - возможность перенести этот код на другой компилятор и знать, что он скомпилируется и отработает как ожидается.
Если другой компилятор не даёт вам этого, значит он не поддерживает стандарт языка, значит он не компилятор С++, значит вам не нужно переносить на него код
Это не то что реально происходит, а лишь какая то демонстрация непонятная
Хорошо, не нравиться cppinsight, давайте посмотрим на AST или трансляция кода это тоже не то, что реально происходит?
Вот для вашего примера
https://godbolt.org/z/3f1rMWG4e
А вот для обычного массива
https://godbolt.org/z/4T6hGoKnW
И во вторых, если в ЯЗЫКЕ нет массива, это не значит что РЕАЛИЗАЦИЯ(например от msvc) не сможет его сделать на built-in'ах, при этом ничего не потеряв вы убрали лишнюю вещь из языка.
Это также не значит что реализация СМОЖЕТ это сделать.
Но если предположить что смогла. Вот я могу взять clang и использовать стандартную библиотеку gcc или наоборот. По libc++, например, https://libcxx.llvm.org/#platform-and-compiler-support. Как это должно работать? Вы предлагает стандартизировать built-in'ы?
У компиляторов есть ограничение на шаблонные рекурсии, так что ваш аналог std::array работает только на крошечных примерах (для clang, если память не изменяет, до 512 ограничение)
На счет слова final - оно позволяет оптимизировать прямые вызовы функций final-типа, т.е. теряется оверхед на поход в vtable
если в ЯЗЫКЕ нет массива, это не значит что РЕАЛИЗАЦИЯ(например от msvc) не сможет его сделать на built-in'ах, при этом ничего не потеряв вы убрали лишнюю вещь из языка. Перечитайте и подумайте над этим
Практика показывает, что ничего не оптимизируется. Или оптимизируется абсолютно также как без final
Добавлять ключевое слово исключительно для оптимизации, ломая при этом шаблонный код - безумие
Ну и да, вообще-то array в статье реализован без рекурсии
Сложилось впечатление, что автору статьи С/С++ не подходит. Лучше ему поискать другой язык.
Уважаемый автор предлагает нечто очень странное, на мой взгляд.
То есть вот до отказа от таблицы вирт. функций еще можно было себе представить реализацию этого, но тут соломинка все же сломала верблюду хребет.
Если мне не изменяет склероз, то вот тут:
template<one_of<Ts...> U>
constexpr U& emplace(auto&&... args) {
return std::launder(new(data) U{std::forward<decltype(args)>(args)....});
}
std::launder
не нужен, т.к. у нас есть результат placement new, который "отмывать" уже не нужно. std::launder
потребовался бы вот в таком случае:
template<one_of<Ts...> U>
constexpr U& emplace(auto&&... args) {
new(data) U{std::forward<decltype(args)>(args)....};
...
return std::launder(reinterpret_cast<U*>(data));
}
PS. Статью целиком не осилил.
Короче, выбросить всё, что активно используется в embedded. Не, так не пойдет.
Я категорически против большинства ваших предложений:)
Вы предлагаете выбросить все простое и заменить это какими-то мозгодробительными решенями на шаблонах.
А я бы наоборот выкинул метапрограммирование на шаблонах, ограничил бы шаблоны их изначальным применением (параметризированные алгоритмы и структуры данных), а для метапрограммирования ввел бы нормальные императивные синтаксические макросы.
Ваши примеры при беглом взгляде совершенно непонятны. Наверное, если помедитировать над ними, то станет понятнее, но зачем? union, массивы, базовые типы - это основы, они просты и понятны. А вы предлагаете и дальше продолжить переносить кишки компилятора наружу, в шаблоны. И в результате С++ превратится в интерпретатор некоего функционального языка с уродским синтаксисом, на котором будет написан некий объектно-ориентинованный язык (с не менее уродским синтаксисом), и вот на смеси этих двух языков и предлагается писать программы обычным программистам.
Отчасти соглашусь с отказом от сишного приведения типа - оно неудобно для поиска в коде, т.к. нет ключевого слова. Также соглашусь с тем что typedef и using пересекаются, можно оставить что-то одно.
??? В чём проблема?) Я специально использовал здесь не [], чтобы самые умные читатели не начали причитать, что я тут синтаксис массива использую в реализации массива.
К оператору [] претензий нет, а вот к массиву в языке - есть. Как показано в статье, массив как языковая абстракция не нужен
Про union вообще не понял. Очень частая задача в с++ (я пишу под микроконтроллеры) это представить какую либо структуру как массив байт( например что бы сохранить на флеш, или отппавить кудато) Бывает так же , например, ситуация когда dword хочется представить как два int ну итд. Без подобных преобразований просто не обойтись. Как автор предлагает действовать без union?
Подобное преобразование нелегально в C++, но т.к. многие его используют по привычке из C, то компиляторы предпочитают не ломать такой код: https://stackoverflow.com/questions/11373203/accessing-inactive-union-member-and-undefined-behavior
у меня один вопрос, вот здесь:
template<one_of<Ts...> U>
constexpr void destroy_as() const {
reintepret_cast<const U*>(reinterpret_cast<void*>(data))->~U();
}
мне предлагается руками писать тип, который я хочу дестроить? Или я просто чего-то не понял?
Если union не нужен, то почему почти в каждом игровом движке, даже на C++17, они пишут самодельную реализацию Variant типа, под капотом которого union, а не шаблон c new? Или интерпретаторы с универсальными типами?
Реализация массива без массива:
Арифметика указателей не работает на void*
. Я бы понял, если бы каст был к char*
или byte*
, но так, как сейчас, это на конформном компиляторе не работает.
6 — typedef
typedef
можно положить в заголовочный файл, разделяемый между C и C++, using
— нет.
Holy C++