Search
Write a publication
Pull to refresh

Comments 46

Насколько понял Ваш основной аргумент в защиту расширений (а также аргументацию из статьи по ссылке): такие выражения:

x.f(y) и f(x, y) являются идентичными (х - инстанс класса, f - метод, y - некий аргумент). А если идентичны, то давайте делать расширения - ведь это так удобно.

Тут видится пара шероховатостей:

Если опираться на саму команду вызова (в ассемблерном коде), то это верно - да, оно идентично.

Если опираться на конструкции языка C++, то (по-моему) возникают вопросы с типом первого аргумента, передаваемым в функцию f(x, y).

Это сам инстанс? - А если его нельзя копировать (например, std::mutex)? Или копирование очень "жирное" по ресурсам?

Это константная ссылка? - И сразу ограничиваемся в вызове методов и доступе к публичным полям (упоминавшийся выше мьютекс - не залочить).

Обычная ссылка? - Может не получиться из константного объекта.

Плюс, это всё должно работать в случае x как r-value. И вот как-то это уже "не очень".

Как результат, утряска всех этих тонкостей может очень дорого обойтись и стандартизаторам, и программистам.

-

Почему в других языках это заходит?

Где-то объектами оперируют только через указатели, где-то аналогов r-value нет.

Возможно, в этом причина.

Я вот смотрю на весь этот код и не понимаю, а к чему вся эта графомания в примерах из статьи? Унаследуйте класс, добавьте методов по вкусу и пользуйтесь, не?

Унаследоваться можно далеко не от всего, и далеко не всем можно сказать, что надо использовать унаследованную форму.

Например, речь о примитивных типах. Или строки, конструирующиеся из строковых литералов.

Правильно ответили. Наследование это сложнее (из-за конструкторов) и не возможно, когда это библиотечный код. Да и обобщённый код сложнее написать, чтобы был один шаблонный метод и работал с множество разных типов. С шаблонными функциями это всё возможно, но пользоваться неудобно (пример - алгоритмы в C++).

Это сработает, только если вы контролируете место создания объекта. А если вам приходит уже созданный объект?

Мы же говорим про состояние до начала компиляции? Как я могу потерять контроль над местом создания объекта? А если объект приходит уже созданный, то это DLL export чтоли?

Это значит, что какая-то библиотечная функция уже вернула вам объект, например строку std::string, которую она создала. И тут у вас есть только возможность обернуть возвращённую строку в вашу строку с дополнительным методом.

Вы прикручиваете к экземпляру объекта дополнительный метод, чтобы необычным образом конструировать цепочки вызовов с ним. Ну, свежо и необычно. Но лучше добиться этого классическим синтаксисом - через точку. Пропатчить компилятор?

Ну вот патчить компилятор это вообще последнее дело

Про шероховатости. Не совсем их вижу. Мы же пишем функцию, там явно указываем тип ссылки на объект, *, &. const ... Компилятор только должен её увидеть и попробовать подставить. В приоритете конечно методы объекта, но потом можно пробовать и функции с подходящей сигнатурой. То что творится в шаблонах вас не смущает, SFINAE например, когда компилятор перебирает кучу всего подряд пытаясь найти подходящее?

Методы-расширения обобщаются до UFCS, т.е. это должно работать в обе стороны: функция может вызываться как метод от своего первого параметра, и метод может вызываться как функция (статический метод класса) с явной передачей первого параметра. Фича интересная, но почему-то стандартизаторы никак не могут договориться. Хотя казалось бы, всего лишь синтаксический сахар.

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

Да, всё верно. Вроде такая простая и полезная фича, а затянуть не могут. Мне не понятно. Зато выкатывают какие-то мудреные странные фичи для каких-то корнер кейсов.

Я поработал с этим в других языках, начиная с C# в 12 году, мне очень зашло, теперь жить без них не могу.

Я проэмулировал, оно немного коряво, но на практике пользоваться удобно, написал один раз помучился, но потом кайфуешь. Ну а про диалекты, чтож, в плюсах это на каждом шагу. Кто-то макросы изобретает, кто-то шаблонную магию, которую очень сложно понять. Таков язык. Если в проекте это используется системно, то все привыкают и тоже используют. Да и std::ranges в принципе это то же самое, только понавороченнее.

А Вы посмотрите, как это было сделано в языке D! Вот там это действительно красиво.

В C# всё-таки немного бойлерплейта надо писать.

Да в C# самая корявая реализация, но это было первое что я увидел в 2012. А так да UFCS то что надо. В Kotlin мне, например, нравится явность определения функции как метода:

// обычная функция
fun <T> first(list: List<T>) = list[0]

// метод - расширение
fun <T> List<T>.first() = this[0]

Кстати Wikipedia говорит, что

This has again, in 2023, been proposed by Herb Sutter[13] claiming new information and insights as well as an experimental implementation in the cppfront compiler.

Возможно мы в каком-то стандарте и увидим такой синтаксический сахар, может и доживём.

Программист читает код, видит:

std::cout << std::stoi("10") << std::endl;

И спокойно читает дальше.

Если же, программист читает код и видит:

std::cout << ("10" | toInt()) << std::endl;

То это потребует как минимум 5...10% рабочего времени на понимание того, что здесь происходит. А зачем, вообще лучше не задумываться.

То есть, это инструкция на тему как украсть рабочее время.

Ну потратил он один раз 5 минут разобрался и дальше проблем не возникает. Мне этот поход зашел в одном реальном проекте, ranges я не могу использовать потому я ограничен C++17, а цепочечные преобразования очень хочется, так как надоело писать постоянно императивные for. Я наклепал по быстрому различные функции над коллекциями, mapToVector, toSet, toVector, joinToString, reduce и теперь пишу однострочники в стиле ranges:

auto names = personList | filter([](auto p){return p.age < 30;}) | 
    map([](auto p){return p.name;}) | joinToString(", ");

Второе где пригодилось это при сериализации моделек данных в Json, мой последний пример. Я сперва пробовал сыграть на перегрузке функции toJson для разных типов, но в итоге столкнулся в шаблонном коде с тем, что компилятор довольно мудрёно ищет подходящие перегрузки и словил кучу проблем с неймспейсами и позицией в коде что первее чего объявлено. Переписал toJson на такой "метод расширения" и всё теперь работает просто и понятно, достаточно перегрузить оператор для нужного типа. Также можно писать конвертеры, например toDto, toModel:

person | toDto();
// или
personDto | toModel();

А если бы в плюсах не было бы столько приседаний, чтобы писать лямбды...

Сравните:

auto names = personList | filter([](auto p){return p.age < 30;}) | 
      map([](auto p){return p.name;}) | joinToString(", ");
var names = string.Join(", ", personList.Where(p => p.age < 30).Select(p => p.name));
people
|> Seq.filter (fun p -> p.age < 30)
|> Seq.map (fun p -> p.name)
|> String.concat ", "

Да в плюсах с лямбдами туго, мне тоже не хватает упрощённого синтаксиса.

var names = string.Join(", ", personList.Where(p => p.age < 30).Select(p => p.name));

В С# (кажется это он) ещё не очень лаконично. Вот то же на Котлине, вот где красота:

val names = personList.filter { it.age < 30 }.map { it.name }.joinToString(",")

В c# можно написать generic-расширение
IEnumerable<T>.joinToString(string delimiter)
Если хочется оставаться в рамках библиотечных функций, то
.Aggregate( (x,y) => x + "," + y);

Ну потратил он один раз 5 минут разобрался и дальше проблем не возникает.

Для разбора приведенного примера, а тем более для того чтобы подобные вещи читать бегло, нужно существенно более 5 минут.

Мы наблюдаем большое количество вложенности, обилие открывающих и закрывающих скобочек, код читается сложнее, можно запутаться какие параметры в какую функцию передаются.

Звучит похоже на рекламу магазина на диване:

Фактически вы просто использовали другой оператор для того, что уже давно и так известно и широко применяется -- перегрузка операторов << и >>. Это ведь и есть те самые "методы расширения". А умные указатели перегружают operator ->, не уверен, но вроде ничего не мешает перегрузить его как внешнюю функцию и даже будет привычный синтаксис вызова. Но как мне кажется, это сильно усложнит понимание написанного кода.

Фактически вы просто использовали другой оператор для того, что уже давно и так известно и широко применяется -- перегрузка операторов << и >>

Да, я перегрузил | по аналогии с библиотекой ranges, хотя можно было перегрузить и оператор >>, было бы нагляднее.

items >> map() >> filter() >> collect()

К сожалению оператор -> и . нельзя перегрузить вне класса. Да и это бы вносило путаницу с указателями.

Это ведь и есть те самые "методы расширения".

Да, но в стандартной библиотеке это применимо только к потокам. Я просто обобщил. Ну и флоу тут "обратный" потокам.

Зато можно сделать подлянку и перегрузить оператора ,

О даааа =)

Жалко что нельзя свои операторы определять, вот тогда бы вообще раздолье было бы )

Кстати в Котлин есть инфиксные функции, наверное и в других каких-то языках есть тоже. Можно делать так:

infix fun <A, B, C> then(a: A, b: T): C { TODO() }

// и потом писать такое
1 then "lala" then false

Пристёгивайтесь крепко!
module Program

open System

type Person = {
    age: int;
    name: string;
}

let people : Person list = [
    {age = 32; name = "Jake"};
    {age = 20; name = "Dave"};
    {age = 18; name = "Anna"}
    ]

people
|> Seq.filter (fun p -> p.age < 30)
|> Seq.map (fun p -> p.name)
|> String.concat ", "
|> Console.WriteLine


let inline (|?>) (collection : 'T seq) (predicate : 'T -> bool) =
    collection |> Seq.filter(predicate)

let inline (|=>) (collection : 'T seq) (map: 'T -> 'R) =
    collection |> Seq.map(map)
    
let inline (<+>) (collection : string seq) (delimiter: string) =
    collection
    |> String.concat delimiter

let inline (|+>) (collection : 'T seq) (delimiter: string) =
    collection
    |> Seq.map (fun x -> x.ToString())
    <+> delimiter
          
people
|?> fun p -> p.age < 30
|=> fun p -> p.name
<+> ", "
|> Console.WriteLine

есть инфиксные функции

В ObjectiveC было такое:

- (int)add:(int)x to:(int)y;

int result = add: 2 to: 3

Аргументы идут внутри названия метода!

что насчёт оператора "->*", кстати? Я глянул: его можно спокойно вне класса перегрузить. И по общей логике подходит. Единственное, не превращать его использование в то, что в первой строчке, ибо приоритет у него чуть ниже, чем у "()", что не совсем интуитивно:

(obj_ptr ->* ptr) (param) //реальность использования указателей на метод
obj ->* extention(param) //ожидание или если писать как конвеер

Не знаю, о чём думали создатели стандарта

О, я даже и не знал что такой оператор есть ) Да, синтаксис вызова неудобный, не подойдёт

Библиотека std::ranges основана на библиотеке range-v3, которая работает на C++14 и новее. Поэтому можно эту библиотеку добавить в свой проект. Кроме того, на сколько я помню, в std::ranges вошли не все возможности из range-v3.

Ссылка: https://github.com/ericniebler/range-v3

Да можно. Но опять таки это только про работу над списками. А хочется применить не только для списков. Надо попробовать старые ranges затащить в мой C++17 проект, вроде сторонних зависимостей нет

Эта фича довольно старая, но применять её надо с осторожностью. Как писали выше, это черевата созданием нового диалекта для конкретно вашей либы а так же нарушением ADL, что иногда плохо заканчивается.

Использование функций для этого не совсем кошерно, можно использовать constexpr переменные, дабы получить более вкусный синтаксис. Условно (value->as_int) навариваем сюда концептов и получаем забавный способ что-то сделать

Ну я это не в библиотеке использую, а в своём проекте. В библиотеке я, наверное бы, не стал навязывать такие интерфейсы.

Использование функций для этого не совсем кошерно, можно использовать constexpr переменные, дабы получить более вкусный синтаксис. Условно (value->as_int) навариваем сюда концептов и получаем забавный способ что-то сделать

Я думал о глобальных константах вместо функций. Но есть функции с параметрами, их constexpr переменными не сделаешь.

Смотря какие функции, кейсов довольно много, но чем новее стандарт тем больше позволяется делать в constexpr выражениях.

> Не разбирался в std::ranges

А лучше бы разобрался и узнал что через adl можно замечательно перегружать любые имена функций, а не только пайпы и одним лёгким ниеблоидом их диспатчить (см. реализацию std::ranges::begin)

ниеблоидом

Сперва подумал что это что-то на матерном ) Почитаю про это )

Но я просто хотел показать как проэмулировать методы расширения. Ranges прикручивать не каждому надо, и заниматься высшей магией с ниеблоидами.

Отлично. Давно уже, где-то с 17 стандарта, тоскую без расширений. Фича-то, казалось бы, простецкая и безопасная, но такая нужная.

Обязательно опробую ваш подход

Как по мне, намного более удобная фича Kotlin, которой мне не хватает в C++ - это чейн-функции вида also/apply/let/run/ ...

Так я же привёл пример реализации функции transform, это как раз и есть прямой аналог let в Kotlin. Остальные функции можно сделать по аналогии.

11 | transform([&](auto v){ return (std::stringstream() << v).str(); })

И я надеюсь, что компилятор это способен оптимизировать до простых вызовов.

Глупости. Это разве похоже на вызов через точку? Ни разу т.к. в плюсах нежелательно перегружать оператор "точка"

И плюс, в плюсах нет возможности последний аргумент являющийся лямбдой вынести за круглые скобки и не писать их вообще:

func(lambda)

vs

func { /*lambda body goes here */ }

В вашем комментарии речь шла о чейн функциях, в моем примере они есть. Не понимаю в чем вы видите различие вызов через точку или вызов через | ? Семантически это одно и то же. И да лямбды в плюсах уродливые, тут ничего с этим не сделаешь.

И в плюсах оператор точку не то чтобы нежелательно, её нельзя перегрузить для чужого класса.

Лямбда - это функциональный объект анонимного типа. То есть у него есть оператор круглые скобки с необходимыми аргументами, как и у функции. А захваченные переменные становятся полями такого объекта.

Объект лямбды можно сконструировать:

auto lambda = (auto v){ return (std::stringstream() << v).str(); };

Тип лямбды можно вывести через decltype(lambda).

Объект лямбды можно передать в функцию.

Новый объект лямбды можно сконструировать.

В языке запретов на это никаких нету. Но смысл лямбды как раз в том, что нет смысла знать её конкретный тип и не надо писать код по конструированию - синтаксический сахар сделает это за вас.

Не совсем понял к чему тут объяснение что такое лямбда? Причем, у вас пример не правильный, не хватает [] в начале.

синтаксический сахар сделает это за вас.

Какой синтаксический сахар? Синтаксис лямбды в C++ сложно назвать сахаром 😅

Да, недокопировал скобки. Но суть от этого не меняется.

Но, простите, что есть лямбда, если не синтаксический сахар поверх функционального объекта, который суть под капотом? Аналогичные вещи можно было делать и до появления лямбд, но руками. А теперь можно сделать то же самое, но короче и лаконичнее.

Sign up to leave a comment.

Articles