Функция для использования прогнозов в устаревших алгоритмах

Мне нравятся «проекции», представленные библиотекой диапазонов, и я хотел бы использовать их в алгоритмах, которые еще не были изменены.

У меня была идея использовать их с помощью такой функции:

#include <utility>
#include <functional>

template<typename TFn, typename TProj = std::identity>
auto
constexpr proj(TFn &&Fn, TProj &&Proj = {})
{
  return [&Fn, &Proj]<typename... TArgs>(TArgs &&...Args)
  {
    return std::invoke(std::forward<TFn>(Fn),
                       std::invoke(std::forward<TProj>(Proj), std::forward<TArgs>(Args))...);
  };
}

Эта функция позволяет писать, например:

  std::sort(std::execution::par_unseq, v.begin(), v.end(), proj(std::less{}, &Pair::Key));

вместо:

  std::sort(std::execution::par_unseq, v.begin(), v.end(), [](auto const &lhs, auto const &rhs) { return lhs.Key < rhs.Key; });

1 ответ
1

Обзор дизайна

Не думаю, что вы много думали о том, что эта библиотека будет на самом деле полезный за.

Позвольте мне начать с рассмотрения идеи proj() в полной изоляции, игнорируя любой реальный контекст и просто рассматривая это как умный трюк. С этой точки зрения я бы сказал, что proj() неплохая идея… но, возможно, не лучший способ ее решения.

Вот что называют диапазоны sort() может выглядеть так:

std::ranges::sort(b, e, {}, &Pair::Key);

Вот что proj() обещает, в лучшем случае:

std::sort(b, e, proj(std::less{}, &Pair::Key));

Вот что еще возможно в C ++ 17, и, возможно, даже раньше, если вы готовы поработать над повторной реализацией std::invokeи т. д. (конечно, это также возможно в C ++ 20, но … мы скоро перейдем к этому вопросу):

myns::sort(b, e, {}, &Pair::Key);

То есть вы можете реализовать оболочку вокруг std::sort() которая обрабатывает проекцию прозрачно.

Для пользователя это короче, проще и не требует ручного указания компаратора.

Не говоря уже о том, что когда я может перейти на C ++ 20 и использовать всю мощь std::ranges, все, что мне нужно сделать (в основном), это найти-заменить для myns к std::ranges. Если бы у меня были эти маленькие proj() подвызовы повсюду, преобразование кода становится МНОГО более сложный и, возможно, утомительный и / или опасный.

И пока вы это делаете, почему бы не реализовать myns::sort() есть нейблоид, чтобы получить все сладкие, сладкие преимущества, которые идут вместе с этим?

Итак, если вы В самом деле хотите получить выгоду от прогнозов, почему бы вам этого не сделать?

Да, я понимаю, что в основном говорю «повторно внедрить std::ranges». Как половинчатое решение proj() только наполовину удовлетворяет. Какой в ​​этом смысл? Если вы собираетесь реализовать библиотечный инструмент, чтобы облегчить жизнь программистам, зачем это делать наполовину? Кому выгодно посредственное решение? Я предполагаю, что человек, пишущий библиотеку, может сэкономить на некоторой работе … но весь смысл написания библиотеки состоит в том, что автор библиотеки берет на себя дополнительную работу, поэтому библиотека пользователи не обязательно. Упрощение работы библиотеки писатель глупо. Вы должны сконцентрироваться на том, чтобы облегчить задачу пользователи.

И, кстати, есть еще куча сложностей с созданием чего-то вроде proj() обычно можно использовать, о чем я не упомянул. Например, вы создали вариант, который работает с большинством стандартных алгоритмов … но он не будет работать с std::find() (нет предиката, поэтому нет Fn). Не будет работать с std::merge() (имеет предикат, но имеет два прогнозы). К тому времени, когда вы разберетесь со всем этим и все это достаточно тщательно протестируете, чтобы показать, что он заслуживает доверия в производственном коде, вы могли бы просто использовать range-v3.

Кроме… и это самая большая проблема всей этой идеи…

Этот код даже не компилируется на C ++ 17. Для этого нужен C ++ 20. C ++ 20… где… std::ranges уже существует.

Так для кого конкретно эта библиотека?

  • Это не может быть для пользователей C ++ 17 (или более ранних версий). (Не компилируется.)
  • Это не имеет смысла для пользователей C ++ 20 (и более поздних версий). (Потому что, если им нужна проекция в устаревшем алгоритме, они могут просто использовать std::ranges версия. В вашем примере std::ranges::sort().)

Вы ориентируетесь на гипотетическое подмножество пользователей, которые нацелены на C ++ 20 в своем проекте, но используют компилятор / библиотеку C ++ 20 с лямбда-шаблонами, std::identity, и constexpr std::invoke… но нет алгоритмы диапазонов? Это, мягко говоря, странная целевая группа. Я имею в виду, кто нацелен на версию C ++ или использует конкретную функцию, например, проекции, без компилятора / библиотеки, которая ее действительно поддерживает? Не говоря уже о том, что это довольно эфемерная группа, потому что, предположительно, если они нацелены на C ++ 20, они могут просто использовать std::ranges как только необходимые им функции станут доступны в выбранном компиляторе / библиотеке, делая proj() просто странная временная задержка, которую они использовали бы какое-то время, а потом должны были бы избавиться от своей кодовой базы.

Итог: proj() не имеет смысла. Единственные люди, которые мог использовать это … в действительности не нужно.

(Или же, В ЛУЧШЕМ СЛУЧАЕ, они могли бы использовать это СЕЙЧАС ЖЕ, на тот короткий период, когда выбранная ими платформа поддерживает все необходимое для создания proj() работает, но не включает std::ranges. Но, честно говоря, я лучше напишу лямбду, чем буду использовать загадочную стороннюю библиотеку, которая будет полезна только в течение нескольких месяцев, а затем превратится в унаследованное бремя обслуживания.)

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

Обзор кода

Конечно, здесь не так уж много кода, но…

auto
constexpr proj(//...

Довольно странно писать constexpr после auto. Я никогда не видел такого раньше. Я не вижу разумной причины для принятия этого стиля; это только может вызвать путаницу.

constexpr proj(TFn &&Fn, TProj &&Proj = {})

Размещение модификаторов типа рядом с именем переменной, а не с типом, является соглашением C, а не соглашением C ++. В C ++ типы имеют большее значение, поэтому имеет смысл сказать TFn&& Fn.

Кроме того, немного своеобразно называть переменные с UpperCamelCase. UpperCamelCase обычно зарезервирован для имен аргументов шаблона. Другими словами, TFn&& fn более обычный.

template<typename TFn, typename TProj = std::identity>
auto
constexpr proj(TFn &&Fn, TProj &&Proj = {})
{
  return [&Fn, &Proj]<typename... TArgs>(TArgs &&...Args)
  {
    return std::invoke(std::forward<TFn>(Fn),
                       std::invoke(std::forward<TProj>(Proj), std::forward<TArgs>(Args))...);
  };
}

Больше всего меня беспокоит в этой функции то, что вы:

  1. взять два аргумента функции как ссылки для пересылки
  2. поместите их в замыкания как ссылки lvalue; тогда
  3. отправьте их в закрытие.

Теперь я признаю, что я не сел и не рассуждал так тщательно, как он того заслуживает (потому что уже поздно и потому что я не вижу смысла функции в любом случае), но даже бегло осмотреть это пахнет неправильный. Вонючая часть состоит в том, что вы «забываете», что имеете дело с пересылкой ссылок на втором этапе.

Если два параметра являются lvalues, то ни вреда, ни фола. Все ссылки переадресации будут свернуты до ссылок lvalue, а forward()s выродится в… ну, ничего, эффективно; просто ссылки lvalue. Таким образом, у вас есть ссылки lvalue на всем протяжении. Все круто.

Но если параметр является rvalue, тогда forward()s будет преобразовываться в ссылки rvalue, а май завершить движущийся Аргумент. Представьте, что вам звонят, скажем, sort(), куда proj() получил объект функции rvalue (для любого параметра не имеет значения). При первом выполнении компаратора, то есть в первый раз, когда лямбда proj() возврат называется — он будет двигаться объект функции… оставляя его в состоянии «перемещено из»… поэтому во второй раз, когда компаратор / лямбда выполняется, он использует «пустой» объект функции. Какой именно ущерб это вызовет, зависит от того, что означает использование объекта функции «перемещено из», что будет зависеть от типа объекта функции. Но в любом случае это нехорошо.

Хорошо, проще говоря:

  • Вы правильно приняли аргументы функции как ссылки для пересылки…
  • … И вы правильно перенаправили их в invoke() выражения…
  • … но у тебя есть НЕТ перенаправил их в лямбда-захвате.

Вам нужны идеально перенаправленные лямбда-захваты. То есть НЕТ тривиально … но не тоже жесткий (совет: используйте std::tuple и тонна матери decltype(TFn), так далее.; не забудьте отметить лямбду mutable).

Резюме

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

Как практическая библиотека… Я этого не вижу. Практически единственные люди, которые могли бы его использовать, в этом не нуждаются. И крохотная демографическая группа, которая могла бы это использовать, но не могла использовать std::ranges решение, было бы неразумно использовать его (лучше подождать, пока их платформа поддержит std::ranges).

Реализация хороша, но для этого есть одна хитрая проблема: вы идеально перенаправили все, кроме лямбда-захвата. Исправьте это, и я считать это должна быть хорошая реализация. Для полного практического удобства использования (такого, как есть) вам понадобится пара перегрузок для случаев, не охватываемых одним предикатом и единственной проекцией (например, перегрузка без предиката для таких вещей, как std::find(), std::count(), std::remove(), так далее.; плюс несколько других перегрузок для более эзотерических случаев), но как только вы заставите одну из них заработать, это не должно быть проблемой.

  • Спасибо за обзор! Я хочу, чтобы эта функция использовалась в основном для параллельных алгоритмов, которые, как я полагаю, не будут расширены, по крайней мере, до C ++ 26. Вы правы, что идеальная пересылка функторов либо бесполезна, либо просто неверна. Я неправильно прочитал stackoverflow.com/a/26831637

    — металлическая лиса

  • «Довольно странно писать constexpr после auto». Я бы тоже не стал писать так для constexpr, но это соответствует правилу C ++ по часовой стрелке. Но хороший ответ!

    — Второй

  • Я бы не рекомендовал использовать что-либо сложное — все, что скрывает то, что происходит, или скрывает это на уровнях косвенного обращения — с параллельными алгоритмами… но тогда я бы вообще не рекомендовал использовать параллельные алгоритмы, так что….

    — инди

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *