C ++ 20 Pojęcia: Którą specjalizację szablonu wybiera się, gdy argument szablonu kwalifikuje się do wielu pojęć?

23

Dany :

#include <concepts>
#include <iostream>

template<class T>
struct wrapper;

template<std::signed_integral T>
struct wrapper<T>
{
    wrapper() = default;
    void print()
    {
        std::cout << "signed_integral" << std::endl;
    }
};

template<std::integral T>
struct wrapper<T>
{
    wrapper() = default;
    void print()
    {
        std::cout << "integral" << std::endl;
    }
};

int main()
{
    wrapper<int> w;
    w.print(); // Output : signed_integral
    return 0;
}

Z powyższego kodu intkwalifikuje się zarówno do, jak std::integrali do std::signed_integralkoncepcji.

Zaskakujące jest to, że kompiluje i drukuje „podpisany_integralny” zarówno na kompilatorach GCC, jak i MSVC. Spodziewałem się, że zakończy się niepowodzeniem z błędem w stylu „już zdefiniowano specjalizację szablonu”.

W porządku, to legalne, dość uczciwe, ale dlaczego std::signed_integralzamiast tego wybrano std::integral? Czy istnieją jakieś reguły zdefiniowane w standardzie, przy wyborze specjalizacji szablonu, gdy do argumentu szablonu kwalifikuje się wiele pojęć?

Lewis Liman
źródło
Nie powiedziałbym, że jest to legalne tylko dlatego, że kompilator (y) akceptują go, szczególnie na wczesnych etapach jego przyjmowania.
Slava
@Slava w tym przypadku chodzi o to, że koncepcje są starannie zaprojektowane, aby zaspokoić się nawzajem w intuicyjny sposób
Guillaume Racicot
@ GuillaumeRacicot jest w porządku, właśnie skomentowałem, że wniosek „jest zgodny z prawem, ponieważ kompilator to zaakceptował” pozwala powiedzieć, że jest mylący. Nie powiedziałem jednak, że to nie jest legalne.
Slava

Odpowiedzi:

14

Wynika to z faktu, że koncepcje mogą być bardziej wyspecjalizowane niż inne, trochę podobnie jak same szablony. Nazywa się to częściowym uporządkowaniem ograniczeń

W przypadku pojęć wzajemnie się zapełniają, gdy zawierają równoważne ograniczenia. Na przykład, oto jak std::integrali std::signed_integralsą realizowane:

template<typename T>
concept integral = std::is_integral_v<T>;

template<typename T> //   v--------------v---- Using the contraint defined above
concept signed_integral = std::integral<T> && std::is_signed_v<T>;

Normalizując ograniczenia kompilator sprowadza wyrażenie ograniczenia do tego:

template<typename T>
concept integral = std::is_integral_v<T>;

template<typename T>
concept signed_integral = std::is_integral_v<T> && std::is_signed_v<T>;

W tym przykładzie signed_integraloznacza integralcałkowicie. W pewnym sensie całka ze znakiem jest „bardziej ograniczona” niż całka.

Standard pisze to tak:

Od [temp.func.order] / 2 ( wyróżnienie moje):

Częściowe porządkowanie wybiera, który z dwóch szablonów funkcyjnych jest bardziej wyspecjalizowany niż drugi, przekształcając kolejno każdy szablon (patrz następny akapit) i przeprowadzając dedukcję argumentów szablonu za pomocą typu funkcji. Proces dedukcji określa, czy jeden z szablonów jest bardziej wyspecjalizowany niż drugi. Jeśli tak, bardziej wyspecjalizowanym szablonem jest ten wybrany w procesie częściowego zamawiania. Jeśli oba odliczenia się powiodą, częściowe uporządkowanie wybiera bardziej ograniczony szablon zgodnie z zasadami opisanymi w [temp.constr.order] .

Oznacza to, że jeśli istnieje wiele możliwych podstawień dla szablonu i oba są wybrane z częściowego uporządkowania, wybierze najbardziej ograniczony szablon.

Od [temp.constr.order] / 1 :

Ograniczenie P podsumowuje ograniczenie Q tylko wtedy, gdy dla każdego rozłącznego klauzuli P I w rozłącznego normalnej postaci P , P I podsumowuje każdy łączący punkt Q j w spojówek normalnej postaci Q , gdzie

  • dająca alternatywę punkt P i podciąga koniunkcyjnej punkt Q j wtedy i tylko wtedy, gdy nie istnieje ograniczenie atomowy P między innymi w P i dla których istnieje atomowej wiązania Q jb w Q j takie, że P ia podsumowuje P jb , i

  • ograniczenie atomowe A obejmuje kolejne ograniczenie atomowe B wtedy i tylko wtedy, gdy A i B są identyczne, stosując zasady opisane w [temp . constr.atomic] .

Opisuje algorytm sumowania wykorzystywany przez kompilator do porządkowania ograniczeń, a zatem pojęć.

Racilla Guillaume
źródło
2
Wygląda na to, że maszerujesz w środku akapitu ...
ShadowRanger
11

C ++ 20 ma mechanizm decydujący, kiedy jeden konkretny ograniczony podmiot jest „bardziej ograniczony” niż inny. To nie jest prosta sprawa.

Zaczyna się od koncepcji rozbicia wiązania na jego komponenty atomowe, procesu zwanego normalizacją wiązania . Jest tu zbyt duży i zbyt skomplikowany, aby go tu wchodzić, ale podstawową ideą jest to, że każde wyrażenie w ograniczeniu jest rekurencyjnie dzielone na atomowe elementy pojęciowe, aż do osiągnięcia podwyrażenia składowego, które nie jest pojęciem.

Biorąc to pod uwagę, spójrzmy jak zdefiniowane są pojęcia integrali :signed_integral

template<class T>
  concept integral = is_integral_v<T>;
template<class T>
  concept signed_­integral = integral<T> && is_signed_v<T>;

Rozkład integraljest sprawiedliwy is_integral_v. Rozkład signed_integraljest is_integral_v && is_signed_v.

Teraz dochodzimy do koncepcji subsumcji ograniczeń . Jest to trochę skomplikowane, ale podstawową ideą jest to, że ograniczenie C1 mówi się, że „przejmuje” ograniczenie C2, jeśli rozkład C1 zawiera każde podwyrażenie w C2. Widzimy, że integralto nie ulega zniszczeniu signed_integral, ale signed_integral się zmniejsza integral, ponieważ zawiera wszystko, co integralrobi.

Następnie przechodzimy do zamawiania podmiotów z ograniczeniami:

Deklaracja D1 jest co najmniej tak ograniczona jak deklaracja D2, jeśli * D1 i D2 są zarówno ograniczonymi deklaracjami, a powiązane ograniczenia D1 obejmują te z D2; lub * D2 nie ma powiązanych ograniczeń.

Ponieważ signed_integralobejmuje integral, <signed_integral> wrapperjest „co najmniej tak samo ograniczony” jak <integral> wrapper. Jednak sytuacja odwrotna nie jest prawdą, ponieważ subskrypcja nie jest odwracalna.

Dlatego zgodnie z zasadą dla „bardziej ograniczonych” podmiotów:

Deklaracja D1 jest bardziej ograniczona niż inna deklaracja D2, gdy D1 jest co najmniej tak samo ograniczona jak D2, a D2 nie jest co najmniej tak ograniczona jak D1.

Ponieważ <integral> wrapperto nie jest co najmniej tak ograniczone jak to <signed_integral> wrapper, drugie jest uważane za bardziej ograniczone niż pierwsze.

A zatem, gdy obaj mogą się ubiegać, wygrywa deklaracja bardziej ograniczona.


Należy pamiętać, że reguły subsumcji ograniczeń kończą się, gdy napotkamy wyrażenie, które nie jest a concept. Więc jeśli to zrobiłeś:

template<typename T>
constexpr bool my_is_integral_v = std::is_integral_v<T>;

template<typename T>
concept my_signed_integral = my_is_integral_v<T> && std::is_signed_v<T>;

W takim przypadku my_signed_integral nie wziąłby udziału std::integral. Mimo że my_is_integral_vjest zdefiniowane identycznie std::is_integral_v, ponieważ nie jest to pojęcie, reguły subskrypcji C ++ nie mogą przejrzeć go w celu ustalenia, że ​​są takie same.

Reguły subskrypcji zachęcają więc do budowania pojęć z operacji na pojęciach atomowych.

Nicol Bolas
źródło
3

Z częściowymi ograniczeniami

Mówi się, że ograniczenie P obejmuje ograniczenie Q, jeśli można udowodnić, że P implikuje Q aż do identyczności ograniczeń atomowych w P i Q.

i

Relacja sumy określa częściową kolejność ograniczeń, która służy do określenia:

  • najlepszy realny kandydat na funkcję niebędącą szablonem w rozdzielczości przeciążenia
  • adres funkcji innej niż szablon w zestawie przeciążenia
  • najlepsze dopasowanie dla argumentu szablonu szablonu
  • częściowe zamawianie specjalizacji szablonów klas
  • częściowe uporządkowanie szablonów funkcji

A koncepcja std::signed_integralobejmuje std::integral<T>koncepcję:

template < class T >
concept signed_integral = std::integral<T> && std::is_signed_v<T>;

Twój kod jest w porządku, podobnie jak std::signed_integralbardziej „specjalistyczny”.

Jarod42
źródło