Pull to refresh

Comments 77

Там еще предложение от Саттера N4165 Unified Call Syntax.
Разница в том что Бьёрн предлагает чтобы методы также срабатывали как функции f(x,y) (что в принципе breaking changes), у Саттера таких изменений нет. Добавьте опрос пожалуйста какой предложение больше нравится.
UFO just landed and posted this here
С одной стороны — хорошо. С другой, как представишь, как отделять зёрна от плевел при анализе кода, так жуть берёт. А возможность достучаться к приватным или защищённым методам, полям не рассматривают? Да, думается, в обобщённом программировании такая штука пригодится.
UFO just landed and posted this here
Выдавать метод класса, за дискретную функцию? Даже не знаю, имхо — только болше путаници, к томуже не совсем понятно — как это будет дружится если метод класса виртуальный? Вот есть __stdcall F(CLASS1*, int) и есть virtual __thiscall CLASS0::F(CLASS0*, int); CLASS1:public CLASS0; И у Какой функции какой приоритет?
UFO just landed and posted this here
ADL чихать хотел на пространства имен.
Метод класса формально ничем и не отличается от функции, которой первым параметром передан указатель на объект этого класса. К примеру, если вешать хуки на методы какого-то класса, то нужно перенаправлять вызовы именно на функции с такими вот сигнатурами. На счёт приоритета в Вашем примере — сказано же, что приоритет у метода класса всегда выше.
UFO just landed and posted this here
Да, но что это меняет? Мы всегда можем «узнать» и метод класса и функцию с соответствующими параметрами, определить их эквивалентность или неэквивалентность. Приоритеты их тоже чётко указаны.
Всегда? В рантайм тоже можем? Связывание то при виртуальном вызове у нас рантайм происходит.
Ну связывание-то как-то же происходит. Да и вспомните тот же механизм переопределения операторов, которые могут быть как в классе переопределены, так и в независимой функции.
Это не имеет значения. Мы же остались на уровне имён, а не спустились на уровень реализации.
Просто немножко больше синтаксического сахара.
В старом С++ приходится писать переходник
void foo(Bar& b) { b.foo(); } // где Bar::foo может быть и виртуальным, и каким угодно

// для того, чтобы применить так
vector<Bar> bars;

foo(bars.front());
// или так
for_each(begin(bars), end(bars), ::foo); // кстати, begin() и end() - уже стандартизованные переходники

а в новом эта свободная функция будет сгенерирована неявно.
>>>приоритет у метода класса всегда выше

Даже если он имплементирован ниже по цепочке наследования?
Объявление его всё-равно ведь в базовом классе.
В пейпере от саттера вроде объясняется. При попытки вызова x.f() сначала будет произведён поиск по методам а потом по функциям. В случае с f(x) будет искаться сначала среди функций а потом среди методов.
UFO just landed and posted this here
А какова будет область видимости такой функции-не-члена? Вероятно, public, дабы не провоцировать людей на написание антипаттерна «паблик морозов». Еще видимо так можно будет доопределять только невиртуальные функции.

Вообще, эта штука может быть полезна во многих других местах. Например, можно объявить операторы приведения типов для «чужих» объектов, доставшихся нам «в наследство» или пришедших из сторонней библиотеки, и тем самым упростить жизнь в локальном коде. А вместе с семантикой перемещения (которую похоже тоже можно будет реализовать для неподдерживаемых сущностей), это будет еще приятнее.
Придется много чего переписывать. Например begin() и end() определены и как глобальные функции и как члены-функции некоторых коллекций.
В описании того-же begin() сказано, что он возвращает «the same as returned by cont.begin()». Так что переписывать не придётся, наоборот — просто выбросить явно лишний код.
Конечно! Они же эквивалентны. Просто придется чистить код.
Нет, достаточно будет выбросить глобальные begin/end из стандартной библиотеки. А пользовательский код будет работать без изменений и дальше.
Да интересно, но, наверное, придётся оговариваться, что вот такие имена функций-членов класса — зарезервивованы — и ни в коем случае не используйте их! Так как приоритет у функции-члена — выше. И, действительно, как уже сказали выше — это breaking changes.
И мультиметоды тогда естественнее можно будет ввести. Делаем С++ ещё более сложным \o/

В том же Dylan есть сахар object.method для method(object). Правда, только для обобщённых функций одного аргумента.
UFO just landed and posted this here
Это уже работает для перегрузки операторов, например operator+ можно сделать как членом с одним аргументом, так и свободной функцией с двумя.

Идея распространить это на все функции мне нравится
UFO just landed and posted this here
stl-контейнеры не проектировались для наследования.
UFO just landed and posted this here
UFO just landed and posted this here
Если Бьерну так хочется догнать шарп, не проще ли просто перейти на шарп?

Вот эти действия: «1. Попробовать вызвать x.f(y):...2. Если пункт №1 не удался» — их ведь на этапе компиляции предлагается делать? Тогда не очень ясно в чем польза. Почему просто не обновить либу.

К тому же надо понимать, что подобный стиль может вести к путанице.

в часности, если есть x.f(y) — то что это:
1. то ли это вызов функции f в обьекте «х» c параметром «y»
2. то ли это вызов функции f без параметров в обьекте «y» который является полем обьекта «x»

Помоему, если уже делать, то лучше такое делать через синтаксис partial classes
Почему просто не обновить либу.

Либа может быть не ваша и вы не можете ее менять.
Тогда будет риск, что частично будет использован f(x,y) а частично — позже написанный x.f(y), в зависимости от того, когда какой обьектник компилился. Это может привести к трудноуловимым ошибкам. Хотя, кого это в си интересовало.

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

И можно словить другие неприятности. Например, если юзаешь функцию init(Foo x). И если ты такую функцию обьявил, то получится, что классе Foo обьявлять init() уже нельзя.
В том виде, в котором эта фича интересна для меня — расширение функциональности чужих библиотек, которые обо мне ничего не знают, эти проблемы довольно легко обходятся. Например:
— могу ввести для своих операций таких операций уникальный префикс
— могу вызов таких операций обложить тестами
— могу в конце концов на regex-ах проверять перед сборкой, что в чужой библиотеке нет функций: MySuperFoo и MySuperBaz — которыми я расширил их классы.
UFO just landed and posted this here
Уникальный префикс — это считайте ограничение на свободу именования. Плохо.
Тесты — это время разработки. Тоже плохо
Регэксами как минимум увеличивает время сборки, и эти телодвижениянадо вносить в Makefile — тоже плохо

Оно все плохо не фатально, но по капельке, как медленная пытка, портит жизнь.

При этом, для дополнения функционала классов есть более логичный синтаксис. Пишешь что-то типа partial class{} и внутри добавлявляешь свой функционал.
Тесты — это время разработки. Тоже плохо

По моему самый правильный способ.
Я беру себе в проект чужую библиотеку, в какой-то момент понимаю, что в ней в каком-то классе не хватает метода foo(). Я его добавляю и согласитесь, я просто обязан написать на него тест, как и на любую другую обертку вокруг чужого кода. Это нужно хотя бы из соображений того, что следующая версия библиотеки может сломать мне весь мой код в самый неподходящий момент. Я должен зафиксировать поведение чужого кода, иначе я по ночам спать буду плохо.
Идем дальше, вас как я понял волнует ситуация, когда разработчики в следующей версии написали свою реализацию метода foo() и после этого все мое приложение начинает работать по другому, т.к. станет использовать библиотечный метод, правильно? Тесты от этого спасут, а вот как поможет синтаксис «partial class{}» — я не понимаю.
во-первых, частичный класс не сломает существующий код.
во-вторых, в данной ситуации он спасет очень просто — будет ошибка компиляции. Компилятор скажет, что такой метод уже есть.
Согласен, не подумал о том, что можно в случае совпадения отказываться компилировать. Не могу сходу придумать когда это может быть плохим решением.
Extension method концептуально ничем не отличается от partial class: также можно отказываться компилировать при конфликте имён. Partial class менее интуитивен, потому что так и захочется кроме фунций вносить в него поля, подклассы, френдов и т.п., а это сделать нельзя, если существует код, скомпилированный без учёта надстройки.
Экстеншен метод при конфликте имен нельзя просто брать и отказываться компилить — это сломает компиляцию существующего кода.

partial class более функционален по сравнению с экстеншен методом: туда можно накидать методов и полей, тогда как экстеншен метод может только работать с паблык полями и методами расширяемого класса.

Насчет сломает код скомпилированый без учета надстройки — к сожалению, это действительно так. Но это только потому, что формат старого кода не заточен под такую фичу. Поэтому следующий логичный шаг — формат, который бы такое поддерживал. Это действительно неприятный момент, но все равно, возможностей partial class дает потенциально больше — в частности, в случаях, когда скомпилированные без учета надстройки либы не используются — и тогда данной фичей можно пользоваться. Если бы это была жава, я б сказал что такое ни в коем случае нельзя, но зато такой подход абсолютно в духе си, поэтому — можно.
Не сломает, сейчас не же таких методов.

Введение partial class потребует пересмотра подходов к компиляции и линковке. Если сейчас файл lib.cpp инклюдит lib.h и компилится, больше ни от чего не завися, то добавление partial в lib-ext.h и lib-ext.cpp потребует форсированного включения lib-ext.h в компиляцию lib.cpp. С учётом того, через какие #if и другие инклюды бывает подключаются хидеры, это задача не имеет автоматического решения, придётся автору lib-ext перепахивать все cpp-файлы, использующие lib.h
Это вопрос реализации. «Под капотом» оно может быть рализовано точно так же, как и f(x,y), если нету полей. Если есть поля — нужен новый формат обьектников, но тут надо понимать, что это фича, которая синтаксисом f(x,y) вообще не поддерживается.
Наследование и инкапсуляция в нынешние времена явно не в моде.
UFO just landed and posted this here
А я и не говорил, что инкапсуляция где-то нарушается. Просто проблема недостаточности интерфеса в классическом С++ решалась наследованием либо инкапсуляцией, в зависимости от задачи. А решение недостаточности интерфейса таким способом выглядит как «наговнокодим по быстрому функцию и выдадим ее за интерфейс». Хотя внутренней логики С++ это и не нарушает, внешне выглядит именно так. Для расширения интерфеса обычно должно быть достаточно всего одного наследования. И да что значит «слишком хрупко»?
Например мы хотим расширить std::string, который встречается в 100500 местах по всему проекту.
Предложите наследоваться от него и заменять во всем проекте на mystring? А при общении с внешними библиотеками кастовать туда сюда?
И да что значит «слишком хрупко»?
То, что наследование — это не подтипизация. Если от A наследуется B, от которого наследуется C, а от него — D, то когда кто-то захочет отнаследовать ещё и E от D, то вполне можно забыть о том, что там в самом низу лежит A, у которого есть свои инварианты, которые, возможно, не совсем подходят для E.
UFO just landed and posted this here
Полезным с точки зрения инкапсуляции будет, как с перегрузкой операторов. Страуструп рекомендует объявлять операторы внешней функцией, если им не требуются закрытые члены. Таким образом, при проверке инвариантов и рефакторинге внутренностей мы просматриваем только операторы-члены.
Проблемы те же: интерфейс класса распадается на на члены и свободные функции, просматиривать глазами такой интерфейс неудобно, а порой и невозможно твердо понять в куче заголовочников, может ли еще где-то быть определен метод.
IDE и инструменты рефакторинга вполне себе могут собирать интерфейс из разбросанных по хидерам методов, как это успешно делается в C# для partial-классов.
Вот поэтому я и уточнил, что:
просматиривать глазами

А код часто приходится читать глазами в текстовых редакторах, в репозиториях… Читаемый интерфейс класса — это важно.
Именно! Это «фича» не должна пройти, из-за вреда читабельности. В C# тратил (в «той самой» IDE) из-за этого кучу времени, пытаясь найти несуществующий метод класса. А когда перемещённый код не компилируется так как надо импортировать какую-то неведомую сборку ради получения этой «функции-расширения» это вообще отлично.
f(x,y) означает ровно то же самое:
Попробовать вызвать x.f(y): если класс объекта х содержит метод f, который может принять аргумент y — используем этот метод.
Если пункт №1 не удался — проверяем, существует ли функция f, которая может принять аргументы x и y. Если это так — используем её.

Этот синтаксис ломает старый код, так что он вряд-ли будет введён. А вот если мы будем вначале пытаться вызвать именно то, что написано, а только потом пробовать альтернативный синтаксис — то ничего не ломается.
Этот синтаксис ломает старый код,

Как? По идее он ничего не ломает. По крайней мере я не могу придумать ситуацию которую он бы ломал.
class Foo
{
public:
    void Bar();
};

void Bar(Foo&);

int main(int,char**)
{
    Foo foo;
    Bar(foo);
}


Раньше вызывалась функция, с этим нововведением будет вызываться функция-член. Нет гарантий что в существующем коде они делают одно и то же — код сломан.
Раньше вызывалась функция, с этим нововведением будет вызываться функция-член.

Вообще то нет.
При попытки вызова x.f() сначала будет произведён поиск по методам а потом по функциям. В случае с f(x) будет искаться сначала среди функций а потом среди методов.
Это было-бы логично, но текст статьи говорит об обратном(и в оригинале тоже)
см. пример из начала статьи. На строке Bar(foo) будет произведена попытка вызова foo.Bar(), которая будет успешна. Т.е. вместо функции вызовется именно метод
из-за SFINAE гипотетически и ваш вариант может что-нибудь сломать
Имхо, нужно сделать так, чтобы по определению функции было явно видно, является ли она методом расширения. Так, в шарпе для этого используется модификатор this на уровне параметра. Это оправдано, т.к. в 90% случаев методы расширения используются для организации т.н. fluent interface, пример:

var x = 42;
x.Should().Be(42);

Обратите внимание, что в шарпе методы расширения можно применять к примитивным типам, и они могут не иметь дополнительных параметров.
Что значит не могут иметь дополнительных параметров?
public static void Is(this int x, int y)
{
42.Is(1);
}

Работает без проблем
Обратите внимание на Should() в моем примере выше, он имеет только один параметр — this int target.
А, я извиняюсь. Первый раз прочитал как «НЕ могут иметь доп параметров». Изза этого и удивился.
А зачем? В шарпе вообще нет понятия глобальных функций. А вот в плюсах, наоборот, они вполне идиоматичны, в т.ч. и как обертки над методами (std::swap, std::begin/end etc), для того, чтобы можно было их легко перегружать для сторонних типов. И для этого же есть ADL. Поэтому правило «любая функция является расширением» вписывается в эту картину вполне гармонично.
Кстати, раз уж пошли делать такие предложения — кто-то уже предлагал возможность использовать любую бинарную функцию как инфиксный оператор? Т.е. чтобы x foo y было равноценно foo(x, y). Можно было бы писать всякие if (foo hasEffectOn bar) { ... }.

А там уже и до автоматического каррирования и вызова функций без скобок недалеко, надеюсь :)

Каррирование грязных функций — плохая идея. Лучше автоматически такое не делать.

Обратите внимание, что методы расширения в С# имеют несколько ограничений:
метод расширения всегда должен быть объявлен как public static метод статического класса
метод расширения имеет доступ полько к public методам и свойствам расширяемого типа

Сперва по поводу второго ограничения: разумеется, метод должен иметь доступ только к public-вещам внутри класса. Иначе получается, что мы можем объявить новый метод, имеющий доступ к private-вещам, это нарушение инкапсуляции. Private на то и private, что есть фиксированное число методов, которые имеют доступ к private-вещам.
По поводу первого: зачем добавлять private метод? Ведь его всё равно никто не сможет вызвать (с учётом моего предыдущего замечания).
Так что совершенно правильные ограничения

Его смогут вызвать другие методы находящиеся в том же классе. Кстати, ограничения что метод -расширение должен быть открытым на самом деле нет: https://ideone.com/at5e4V

В случае когда x.f(y) и f(x,y) эквиваленты — вышеуказанный код абсолютно валиден (и красив).

Вот только в оригинале Страуструпа всё как раз наоборот — он считает нотацию f(x, y) более общей и красивой. Что нотация x.f(y) вызвана попросту тем обстоятельством, что в C++ (и том ООП-языке, из которого C++ подчерпнул свою ООП-идею) нет мультиметодов
auto v = std::vector<int> {1,2,3,4,5,6,7,8,9};
auto s = v.where([](int e){return e % 2 == 0; })
          .select([](int e){return e*e; })
          .sum();

Правда, красивый? Мне кажется — да.

Вы сейчас доказали не то, что x.f(y) и f(x, y) должны быть эквивалентны. Вы попросту доказали, что нужно разрешить функциям иметь свой собственный синтаксис. Т. е. конкретно для where нужен синтаксис, где сперва идёт контейнер, потом слово where, а потом предикат. Например, v.where([](int e){… }). Или v where [](int e){… }. Это напоминает мне BASIC, где встроенные функции имеют каждая свой синтаксис, например, функция LINE для рисования линии имеет синтаксис LINE (0, 0) — (1, 1)
>> Вы попросту доказали, что нужно разрешить функциям иметь свой собственный синтаксис. Т. е. конкретно для where нужен синтаксис, где сперва идёт контейнер, потом слово where, а потом предикат.

Дело в том, что именно такой синтаксис (эквивалентный subject-verb-object в естественных языках) является оптимальным для очень большого количества операций. Благодаря чему, собственно, нотация с точкой и является столь популярной. Если это предложение пройдет, то в плюсах точка и станет такой альтернативной нотацией. По сути, это эквивалентно инфиксной нотации в Haskell, где f x y можно записать как x `f` y.

А возможность задавать произвольный синтаксис для функций — это огромное усложнение парсера языка (и всех инструментов, на него завязанных, вроде редакторов/IDE). Или же, наоборот, упрощение с перекладыванием парсинга каждого конкретного вызова непосредственно на функцию, как в Tcl.
Sign up to leave a comment.