C ++ 20-Konzepte: Welche Vorlagenspezialisierung wird ausgewählt, wenn das Vorlagenargument für mehrere Konzepte geeignet ist?

23

Gegeben :

#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;
}

intQualifiziert sich aus dem obigen Code sowohl für das Konzept std::integralals auch für das std::signed_integralKonzept.

Überraschenderweise kompiliert und druckt dies "signiertes_Integral" sowohl auf GCC- als auch auf MSVC-Compilern. Ich hatte erwartet, dass es mit einem Fehler im Sinne von "Template-Spezialisierung bereits definiert" fehlschlagen würde.

Okay, das ist legal, fair genug, aber warum wurde statt std::signed_integralgewählt std::integral? Gibt es im Standard Regeln, mit denen die Vorlagenspezialisierung ausgewählt wird, wenn sich mehrere Konzepte für das Vorlagenargument qualifizieren?

Lewis Liman
quelle
Ich würde nicht sagen, dass es legal ist, nur weil Compiler es akzeptieren, insbesondere in diesen frühen Phasen der Übernahme.
Slava
@Slava in diesem Fall sind die Konzepte sorgfältig entworfen, so dass sie sich auf intuitive Weise zusammenfassen
Guillaume Racicot
@ GuillaumeRacicot es ist in Ordnung, ich habe gerade kommentiert, dass die Schlussfolgerung "es ist legal, weil der Compiler es akzeptiert hat", sagen wir irreführend. Ich habe nicht gesagt, dass dies nicht legal ist.
Slava

Antworten:

14

Dies liegt daran, dass Konzepte spezialisierter sein können als andere, ähnlich wie die Reihenfolge der Vorlagen selbst. Dies wird als Teilreihenfolge von Einschränkungen bezeichnet

Bei Konzepten subsumieren sie sich gegenseitig, wenn sie äquivalente Einschränkungen enthalten. Zum Beispiel, wie std::integralund std::signed_integralimplementiert werden:

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>;

Wenn der Compiler die Einschränkungen normalisiert, reduziert er den Contraint-Ausdruck auf Folgendes:

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>;

In diesem Beispiel signed_integralimpliziert integralvollständig. In gewissem Sinne ist ein vorzeichenbehaftetes Integral "eingeschränkter" als ein Integral.

Der Standard schreibt es so:

Aus [temp.func.order] / 2 (Schwerpunkt Mine):

Durch die teilweise Reihenfolge wird ausgewählt, welche der beiden Funktionsvorlagen spezialisierter als die andere ist, indem jede Vorlage nacheinander transformiert wird (siehe nächster Absatz) und die Ableitung von Vorlagenargumenten mithilfe des Funktionstyps durchgeführt wird. Der Abzugsprozess bestimmt, ob eine der Vorlagen spezialisierter ist als die andere. In diesem Fall ist die speziellere Vorlage diejenige, die durch den Teilbestellvorgang ausgewählt wurde. Wenn beide Abzüge erfolgreich sind, wählt die Teilreihenfolge die eingeschränktere Vorlage aus, wie in den Regeln in [temp.constr.order] beschrieben .

Das heißt, wenn eine Vorlage mehrfach ersetzt werden kann und beide aus der Teilreihenfolge ausgewählt werden, wird die am stärksten eingeschränkte Vorlage ausgewählt.

Aus [temp.constr.order] / 1 :

Eine Einschränkung P subsumiert eine Einschränkung Q , wenn und nur wenn für jeden disjunktive Klausel P i in der disjunktiven Normalform von P , P i subsumiert jede Klausel konjunktive Q j in der konjunktiven Normalform von Q , wobei

  • eine disjunktive Klausel P i subsumiert eine konjunktive Klausel Q j genau dann, wenn in P i eine atomare Beschränkung P ia existiert, für die eine atomare Beschränkung Q jb in Q j existiert, so dass P ia Q jb subsumiert , und

  • Eine atomare Bedingung A subsumiert genau dann eine andere atomare Bedingung B, wenn A und B unter Verwendung der in [temp.constr.atomic] beschriebenen Regeln identisch sind .

Dies beschreibt den Subsumtionsalgorithmus, den der Compiler verwendet, um Einschränkungen und damit Konzepte zu ordnen.

Guillaume Racicot
quelle
2
Sieht so aus, als ob Sie mitten in einem Absatz
nachlassen
11

C ++ 20 verfügt über einen Mechanismus zum Entscheiden, wann eine bestimmte eingeschränkte Entität "stärker eingeschränkt" ist als eine andere. Das ist keine einfache Sache.

Dies beginnt mit dem Konzept, eine Einschränkung in ihre atomaren Komponenten zu zerlegen, ein Prozess, der als Einschränkungsnormalisierung bezeichnet wird . Es ist groß und zu komplex, um hier darauf einzugehen, aber die Grundidee ist, dass jeder Ausdruck in einer Einschränkung rekursiv in seine atomaren konzeptuellen Teile zerlegt wird, bis Sie einen Komponenten-Unterausdruck erreichen, der kein Konzept ist.

Schauen wir uns vor diesem Hintergrund an, wie die integralund signed_integralKonzepte definiert sind :

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

Die Zersetzung von integralist gerecht is_integral_v. Die Zersetzung von signed_integralist is_integral_v && is_signed_v.

Nun kommen wir zum Konzept der Constraint-Subsumtion . Es ist etwas kompliziert, aber die Grundidee ist, dass eine Einschränkung C1 eine Einschränkung C2 "subsumiert", wenn die Zerlegung von C1 jeden Unterausdruck in C2 enthält. Wir können sehen , dass integralnicht nicht subsumieren signed_integral, sondern signed_integral tut subsume integral, da es alles enthält integraltut.

Als nächstes kommen wir zur Bestellung von eingeschränkten Entitäten:

Eine Deklaration D1 ist mindestens so eingeschränkt wie eine Deklaration D2, wenn * D1 und D2 beide eingeschränkte Deklarationen sind und die zugehörigen Einschränkungen von D1 die von D2 subsumieren; oder * D2 hat keine zugehörigen Einschränkungen.

Weil signed_integralsubsumiert integral, <signed_integral> wrapperist das "mindestens so eingeschränkt" wie das <integral> wrapper. Das Gegenteil ist jedoch nicht der Fall, da die Subsumtion nicht umkehrbar ist.

In Übereinstimmung mit der Regel für "eingeschränktere" Entitäten:

Eine Deklaration D1 ist stärker eingeschränkt als eine andere Deklaration D2, wenn D1 mindestens so eingeschränkt ist wie D2 und D2 nicht mindestens so eingeschränkt ist wie D1.

Da das <integral> wrappernicht mindestens so eingeschränkt ist wie <signed_integral> wrapperdas letztere, wird das letztere als stärker eingeschränkt angesehen als das erstere.

Und daher gewinnt die eingeschränktere Deklaration, wenn beide zutreffen könnten.


Beachten Sie, dass die Regeln für die Subsumtion von Einschränkungen aufhören, wenn ein Ausdruck gefunden wird, der kein a ist concept. Wenn Sie dies getan haben:

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>;

In diesem Fall my_signed_integral würde nicht subsumieren std::integral. Obwohl C ++ my_is_integral_videntisch definiert ist std::is_integral_v, weil es kein Konzept ist, können die Subsumtionsregeln von C ++ nicht durchsehen, um festzustellen, ob sie identisch sind.

Die Subsumtionsregeln ermutigen Sie daher, Konzepte aus Operationen auf atomaren Konzepten zu erstellen.

Nicol Bolas
quelle
3

Mit Partial_ordering_of_constraints

Eine Bedingung P wird als Einschränkung Q bezeichnet, wenn nachgewiesen werden kann, dass P Q bis zur Identität der atomaren Einschränkungen in P und Q impliziert.

und

Die Subsumtionsbeziehung definiert die teilweise Reihenfolge der Einschränkungen, anhand derer Folgendes bestimmt wird:

  • der beste Kandidat für eine Nicht-Template-Funktion bei der Überlastungsauflösung
  • Die Adresse einer Nicht-Vorlagenfunktion in einem Überlastungssatz
  • Die beste Übereinstimmung für ein Vorlagenvorlagenargument
  • Teilbestellung von Klassenvorlagenspezialisierungen
  • Teilbestellung von Funktionsvorlagen

Und Konzept std::signed_integralfasst std::integral<T>Konzept zusammen:

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

Ihr Code ist also in Ordnung, ebenso wie std::signed_integral"spezialisierter".

Jarod42
quelle