Pytanie Dlaczego std :: function not equality porównywalne?


To pytanie dotyczy również boost::function i std::tr1::function.

std::function nie jest równy porównywalny:

#include <functional>
void foo() { }

int main() {
    std::function<void()> f(foo), g(foo);
    bool are_equal(f == g); // Error:  f and g are not equality comparable
}

W C ++ 11, operator== i operator!= przeciążenia po prostu nie istnieją. We wczesnej wersji C ++ 11 przeciążenia zostały zadeklarowane jako usunięte z komentarzem (N3092 §20.8.14.2):

// deleted overloads close possible hole in the type system

Nie mówi, jaka jest "możliwa dziura w systemie typu". W TR1 i Boost przeciążenia są zadeklarowane, ale nie zdefiniowane. Komentarze do specyfikacji TR1 (N1836 §3.7.2.6):

Te funkcje członkowskie pozostają niezdefiniowane.

[Uwaga: konwersja typu boolowskiego otwiera lukę, dzięki której można porównywać dwie instancje funkcji == lub !=. Nieokreślone void operatorzy zamykają lukę i zapewniają błąd podczas kompilacji. -Notuj notatkę]

Moje rozumienie "luki" polega na tym, że jeśli mamy bool funkcja konwersji, ta konwersja może być używana w porównaniach równości (i w innych okolicznościach):

struct S {
    operator bool() { return false; }
};

int main() {
    S a, b;
    bool are_equal(a == b); // Uses operator bool on a and b!  Oh no!
}

Odniosłem wrażenie, że w celu uniknięcia tej "luki" użyto idiomu safe-bool w C ++ 03 i użyciu jawnej funkcji konwersji w C ++ 11. Boost i TR1 używają idiomu safe-bool function a C ++ 11 tworzy bool funkcja konwersji jawnie.

Jako przykład klasy, która ma obie, std::shared_ptr oba mają wyraźne bool funkcja konwersji i jest porównywalna pod względem równości.

Dlaczego jest std::function nie równość porównywalna? Jaka jest "możliwa dziura w systemie typu?" Czym różni się od std::shared_ptr?


58
2017-09-02 18:01


pochodzenie


Pamiętaj, że możesz poprosić * a.target< ftor_type >() == * b.target< ftor_type >() jeśli wskazują na funktory o porównywalnej równości. Chociaż jest to trochę skomplikowane (obiekt bazowy nie zostanie niejawnie przekonwertowany na żądany typ), to jednak dokładnie określa, którego semantyki porównania są używane. - Potatoswatter


Odpowiedzi:


Dlaczego jest std::function nie równość porównywalna?

std::function to wrapper dla dowolnych typów wywoływalnych, więc w celu wdrożenia porównania równości, musiałbyś wymagać, aby wszystkie typy wywoływalne były porównywalne pod względem równości, co obciążałoby każdego, kto implementował obiekt funkcji. Nawet wtedy dostałbyś wąską koncepcję równości, ponieważ równoważne funkcje porównywałyby nierówność, gdyby (na przykład) zostały skonstruowane przez powiązanie argumentów w innej kolejności. Uważam, że w przypadku ogólnym nie można testować równoważności.

Jaka jest "możliwa dziura w systemie typu?"

Zgaduję, że to oznacza, że ​​łatwiej jest usunąć operatorów i wiedzieć na pewno, że ich użycie nigdy nie da prawidłowego kodu, niż udowodnić, że nie ma możliwości niepożądanych niejawnych konwersji w nieodkrytych wcześniej przypadkach narożnych.

Czym różni się od std::shared_ptr?

std::shared_ptr ma dobrze zdefiniowaną semantykę równości; dwa wskaźniki są równe wtedy i tylko wtedy, gdy są albo puste, albo oba niepuste i wskazują ten sam obiekt.


37
2017-09-02 18:45



Dzięki. Byłem tak skupiony na komentarzach do "luki systemu typu", że nie myślałem o semantykach. Przyjmuję tę odpowiedź, ponieważ moim zdaniem najwyraźniej odpowiada ona na pierwszą i trzecią część, chociaż druga część jest najlepiej wyjaśniona Odpowiedź In Silico. - James McNellis


Jest to dokładnie omówione w Najczęściej zadawane pytania dotyczące Boost.Function. :-)


24
2017-09-02 18:17



Powinienem był się domyślić, że pojawi się FAQ Boost, który omawia to. Dzięki. - James McNellis


Mogę się mylić, ale myślę, że równość std::function obiekty niestety nie są rozwiązalne w sensie ogólnym. Na przykład:

#include <boost/bind.hpp>
#include <boost/function.hpp>
#include <cstdio>

void f() {
    printf("hello\n");
}

int main() {
    boost::function<void()> f1 = f;
    boost::function<void()> f2 = boost::bind(f);

    f1();
    f2();
}

f1 i f2 równy? Co się stanie, jeśli dodaję dowolną liczbę obiektów funkcji, które po prostu owijają się nawzajem na różne sposoby, które ostatecznie sprowadzają się do połączenia f... nadal jest równy?


20
2017-09-02 18:21



+1 dla łatwego do zniesienia przykładu, standardy i często zadawane pytania są świetne, ale często są zbyt abstrakcyjne, aby ludzie mogli po prostu poczuć powód. - Matthieu M.
Uzgodnione z @Matthieu: to dobry, prosty przykład problemu, o którym nie pomyślałem. - James McNellis
Zadanie funkcji Boost :: function (jeśli wspierałoby to porównywanie) - nie polega na dedukcji algorytmu porównania, ale tylko na przekierowaniu wywołania porównania. Jeśli ukryty obiekt nie definiuje porównania - jest to po prostu błąd kompilacji. Jeśli obiekt bazowy definiuje błędne porównanie - nie jest to również problemem funkcji boost :: function. - Evgeny Panasyuk
@EvgenyPanasyuk: Zapominasz o wymazaniu typu. Z wielu powodów, w tym nadużywania kodu i wydajności, function może zdecydować o zastąpieniu obiektu bazowego innym, nieodróżnialnym funkcjonalnie obiektem. Twoja propozycja ograniczyłaby tę zdolność. Ponadto, std::function będzie mieć wiele implementacji, więc musisz być bardzo ostrożny w określaniu jego zachowania. (Tj. Jak działa ADL?) - MSalters
@MSalters: 1. Dlaczego myślisz, że zapominam o wymazaniu typu? 2. "zastąp przedmiot" - jak to możliwe? Zwłaszcza w obecności :: target_type i :: target? Czy masz jakieś dowody? 3. "więc musisz być bardzo ostrożny w określaniu swojego zachowania" - oczywiście wszystko, co idzie do ISO, musi być dobrze zdefiniowane. Jakie problemy z ADL może być w tym zakresie? - Evgeny Panasyuk


Dlaczego std :: function not equality porównywalne?

Myślę, że głównym powodem jest to, że gdyby tak było, to nie można go używać z porównywalnymi typami innymi niż równość, nawet jeśli nigdy nie przeprowadzono porównania równości.

To znaczy. kod, który dokonuje porównania, powinien zostać utworzony wcześnie - w momencie, gdy obiekt wywoływalny jest przechowywany w funkcji std ::, na przykład w jednym z konstruktorów lub operatorów przypisania.

Takie ograniczenie znacznie zmniejszyłoby zakres stosowania i oczywiście nie do przyjęcia "opakowanie z funkcją polimorficzną ogólnego przeznaczenia".


Ważne jest, aby pamiętać, że jest to możliwe porównaj funkcję boost :: function z obiektem wywoływalnym (ale nie z inną funkcją boost :: function)

Funkcja wrapperów obiektów może być porównywana poprzez == lub! = Z dowolnym obiektem funkcji, który może być przechowywany w opakowaniu.

Jest to możliwe, ponieważ funkcja, która wykonuje takie porównanie, jest natychmiast analizowana w punkcie porównania na podstawie typu znanego argumentu operacji.

Co więcej, std :: function ma cel funkcja członka szablonu, które można wykorzystać do przeprowadzenia podobnego porównania. W rzeczywistości operatory porównania :: funkcja są realizowane pod względem cel funkcja członka.

Nie ma więc barier technicznych, które blokują implementację function_comparable.


Wśród odpowiedzi znajduje się powszechny wzorzec "niemożliwe w ogóle":

  • Nawet wtedy dostałbyś wąską koncepcję równości, ponieważ równoważne funkcje porównywałyby nierówność, gdyby (na przykład) zostały skonstruowane przez powiązanie argumentów w innej kolejności. Uważam, że w przypadku ogólnym nie można testować równoważności.

  • Mogę się mylić, ale myślę, że równość jest std :: obiekty funkcji niestety nie da się rozwiązać w sensie generycznym.

  • Ponieważ równoważność maszyn tartacznych jest nierozstrzygalna. Biorąc pod uwagę dwa różne obiekty funkcyjne, nie można określić, czy obliczają tę samą funkcję, czy nie. [Ta odpowiedź została usunięta]

Całkowicie się z tym nie zgadzam: nie jest zadaniem std :: function samo porównywanie, zadaniem jest po prostu przeadresować prośba o porównanie z obiektami leżącymi u podstaw - to wszystko.

Jeśli podstawowy typ obiektu nie definiuje porównania - w każdym przypadku będzie to błąd kompilacji, funkcja std :: nie jest wymagana do wyprowadzenia algorytmu porównania.

Jeśli bazowy typ obiektu definiuje porównanie, ale działa źle, lub ma jakiś nietypowy semantyczny - to nie jest problem samej funkcji std ::, ale jest to problem rodzaj bazowy.


Możliwe jest wdrożenie function_comparable na podstawie funkcji std ::.

Oto dowód koncepcji:

template<typename Callback,typename Function> inline
bool func_compare(const Function &lhs,const Function &rhs)
{
    typedef typename conditional
    <
        is_function<Callback>::value,
        typename add_pointer<Callback>::type,
        Callback
    >::type request_type;

    if (const request_type *lhs_internal = lhs.template target<request_type>())
        if (const request_type *rhs_internal = rhs.template target<request_type>())
            return *rhs_internal == *lhs_internal;
    return false;
}

#if USE_VARIADIC_TEMPLATES
    #define FUNC_SIG_TYPES typename ...Args
    #define FUNC_SIG_TYPES_PASS Args...
#else
    #define FUNC_SIG_TYPES typename function_signature
    #define FUNC_SIG_TYPES_PASS function_signature
#endif

template<FUNC_SIG_TYPES>
struct function_comparable: function<FUNC_SIG_TYPES_PASS>
{
    typedef function<FUNC_SIG_TYPES_PASS> Function;
    bool (*type_holder)(const Function &,const Function &);
public:
    function_comparable() {}
    template<typename Func> function_comparable(Func f)
        : Function(f), type_holder(func_compare<Func,Function>)
    {
    }
    template<typename Func> function_comparable &operator=(Func f)
    {
        Function::operator=(f);
        type_holder=func_compare<Func,Function>;
        return *this;
    }
    friend bool operator==(const Function &lhs,const function_comparable &rhs)
    {
        return rhs.type_holder(lhs,rhs);
    }
    friend bool operator==(const function_comparable &lhs,const Function &rhs)
    {
        return rhs==lhs;
    }
    friend void swap(function_comparable &lhs,function_comparable &rhs)// noexcept
    {
        lhs.swap(rhs);
        lhs.type_holder.swap(rhs.type_holder);
    }
};

Jest jakaś dobra nieruchomość - function_comparable można porównać z std :: function także.

Na przykład, załóżmy, że mamy wektor std :: function's, i chcemy dać dla użytkownika register_callback i unregister_callback Funkcje. Korzystanie z function_comparable jest wymagany tylko dla unregister_callback parametr:

void register_callback(std::function<function_signature> callback);
void unregister_callback(function_comparable<function_signature> callback);

Wersja demonstracyjna na żywo w Ideone

Kod źródłowy wersji demonstracyjnej:

//             Copyright Evgeny Panasyuk 2012.
// Distributed under the Boost Software License, Version 1.0.
//    (See accompanying file LICENSE_1_0.txt or copy at
//          http://www.boost.org/LICENSE_1_0.txt)

#include <type_traits>
#include <functional>
#include <algorithm>
#include <stdexcept>
#include <iostream>
#include <typeinfo>
#include <utility>
#include <ostream>
#include <vector>
#include <string>

using namespace std;

// _____________________________Implementation__________________________________________

#define USE_VARIADIC_TEMPLATES 0

template<typename Callback,typename Function> inline
bool func_compare(const Function &lhs,const Function &rhs)
{
    typedef typename conditional
    <
        is_function<Callback>::value,
        typename add_pointer<Callback>::type,
        Callback
    >::type request_type;

    if (const request_type *lhs_internal = lhs.template target<request_type>())
        if (const request_type *rhs_internal = rhs.template target<request_type>())
            return *rhs_internal == *lhs_internal;
    return false;
}

#if USE_VARIADIC_TEMPLATES
    #define FUNC_SIG_TYPES typename ...Args
    #define FUNC_SIG_TYPES_PASS Args...
#else
    #define FUNC_SIG_TYPES typename function_signature
    #define FUNC_SIG_TYPES_PASS function_signature
#endif

template<FUNC_SIG_TYPES>
struct function_comparable: function<FUNC_SIG_TYPES_PASS>
{
    typedef function<FUNC_SIG_TYPES_PASS> Function;
    bool (*type_holder)(const Function &,const Function &);
public:
    function_comparable() {}
    template<typename Func> function_comparable(Func f)
        : Function(f), type_holder(func_compare<Func,Function>)
    {
    }
    template<typename Func> function_comparable &operator=(Func f)
    {
        Function::operator=(f);
        type_holder=func_compare<Func,Function>;
        return *this;
    }
    friend bool operator==(const Function &lhs,const function_comparable &rhs)
    {
        return rhs.type_holder(lhs,rhs);
    }
    friend bool operator==(const function_comparable &lhs,const Function &rhs)
    {
        return rhs==lhs;
    }
    // ...
    friend void swap(function_comparable &lhs,function_comparable &rhs)// noexcept
    {
        lhs.swap(rhs);
        lhs.type_holder.swap(rhs.type_holder);
    }
};

// ________________________________Example______________________________________________

typedef void (function_signature)();

void func1()
{
    cout << "func1" << endl;
}

void func3()
{
    cout << "func3" << endl;
}

class func2
{
    int data;
public:
    explicit func2(int n) : data(n) {}
    friend bool operator==(const func2 &lhs,const func2 &rhs)
    {
        return lhs.data==rhs.data;
    }
    void operator()()
    {
        cout << "func2, data=" << data << endl;
    }
};
struct Caller
{
    template<typename Func>
    void operator()(Func f)
    {
        f();
    }
};
class Callbacks
{
    vector<function<function_signature>> v;
public:
    void register_callback_comparator(function_comparable<function_signature> callback)
    {
        v.push_back(callback);
    }
    void register_callback(function<function_signature> callback)
    {
        v.push_back(callback);
    }
    void unregister_callback(function_comparable<function_signature> callback)
    {
        auto it=find(v.begin(),v.end(),callback);
        if(it!=v.end())
            v.erase(it);
        else
            throw runtime_error("not found");
    }
    void call_all()
    {
        for_each(v.begin(),v.end(),Caller());
        cout << string(16,'_') << endl;
    }
};

int main()
{
    Callbacks cb;
    function_comparable<function_signature> f;
    f=func1;
    cb.register_callback_comparator(f);

    cb.register_callback(func2(1));
    cb.register_callback(func2(2));
    cb.register_callback(func3);
    cb.call_all();

    cb.unregister_callback(func2(2));
    cb.call_all();
    cb.unregister_callback(func1);
    cb.call_all();
}

Wynik to:

func1
func2, data=1
func2, data=2
func3
________________
func1
func2, data=1
func3
________________
func2, data=1
func3
________________

P.S. Wygląda na to, że z pomocą std :: typ_indeks możliwe jest zaimplementowanie podobne do function_comparable klasa, która obsługuje również porządkowanie (tj. mniej) lub nawet mieszanie. Ale nie tylko zamawianie między różnymi typami, ale także zamawianie w tym samym typie (wymaga to wsparcia z typów, takich jak LessThanComparable).


12
2017-10-27 23:23





Według http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-active.html#1240:

Główny komentarz tutaj jest częścią   Historia std::function, który   został wprowadzony przy pomocy N1402. Podczas tego   czas brak wyraźnych funkcji konwersji   istniały i idiom "bezpieczny bool"   (w oparciu o wskaźniki do członka) był   popularna technika. Jedyny   wadą tego idiomu było to   dano dwa obiekty typu f1 i f2   std :: funkcja wyrażenia

f1 == f2;

był dobrze uformowany, tylko dlatego, że   wbudowany operator == dla wskaźnika do   członek został rozpatrzony po singlu   konwersja zdefiniowana przez użytkownika. Aby to naprawić,   zestaw przeciążeniowy niezdefiniowany   dodano funkcje porównawcze, takie   ta rozdzielczość przeciążania wolałaby   te kończące się błędem powiązania.   Nowy język usunięty   funkcje są o wiele lepsze   mechanizm diagnostyczny, aby to naprawić   kwestia.

W C ++ 0x skasowane funkcje są uważane za zbyteczne przy wprowadzeniu operatorów konwersji jawnej, więc prawdopodobnie zostaną usunięte dla C ++ 0x.

Głównym punktem tego problemu jest:   to z wymianą   idiom bezpieczny przez wyraźną konwersję   aby obrócić oryginalną "dziurę w typie   system "już nie istnieje i   dlatego komentarz jest zły i   zbyteczne definicje funkcji   również powinny zostać usunięte.

Co do tego, dlaczego nie możesz porównywać std::function obiektów, prawdopodobnie dlatego, że mogą one posiadać funkcje globalne / statyczne, funkcje składowe, funktory, itp. i to zrobić std::function "usuwa" niektóre informacje o typie podstawowym. Wdrożenie operatora równości prawdopodobnie nie byłoby możliwe z tego powodu.


6
2017-09-02 18:17



Racja, ale ten (oryginalny post, odkąd naprawiono :-P) nie odnosi się do "dlaczego", tylko do "co". Zobacz FAQ do Boost.Function dla "dlaczego". - Chris Jester-Young
Dzięki za link. Powinienem był sprawdzić listę wad; Po prostu pomyślałem, że we wszystkich trzech przypadkach określono to samo, co celowe, a nie wadliwe. - James McNellis


W rzeczywistości możesz porównywać cele. Może działać zależy od tego, czego chcesz od porównania.

Tutaj kod z nierównością, ale możesz zobaczyć, jak to działa:

template <class Function>
struct Comparator
{
    bool operator()(const Function& f1, const Function& f2) const
    {
        auto ptr1 = f1.target<Function>();
        auto ptr2 = f2.target<Function>();
        return ptr1 < ptr2;
    }
};

typedef function<void(void)> Function;

set<Function, Comparator<Function>> setOfFunc;

void f11() {}

int _tmain(int argc, _TCHAR* argv[])
{
    cout << "was inserted - " << setOfFunc.insert(bind(&f11)).second << endl;  // 1 - inserted
    cout << "was inserted - " << setOfFunc.insert(f11).second << endl;         // 0 - not inserted
    cout << "# of deleted is " << setOfFunc.erase(f11) << endl;

    return 0;
}

Ups, jest ważny tylko od C ++ 11.


3
2017-09-24 13:47



Świetnie .. rozwiązałem mój problem. - ashish
Nie robi to, co myślisz, że robi. target<T> zwraca a T* to nie jest zerowy IFF T odpowiada podstawowej funkcji typu wskaźnik / typ / typ wyrażenia typu funktor. Nazywasz to jako function<void(void)>::target<function<void(void)>>. Typ podstawowy function<void(void)> jest void(void), nie  function<void(void)>. Otrzymano dwa wskaźniki (ptr1 i ptr2) są oba zawsze  nullptr, i nullptr < nullptr jest zawsze false, co uniemożliwia drugie wstawienie. Pierwsze wstawienie tylko się powiedzie, ponieważ set jest pusty, więc nie ma nic do porównania. - monkey0506
Live Demo pokazując, że set zaakceptuje maksymalnie jeden function. - monkey0506


Co powiesz na wypróbowanie czegoś podobnego, działa to dobrze w testowaniu szablonów.

if (std::is_same<T1, T2>::value)
{
    ...
}

0
2017-09-24 20:19