Dlaczego std :: function nie uczestniczy w rozwiązywaniu problemów z przeciążeniem?

17

Wiem, że następujący kod nie zostanie skompilowany.

void baz(int i) { }
void baz() {  }


class Bar
{
    std::function<void()> bazFn;
public:
    Bar(std::function<void()> fun = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

Ponieważ std::functionmówi się, że nie należy rozważać rozdzielczości przeciążenia, jak czytałem w tym innym poście .

Nie do końca rozumiem ograniczenia techniczne, które wymusiły tego rodzaju rozwiązanie.

Czytałem o fazach tłumaczenia i szablonach na temat preferencji, ale nie mogę wymyślić żadnego uzasadnienia, na które nie mogłem znaleźć kontrprzykładu. Wyjaśniony pół-laikowi (wciąż nowicjuszowi w C ++), co i na jakim etapie tłumaczenia sprawia, że ​​powyższe się nie kompiluje?

TuRtoise
źródło
1
@Evg, ale istnieje tylko jedno przeciążenie, które ma sens, aby zostać zaakceptowanym w przykładzie PO. W twoim przykładzie oba pasowałyby do siebie
idclev 463035818

Odpowiedzi:

13

To tak naprawdę nie ma nic wspólnego z „fazami tłumaczenia”. Chodzi wyłącznie o konstruktorów std::function.

Zobacz, std::function<R(Args)>nie wymaga, aby dana funkcja była dokładnie tego samego typu R(Args). W szczególności nie wymaga, aby otrzymał wskaźnik funkcji. Może przyjmować dowolny możliwy do wywołania typ (wskaźnik funkcji składowej, jakiś obiekt, który ma przeciążenie operator()), pod warunkiem, że można go wywoływać tak, jakby pobierał Argsparametry i zwraca coś, co można przekonwertować R (lub jeśli Rjest void, może zwrócić cokolwiek).

Aby to zrobić, odpowiedni konstruktor std::functionmusi być szablon : template<typename F> function(F f);. Oznacza to, że może przyjąć dowolny typ funkcji (z zastrzeżeniem powyższych ograniczeń).

Wyrażenie bazreprezentuje zestaw przeciążenia. Jeśli użyjesz tego wyrażenia do wywołania zestawu przeciążenia, nie ma sprawy. Jeśli użyjesz tego wyrażenia jako parametru dla funkcji, która pobiera określony wskaźnik funkcji, C ++ może zmniejszyć zestaw przeciążeń do pojedynczego wywołania, dzięki czemu będzie dobrze.

Jednak gdy funkcja jest szablonem i używasz dedukcji argumentów szablonu, aby dowiedzieć się, co to za parametr, C ++ nie ma już możliwości ustalenia, jakie jest prawidłowe przeciążenie w zestawie przeciążenia. Musisz więc podać to bezpośrednio.

Nicol Bolas
źródło
Wybacz mi, jeśli coś jest nie tak, ale dlaczego C ++ nie ma możliwości rozróżnienia dostępnych przeciążeń? Widzę teraz, że std :: function <T> akceptuje dowolny kompatybilny typ, nie tylko dokładne dopasowanie, ale tylko jedno z tych dwóch przeciążeń baz () jest wywoływalne, jakby pobierało określone parametry. Dlaczego nie można jednoznacznie określić?
TuRtoise,
Ponieważ wszystko, co widzi C ++, to cytowany przeze mnie podpis. Nie wie, z czym ma się równać. Zasadniczo, za każdym razem, gdy użyjesz zestawu przeciążenia w stosunku do czegokolwiek, co nie jest oczywiste, jaka jest prawidłowa odpowiedź wprost z kodu C ++ (kod jest deklaracją szablonu), język zmusza cię do przeliterowania tego, co masz na myśli.
Nicol Bolas,
1
@TuRtoise: parametr szablonu w functionszablonie klasy nie ma znaczenia . Liczy się to parametr szablonu na konstruktora dzwonisz. Co jest po prostu typename F: aka, dowolny typ.
Nicol Bolas,
1
@TuRtoise: Myślę, że coś nie rozumiesz. To „just F”, ponieważ tak działają szablony. Z podpisu ten konstruktor funkcji przyjmuje dowolny typ, więc każda próba wywołania go za pomocą dedukcji argumentu szablonu spowoduje wywnioskowanie typu z parametru. Każda próba zastosowania odliczenia do zestawu przeciążenia jest błędem kompilacji. Kompilator nie próbuje wydedukować każdego możliwego typu ze zbioru, aby sprawdzić, które z nich działają.
Nicol Bolas,
1
„Każda próba zastosowania odliczenia do zestawu przeciążenia jest błędem kompilacji. Kompilator nie próbuje dedukować każdego możliwego typu ze zbioru, aby sprawdzić, które z nich działają”. Dokładnie tego, czego mi brakowało, dziękuję :)
TuRtoise
7

Rozwiązanie problemu przeciążenia występuje tylko wtedy, gdy (a) wywołujesz nazwę funkcji / operatora lub (b) przesyłasz ją do wskaźnika (do funkcji lub funkcji członka) z wyraźną sygnaturą.

Tutaj też nie ma miejsca.

std::functionpobiera dowolny obiekt zgodny z jego podpisem. Nie bierze specjalnie wskaźnika funkcji. (lambda nie jest funkcją std, a funkcja std nie jest lambda)

Teraz w moich wariantach funkcji homebrew, do podpisu R(Args...)akceptuję również R(*)(Args...)argument (dokładne dopasowanie) właśnie z tego powodu. Ale oznacza to, że podnosi podpisy „ścisłe dopasowanie” ponad „zgodne” podpisy.

Podstawowym problemem jest to, że zestaw przeciążeń nie jest obiektem C ++. Możesz nazwać zestaw przeciążenia, ale nie możesz go przekazać „natywnie”.

Teraz możesz utworzyć zestaw pseudo-przeciążenia funkcji takiej jak ta:

#define RETURNS(...) \
  noexcept(noexcept(__VA_ARGS__)) \
  -> decltype(__VA_ARGS__) \
  { return __VA_ARGS__; }

#define OVERLOADS_OF(...) \
  [](auto&&...args) \
  RETURNS( __VA_ARGS__(decltype(args)(args)...) )

tworzy to pojedynczy obiekt C ++, który może rozwiązać problem przeciążenia dla nazwy funkcji.

Rozszerzając makra, otrzymujemy:

[](auto&&...args)
noexcept(noexcept( baz(decltype(args)(args)...) ) )
-> decltype( baz(decltype(args)(args)...) )
{ return baz(decltype(args)(args)...); }

irytujące do pisania. Prostsza, tylko nieco mniej użyteczna wersja jest tutaj:

[](auto&&...args)->decltype(auto)
{ return baz(decltype(args)(args)...); }

mamy lambda, która przyjmuje dowolną liczbę argumentów, a następnie doskonale je przekazuje baz.

Następnie:

class Bar {
  std::function<void()> bazFn;
public:
  Bar(std::function<void()> fun = OVERLOADS_OF(baz)) : bazFn(fun){}
};

Pracuje. Odraczamy rozdzielczość przeciążenia do lambda, w którym przechowujemy fun, zamiast przekazywać funbezpośrednio zestaw przeciążenia (którego nie może rozwiązać).

Istnieje co najmniej jedna propozycja zdefiniowania operacji w języku C ++, która konwertuje nazwę funkcji na obiekt zestawu przeciążenia. Dopóki taka standardowa propozycja nie będzie w standardzie, OVERLOADS_OFmakro jest przydatne.

Możesz pójść o krok dalej i wesprzeć wskaźnik cast-to-zgodny-funkcyjny.

struct baz_overloads {
  template<class...Ts>
  auto operator()(Ts&&...ts)const
  RETURNS( baz(std::forward<Ts>(ts)...) );

  template<class R, class...Args>
  using fptr = R(*)(Args...);
  //TODO: SFINAE-friendly support
  template<class R, class...Ts>
  operator fptr<R,Ts...>() const {
    return [](Ts...ts)->R { return baz(std::forward<Ts>(ts)...); };
  }
};

ale to zaczyna być tępe.

Przykład na żywo .

#define OVERLOADS_T(...) \
  struct { \
    template<class...Ts> \
    auto operator()(Ts&&...ts)const \
    RETURNS( __VA_ARGS__(std::forward<Ts>(ts)...) ); \
\
    template<class R, class...Args> \
    using fptr = R(*)(Args...); \
\
    template<class R, class...Ts> \
    operator fptr<R,Ts...>() const { \
      return [](Ts...ts)->R { return __VA_ARGS__(std::forward<Ts>(ts)...); }; \
    } \
  }
Jak - Adam Nevraumont
źródło
5

Problem polega na tym, że nic nie mówi kompilatorowi, jak wykonać funkcję rozpadu wskaźnika. Jeśli masz

void baz(int i) { }
void baz() {  }

class Bar
{
    void (*bazFn)();
public:
    Bar(void(*fun)() = baz) : bazFn(fun){}

};

int main(int argc, char **argv)
{
    Bar b;
    return 0;
}

Wtedy kod działałby, ponieważ teraz kompilator wie, jakiej funkcji chcesz, ponieważ istnieje konkretny typ, który przypisujesz.

Podczas używania std::functionwywołujesz konstruktor funkcji obiektu, który ma postać

template< class F >
function( F f );

a ponieważ jest to szablon, musi wydedukować typ przekazywanego obiektu. ponieważ bazjest to funkcja przeciążona, nie ma jednego typu, który można by wywnioskować, więc dedukcja szablonu nie powiedzie się i pojawi się błąd. Musisz użyć

Bar(std::function<void()> fun = (void(*)())baz) : bazFn(fun){}

aby uzyskać siłę jednego typu i pozwolić na odliczenie.

NathanOliver
źródło
„ponieważ baz jest funkcją przeciążoną, nie można wyprowadzić żadnego pojedynczego typu”, ale ponieważ C ++ 14 ”ten konstruktor nie uczestniczy w rozwiązywaniu problemów z przeciążeniem, chyba że f można wywołać dla typów argumentów Args… i zwrócić typ R„ Jestem nie jestem pewien, ale byłem bliski oczekiwania, że ​​to wystarczy, aby rozwiązać niejednoznaczność
idclev 463035818,
3
@ previouslyknownas_463035818 Ale aby to ustalić, najpierw trzeba wydedukować typ, a nie może, ponieważ jest to nazwa przeciążona.
NathanOliver,
1

W momencie, gdy kompilator decyduje, które przeciążenie przejdzie do std::functionkonstruktora, wie tylko, że std::functionkonstruktor ma szablony, aby przyjąć dowolny typ. Nie ma możliwości wypróbowania obu przeciążeń i stwierdzenia, że ​​pierwszy nie kompiluje się, a drugi tak.

Sposobem na rozwiązanie tego jest wyraźne poinformowanie kompilatora, które przeciążenie chcesz static_cast:

Bar(std::function<void()> fun = static_cast<void(*)()>(baz)) : bazFn(fun){}
Alan Birtles
źródło