Почему указатели на нестатические функции-члены нельзя использовать в качестве унарного предиката для алгоритмов стандартной библиотеки?

Многие алгоритмы из стандартной библиотеки принимают унарный предикат с сигнатурой bool (Type & item), поэтому прямое предоставление указателя на нестатическую функцию-член не работает. Это кажется довольно ограничительным, учитывая, что такое ограничение можно снять, заменив прямой вызов operator () в предикате вызовом std::invoke. Может быть, предложенный подход имеет какие-то недостатки, которые я упустил из виду?

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

Пример кода (онлайн-компилятор):

#include <array>
#include <algorithm>
#include <iostream>
#include <functional>
#include <cassert>

template<typename TForwardIterator, typename TPredicate> TForwardIterator
my_find_if
(
    const TForwardIterator p_items_begin
,   const TForwardIterator p_items_end
,   TPredicate &&          predicate
)
{
    TForwardIterator p_item(p_items_begin);
//  while((p_items_end != p_item) && (!predicate(*p_item)))
    while((p_items_end != p_item) && (!::std::invoke(predicate, *p_item)))
    {
        ++p_item;
    }
    return(p_item);
}

class t_Ticket
{
    private: int m_number;

    public:
    t_Ticket(const int number): m_number(number) {}

    public: bool
    Is_Lucky(void) const {return(8 == m_number);}
};

int main()
{
    ::std::array<t_Ticket, 10> tickets{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    //  using standard library
    auto p_ticket1(::std::find_if(tickets.begin(), tickets.end(), [](const t_Ticket & ticket) {return(ticket.Is_Lucky());}));
    //  still works
    auto p_ticket2(my_find_if(tickets.begin(), tickets.end(), [](const t_Ticket & ticket) {return(ticket.Is_Lucky());}));
    //  same thing, but shorter and not sufferring from potential lambda code duplication
    auto p_ticket3(my_find_if(tickets.begin(), tickets.end(), &t_Ticket::Is_Lucky));
    //  using standard library like this won't compile
    //auto p_ticket4(::std::find_if(tickets.begin(), tickets.end(), &t_Ticket::Is_Lucky));

    assert(p_ticket1 == p_ticket2);
    assert(p_ticket2 == p_ticket3);
    return(0);
}

person user7860670    schedule 11.11.2017    source источник
comment
Есть предложение для [](auto&& t) -> t.IsLucky(), которое максимально сжато. Обратите внимание, что это также более общее: -> t.sLucky(1) || t.isLucky(2) тоже работает. Возможно, однажды у нас будет '@', представляющий auto&& для переменных (и, возможно, использование для типов).   -  person lorro    schedule 11.11.2017
comment
Между прочим, есть также std::mem_fn.   -  person Tony Delroy    schedule 11.11.2017
comment
bool Is_Lucky(void) const {return(8 == m_number);} - return не является функцией. Эти скобки не нужны. return 8 == m_number; (ИМХО) более идиоматичен. Кроме того, void для обозначения отсутствия аргументов функции является C-измом - не делайте этого в C++ - подойдет только bool Is_Lucky() const.   -  person Jesper Juhl    schedule 11.11.2017
comment
@lorro Что должен делать [](auto&& t) -> t.IsLucky()? Это лямбда с {return();} выброшена?   -  person user7860670    schedule 11.11.2017
comment
@VTT предлагается означать то же самое, что и: [](auto&& t) -> decltype(t.isLucky()) { return t.isLucky(); }   -  person lorro    schedule 11.11.2017


Ответы (1)


Во-первых, если предположить, что вопрос:

Почему алгоритмы не могут использовать общий Callable в качестве своего предиката/действия?

Я могу думать о нескольких причинах:

  • Концепция Callable была введена в C++11 — алгоритмы до C++11 не разрабатывались с ее учетом.

  • Принятие любого Callable потребует расширения концепций, таких как Predicate, и разрешения алгоритмам условно принимать дополнительный аргумент для указателей на функции-члены. Это может привести к ненужной сложности, которой можно избежать, просто ограничив интерфейс до FunctionObject и заставив пользователя выполнять "привязку".

  • Это значительно увеличило бы количество перегрузок, учитывая, что сейчас также существуют перегрузки политики выполнения. В качестве альтернативы можно сделать каждый алгоритм вариативным шаблоном, где пакет параметров может содержать либо ноль, либо один аргумент (в случае, если *this необходимо предоставить для Callable).

  • std::invoke не constexpr. Его использование может предотвратить пометку алгоритмов как constexpr в будущем.


Если вы имеете в виду конкретный случай, когда:

  • Алгоритм работает в однородном диапазоне T.

  • Предикат выполняется для каждого элемента диапазона T типа.

  • T имеет функцию-член, которую можно использовать как предикат.

Тогда я все еще могу думать о некоторых возможных причинах:

  • std::invoke все еще не constexpr. Мы могли бы избежать использования std::invoke вместо .*, но тогда нам пришлось бы предоставлять две отдельные реализации для каждого алгоритма.

  • Если функция-член является template или набором перегрузки, это не скомпилируется и запутает новичков. Общие лямбда-выражения не имеют этой проблемы.

  • Это усложнит требования к алгоритмам — потребуется какое-то ограничение на тип/сигнатуру функции-члена, которая гарантирует, что ее можно использовать в диапазоне T.

Или, может быть... это просто еще не было предложено. Если вы все еще считаете, что это ценно иметь после причин, которые я привел, вы можете начать здесь, чтобы узнать, как написать предложение: https://isocpp.org/std/submit-a-предложение

person Vittorio Romeo    schedule 11.11.2017
comment
Зачем нужны дополнительные перегрузки для функций-членов? Это уже шаблон, зачем менять? - person Rakete1111; 11.11.2017
comment
@ Rakete1111: вам также придется передать экземпляр, для которого будет вызываться функция-член. Например. std::remove_if(fst, last, &foo::some_predicate, my_foo_instance). - person Vittorio Romeo; 11.11.2017
comment
Разве &foo::some_predicate не будет вызываться для значений диапазона, а не для другого экземпляра? - person Rakete1111; 11.11.2017
comment
(2) Мой вопрос подразумевает, что нестатическая функция-член, используемая в качестве предиката, отличается от функции bool (Type & item) тем, что ссылка на элемент передается как неявный параметр. (3) больше похоже на маркировку std::invoke как constexpr, чтобы пометить алгоритмы constexpr - person user7860670; 11.11.2017
comment
@VTT: std::invoke нельзя пометить как constexpr из-за проблемы CWG. Проверьте ссылку в моем ответе. - person Vittorio Romeo; 11.11.2017
comment
Но в первом предложении говорится, что существует возможность реализовать стандартный шаблон функции вызова, соответствующий стандарту, как функцию constexpr. А контраргументы включают в себя трудности с работой с mem_fn, который все равно был удален. Хотя я не уверен, что означает CWG. - person user7860670; 11.11.2017
comment
@VTT: опять же, это в ссылке, которую я разместил. Здесь. Пока эта проблема с CWG (основной рабочей группой) не будет решена, std::invoke нельзя пометить как constexpr. - person Vittorio Romeo; 11.11.2017