>Вопрос к сообществу: какой вы считаете правильный перевод термина «future» в контексте статьи?
Имхо, не стоит его переводить. Википедия предлагает несколько переводов, которые где-то используются, но кажется, в реальной жизни они не используются. Терминал попался такой, который переводится плохо (переводы либо длинные, либо звучат тупо), поэтому везде устоялась калька «фьюче» и её более обрусевшие варианты типа «футуры», «фьючеры», «фьючерсы» (это ещё и с экономикой пересекается), можно выбирать любой.
P. S. В C++11 на стадии разработки предлагались типы std::shared_future и std::atomic_future. Вот не хотел бы я поледний переводить.
Есть ещё варианты в стиле «человек с опытом паскаля решил помочь в опенсорс-проекте или на работе перекинули за отсутствием других возможностей», вполне себе типичная ситуация, не раз видел. Синтаксис знакомый, общие принципы можно по-быстрому где-то прочитать, сел и пошёл писать. Или этот ваш «начинающий профессионал» профукал и забыл этот момент в учебнике, не все же отличники.
> Это уже не для новчика.
Ну, не знаю, мне кажется, что новичок вполне может сразу гуй писать. Это студенту такое не дадут, а заставят до посинения писать алгоритмы сортировки, но студент != новичок.
> Это, конечно, может быть проблемой
Главная на самом деле проблема — это то, что такое знание энфорсится в программиста. Т.е. это правило написано в каких-то книжках, на любом форуме вам авторитетно объяснят, что вы идиот и должны сначала были читать учебник, но в лучших традициях C++ требование не прописано в стандарте, программисту разрешается стрелять в ногу «и так тоже», и вся надежда на добрую волю IDE и компилятора с их подсказками (которые программист конечно же не факт, что прочитает или не проигнорирует).
> Ресурсов это не сожрет.
Как же не сожрёт, когда сожрёт? :) И размер объекта вырастет:
0 ➜ compileOnce 'class A { public: ~A(){}}; class B { public: virtual ~B(){}}; std::cout << sizeof(A) << std::endl << sizeof(B) << std::endl;'
1
8
И производительность ухудшится (накладные расходы на походы в vtable). Другое дело, что важным это замедление станет ещё хрен знает когда, но факт есть :)
А ещё я видел библиотеку, где объявление реализации интерфейсного метода с virtual приводило к сегфолту! (т.е. в интерфейсе он был не virtual). Но это уже совсем другая история.
> И в их числе должны быть не только «простота допущения ошибки», но и «простота её поиска\исправления», и «накладные расходы по её автоматическому выявлению»…
Это уже больше переход в обсуждение «чья реализация оказалась лучше», а не «почему так», я, пожалуй, в ней участвовать не буду :) Хорошо, что мы под конец друг друга поняли.
Ну тут как. Вы можете наложить на T ограничения (типа для всех T, которые реализуют какие-то определённые типажи), и если вы в дефолтной реализации хотите использоввать какие-то свойства T, то очевидно, что это придётся делать. Но в общем и целом получается, что да, всем, тут полиморфизм в стиле шаблонов C++, если бы для них наконец допилили концепты.
Я честно постарался и не смог придумать ситуацию, зачем мы можем не хотеть давать фичу всем классам, которые удовлетворяют требованиям этой фичи. По идее это плюс в любой непонятной ситуации, если мы не ограничиваем применение алгоритма конкретным типом.
> Мы не можем сказать, что было мотивом именно такой реализации системы наследования в расте, и не думаю, что, к примеру, Хор через пару дюжин дней или лет признается
Не, ну в голове-то он может думать что угодно, но процесс обсуждения и разработки в целом весь на гитхабе и на internals.rust-lang.org есть :) Оригинальное обсуждение дизайна я не нашёл, но вот, например, очень интересная старенькая дискуссия про добавление наследования структур. Оттуда ещё и можно ещё глубже по ссылкам сходить.
> Что до исходной аргументации, она так же не к месту. В Rust нет полноценного наследования. Есть реализация интерфейсов, то есть АОП, но не ООП. Сравнение плюсов с растам вообще не имеет смысла в этом контексте
Это, имхо, уже демагогия. Классическое наследование в ООП подразумевает наследование данных и поведения, в расте есть второе, но нет первого. Я слабо знаком с АОП и мне кажется, что оно вообще не про то, но готов поверить на слово. В любом случае, мы вынуждены сравнивать, просто потому что используем язык для решения задачи, а не потому что «там ООП». Даже исходный тред NeoCode начинается именно с жалобы, что ООП не такой.
Наложенные ограничения — это причина разницы, мы же работаем с последствиями.
Страуструп с вами, я не говорю, что виртуальный деструктор — это какая-то бага, что она меня бесит или что-то ещё в этом же духе.
Вся моя аргументация относится к исходному поинту дискуссии: в C++ есть потенциальные грабли с памятью при таком наследовании, неважно, неопытный/неумелый ты программист, или баг не отследили в процессе рефакторинг, важно, что этот проезд достаточно легко стриггерить. И система наследования в расте сделана так, чтобы этого проезда избежать, а не чтобы «не как в C++». Вот и всё.
Давайте попробуем сначала :) Попробуйте представить, что вы зелёный новичок, а я попробую рассказать, как он может воспринимать это место в C++.
Мы знаем, что есть класс, у него есть поля и методы. Мы можем создавать новые объекты оператором new и удалять — оператором delete. Можно создать поле-объект в конструкторе и удалить в деструкторе.
Ещё мы знаем, что классы можно наследовать. При этом если мы конструируем класс-наследник, у него вызовется и конструктор базового класса, и конструктор наследника. Аналогично с деструктором, мы просто унаследовались, и вот уже разрушается и наследник, и базовый класс.
Конструкторы и деструкторы для нас при этом отделены от обычных функций: мы знаем, что они вызываются каскадом, а обычная унаследованная функция не будет звать функцию базового класса, если только в ней явно этот вызов не написать. И вот в этом месте таится наш подвох: мы знаем, что есть виртуальные функции, которые нужны, чтобы из базового класса звать реализацию наследника, но при этом и в голову не придёт, что то же самое может быть нужно для деструктора (тем более, что виртуальных конструкторов вообще не бывает), потому что он вроде как и так зовёт обе реализации.
Более того, конкретно в этом месте можно напороться ещё и потому, что общепринятая инструкция про виртуальные деструкторы ничего не говорит про динамическое выделение памяти, а учит, что виртуальный деструктор вам нужен, если вы пишете хотя бы одну чисто виртуальную функцию. А у нас таких и нет.
> 1. Многие водители видят заранее, что человек будет перестраивать в их ряд
> 2. Многие водители заметив сзади лихача-шашечника переводит подсознательно фокус на него и считают траекторию. Т.е водитель уже готовится.
Кажется, ничего такого, чего не увидели бы камеры ИИ.
Это ни капельки не логично, если задуматься. Более того, не знаю, что вы имеете в виду под «особой» инициализацией, но нам достаточно иметь в классе любое динамическое выделение памяти, чтобы получить отличный проезд:
class A {
private:
int *x;
public:
A() : x(new int(1)) {}
~A() { delete x; }
};
class B : public A {
private:
int *x;
public:
B() : x(new int(2)) {}
~B() { delete x; }
};
int main() {
A *b = new B;
delete b;
return 0;
}
Вот в этом уточнении: «правильно написанный» — вся соль :)
Идея раста заключалась в том, чтобы не дать возможности случайно или по незнанию напороть ерунды в этом месте, неважно, понимаешь ты его полиморфизм или нет. Т.е. либо код не скомпилируется, либо какого-то проезда по памяти не будет.
Как ни странно, это сделано не просто так (в расте вообще нет вещей, которые делались с мыслью «давайте не как в C++»).
В расте нет наследования типов и есть наследование типажей, потому что первое в C++ приводит к различным проблемам с памятью (каждый программист на C++ в этом месте может и захочет сказать: «да надо просто знать несколько простых правил, а если ты их не знаешь, то и нечего программировать, это азы языка», но это плохая контраргументация против «нужно знать несколько простых правил обращения с типами в раст»).
В расте это место намеренно упростили, в результате чего поверх структуры можно реализовывать любые интерфейсы, наследовать функциональность, но при этом: практически* любой объект можно удалить фактически простым free(), а скопировать — простым memcpy(). И не иметь при этом проблемы с памятью и головную боль с виртуальным наследованием, виртуальными деструкторами и вот этим вот всем.
* конечно, для объектов типа fd/socket придётся реализовывать типаж Drop — аналог dispose в C# и close() в Java.
Фактически это и есть си с классами, только лучше (вспомните, например, как си живёт со структурами типа sockaddr_in и sockaddr_in6 в стандартной библиотеке: они вообще никак не наследуются, но имеют обязательный список одинаковых первых полей, а в памяти кастуются к «базовому» классу).
Не скажу за jabber.ru, я лично сталкивался с таким увлекательным рядом проблем:
* при отсутствии взаимной авторизации gmail может вообще творить что угодно (например, сообщение абоненту gmail будет возвращаться тебе в неизменном виде как бы от его имени, ему оно при этом не дойдёт. В обратную сторону сообщения при этом ходят)
* проблемы с видимостью пользователей в сети
* какие-то самопроизвольные действия с MUC (например, никнейм периодически принудительно меняется на имя-фамилия из гуглового профиля)
* ещё какая-то самодеятельность с контактами того же рода
* ещё видел смешную ситуацию, не уверен, может ли её сейчас стриггерить злоумышленик, вероятнее всего нет: если где-то в apps for domains в гугле включен жаббер-сервер для домена, то гугл плюёт на реальные записи DNS, отпинывает s2s коннекты от настоящего сервера этого домена и соответственно отказывается доставлять туда сообщения. Это конечно минорщина, но дебажить такое неприятно.
Вы не то что неудачно выбрали оператор, а вы в каждом примере присваивающе-модифицирующего оператора вместо void или ссылки на результат присвоения возвращаете другой новый объект, что идёт вразрез со стандартной библиотекой и общепринятым подходом.
Я давно это сделал чуть ниже.
Главная ошибка этой статьи заключается в том, что она подталкивает гипотетическую ЦА к имплементации операторов с ошибками и там, где это совершенно не нужно.
Имхо, не стоит его переводить. Википедия предлагает несколько переводов, которые где-то используются, но кажется, в реальной жизни они не используются. Терминал попался такой, который переводится плохо (переводы либо длинные, либо звучат тупо), поэтому везде устоялась калька «фьюче» и её более обрусевшие варианты типа «футуры», «фьючеры», «фьючерсы» (это ещё и с экономикой пересекается), можно выбирать любой.
P. S. В C++11 на стадии разработки предлагались типы std::shared_future и std::atomic_future. Вот не хотел бы я поледний переводить.
Ну, не знаю, мне кажется, что новичок вполне может сразу гуй писать. Это студенту такое не дадут, а заставят до посинения писать алгоритмы сортировки, но студент != новичок.
> Это, конечно, может быть проблемой
Главная на самом деле проблема — это то, что такое знание энфорсится в программиста. Т.е. это правило написано в каких-то книжках, на любом форуме вам авторитетно объяснят, что вы идиот и должны сначала были читать учебник, но в лучших традициях C++ требование не прописано в стандарте, программисту разрешается стрелять в ногу «и так тоже», и вся надежда на добрую волю IDE и компилятора с их подсказками (которые программист конечно же не факт, что прочитает или не проигнорирует).
> Ресурсов это не сожрет.
Как же не сожрёт, когда сожрёт? :) И размер объекта вырастет:
И производительность ухудшится (накладные расходы на походы в vtable). Другое дело, что важным это замедление станет ещё хрен знает когда, но факт есть :)
А ещё я видел библиотеку, где объявление реализации интерфейсного метода с virtual приводило к сегфолту! (т.е. в интерфейсе он был не virtual). Но это уже совсем другая история.
Это уже больше переход в обсуждение «чья реализация оказалась лучше», а не «почему так», я, пожалуй, в ней участвовать не буду :) Хорошо, что мы под конец друг друга поняли.
Я честно постарался и не смог придумать ситуацию, зачем мы можем не хотеть давать фичу всем классам, которые удовлетворяют требованиям этой фичи. По идее это плюс в любой непонятной ситуации, если мы не ограничиваем применение алгоритма конкретным типом.
без дальнейших действий с точки зрения MyStruct. Частные специализации её просто перекроют, как шаблоны в C++.
Не, ну в голове-то он может думать что угодно, но процесс обсуждения и разработки в целом весь на гитхабе и на internals.rust-lang.org есть :) Оригинальное обсуждение дизайна я не нашёл, но вот, например, очень интересная старенькая дискуссия про добавление наследования структур. Оттуда ещё и можно ещё глубже по ссылкам сходить.
> Что до исходной аргументации, она так же не к месту. В Rust нет полноценного наследования. Есть реализация интерфейсов, то есть АОП, но не ООП. Сравнение плюсов с растам вообще не имеет смысла в этом контексте
Это, имхо, уже демагогия. Классическое наследование в ООП подразумевает наследование данных и поведения, в расте есть второе, но нет первого. Я слабо знаком с АОП и мне кажется, что оно вообще не про то, но готов поверить на слово. В любом случае, мы вынуждены сравнивать, просто потому что используем язык для решения задачи, а не потому что «там ООП». Даже исходный тред NeoCode начинается именно с жалобы, что ООП не такой.
Наложенные ограничения — это причина разницы, мы же работаем с последствиями.
Вся моя аргументация относится к исходному поинту дискуссии: в C++ есть потенциальные грабли с памятью при таком наследовании, неважно, неопытный/неумелый ты программист, или баг не отследили в процессе рефакторинг, важно, что этот проезд достаточно легко стриггерить. И система наследования в расте сделана так, чтобы этого проезда избежать, а не чтобы «не как в C++». Вот и всё.
Мы знаем, что есть класс, у него есть поля и методы. Мы можем создавать новые объекты оператором new и удалять — оператором delete. Можно создать поле-объект в конструкторе и удалить в деструкторе.
Ещё мы знаем, что классы можно наследовать. При этом если мы конструируем класс-наследник, у него вызовется и конструктор базового класса, и конструктор наследника. Аналогично с деструктором, мы просто унаследовались, и вот уже разрушается и наследник, и базовый класс.
Конструкторы и деструкторы для нас при этом отделены от обычных функций: мы знаем, что они вызываются каскадом, а обычная унаследованная функция не будет звать функцию базового класса, если только в ней явно этот вызов не написать. И вот в этом месте таится наш подвох: мы знаем, что есть виртуальные функции, которые нужны, чтобы из базового класса звать реализацию наследника, но при этом и в голову не придёт, что то же самое может быть нужно для деструктора (тем более, что виртуальных конструкторов вообще не бывает), потому что он вроде как и так зовёт обе реализации.
Более того, конкретно в этом месте можно напороться ещё и потому, что общепринятая инструкция про виртуальные деструкторы ничего не говорит про динамическое выделение памяти, а учит, что виртуальный деструктор вам нужен, если вы пишете хотя бы одну чисто виртуальную функцию. А у нас таких и нет.
> 2. Многие водители заметив сзади лихача-шашечника переводит подсознательно фокус на него и считают траекторию. Т.е водитель уже готовится.
Кажется, ничего такого, чего не увидели бы камеры ИИ.
Идея раста заключалась в том, чтобы не дать возможности случайно или по незнанию напороть ерунды в этом месте, неважно, понимаешь ты его полиморфизм или нет. Т.е. либо код не скомпилируется, либо какого-то проезда по памяти не будет.
В расте нет наследования типов и есть наследование типажей, потому что первое в C++ приводит к различным проблемам с памятью (каждый программист на C++ в этом месте может и захочет сказать: «да надо просто знать несколько простых правил, а если ты их не знаешь, то и нечего программировать, это азы языка», но это плохая контраргументация против «нужно знать несколько простых правил обращения с типами в раст»).
В расте это место намеренно упростили, в результате чего поверх структуры можно реализовывать любые интерфейсы, наследовать функциональность, но при этом: практически* любой объект можно удалить фактически простым free(), а скопировать — простым memcpy(). И не иметь при этом проблемы с памятью и головную боль с виртуальным наследованием, виртуальными деструкторами и вот этим вот всем.
* конечно, для объектов типа fd/socket придётся реализовывать типаж Drop — аналог dispose в C# и close() в Java.
Фактически это и есть си с классами, только лучше (вспомните, например, как си живёт со структурами типа sockaddr_in и sockaddr_in6 в стандартной библиотеке: они вообще никак не наследуются, но имеют обязательный список одинаковых первых полей, а в памяти кастуются к «базовому» классу).
* при отсутствии взаимной авторизации gmail может вообще творить что угодно (например, сообщение абоненту gmail будет возвращаться тебе в неизменном виде как бы от его имени, ему оно при этом не дойдёт. В обратную сторону сообщения при этом ходят)
* проблемы с видимостью пользователей в сети
* какие-то самопроизвольные действия с MUC (например, никнейм периодически принудительно меняется на имя-фамилия из гуглового профиля)
* ещё какая-то самодеятельность с контактами того же рода
* ещё видел смешную ситуацию, не уверен, может ли её сейчас стриггерить злоумышленик, вероятнее всего нет: если где-то в apps for domains в гугле включен жаббер-сервер для домена, то гугл плюёт на реальные записи DNS, отпинывает s2s коннекты от настоящего сервера этого домена и соответственно отказывается доставлять туда сообщения. Это конечно минорщина, но дебажить такое неприятно.
P.s. За сигнатуру я бы повесил вас на сосне.
И где-то есть ещё и видео?!
Главная ошибка этой статьи заключается в том, что она подталкивает гипотетическую ЦА к имплементации операторов с ошибками и там, где это совершенно не нужно.