Concepts C ++ 20: Quelle spécialisation de modèle est choisie lorsque l'argument de modèle se qualifie pour plusieurs concepts?

23

Donné :

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

Du code ci-dessus, intqualifie à la fois std::integralet std::signed_integralconcept.

Étonnamment, cela compile et imprime "signed_integral" sur les compilateurs GCC et MSVC. Je m'attendais à ce qu'il échoue avec une erreur du type "la spécialisation du modèle a déjà été définie".

D'accord, c'est légal, c'est juste, mais pourquoi a été std::signed_integralchoisi à la place de std::integral? Y a-t-il des règles définies dans la norme avec quelle spécialisation de modèle est choisie lorsque plusieurs concepts se qualifient pour l'argument de modèle?

Lewis Liman
la source
Je ne dirais pas qu'il est légal simplement par le fait que les compilateurs l'acceptent, en particulier à ce stade précoce de son adoption.
Slava
@Slava dans ce cas c'est le cas, les concepts sont soigneusement conçus pour qu'ils se subsument de manière intuitive
Guillaume Racicot
@GuillaumeRacicot ça va, je viens de commenter que la conclusion "c'est légal car le compilateur l'a acceptée" est disons trompeuse. Je n'ai pas dit que ce n'était pas légal cependant.
Slava

Réponses:

14

En effet, les concepts peuvent être plus spécialisés que d'autres, un peu comme la façon dont les modèles s'ordonnent. C'est ce qu'on appelle l'ordre partiel des contraintes

Dans le cas des concepts, ils se subsument mutuellement lorsqu'ils incluent des contraintes équivalentes. Par exemple, voici comment std::integralet std::signed_integralsont mis en œuvre:

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

En normalisant les contraintes, le compilateur résume l'expression de contraint à ceci:

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

Dans cet exemple, signed_integralimplique integralcomplètement. Donc, dans un sens, une intégrale signée est "plus contrainte" qu'une intégrale.

La norme l'écrit comme ceci:

À partir de [temp.func.order] / 2 (c'est moi qui souligne):

L'ordonnancement partiel sélectionne lequel des deux modèles de fonction est plus spécialisé que l'autre en transformant tour à tour chaque modèle (voir paragraphe suivant) et en effectuant la déduction des arguments de modèle à l'aide du type de fonction. Le processus de déduction détermine si l'un des modèles est plus spécialisé que l'autre. Si tel est le cas, le modèle le plus spécialisé est celui choisi par le processus de commande partielle. Si les deux déductions réussissent, l'ordre partiel sélectionne le modèle le plus contraint comme décrit par les règles dans [temp.constr.order] .

Cela signifie que s'il existe plusieurs substitutions possibles pour un modèle et que les deux sont choisis dans un ordre partiel, il sélectionnera le modèle le plus contraint.

Depuis [temp.constr.order] / 1 :

Une contrainte P subsume une contrainte Q si et seulement si, pour chaque clause disjonctive P i dans la forme normale disjonctive de P , P i subsume chaque clause conjonctive Q j dans la forme normale conjonctive de Q , où

  • une clause disjonctive P i subsume une clause conjonctive Q j si et seulement s'il existe une contrainte atomique P ia dans P i pour laquelle il existe une contrainte atomique Q jb dans Q j telle que P ia subsume Q jb , et

  • une contrainte atomique A subsume une autre contrainte atomique B si et seulement si A et B sont identiques en utilisant les règles décrites dans [temp.constr.atomic] .

Ceci décrit l'algorithme de subsomption que le compilateur utilise pour ordonner les contraintes et donc les concepts.

Guillaume Racicot
la source
2
On dirait que tu t'arrêtes au milieu d'un paragraphe ...
ShadowRanger
11

C ++ 20 a un mécanisme pour décider quand une entité contrainte particulière est "plus contrainte" qu'une autre. Ce n'est pas simple.

Cela commence par le concept de décomposer une contrainte en ses composants atomiques, un processus appelé normalisation des contraintes . C'est grand et trop complexe pour être abordé ici, mais l'idée de base est que chaque expression dans une contrainte est décomposée en ses pièces conceptuelles atomiques, récursivement, jusqu'à ce que vous atteigniez une sous-expression de composant qui n'est pas un concept.

Donc, étant donné cela, regardons comment les concepts integralet sont définis :signed_integral

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

La décomposition de integralest juste is_integral_v. La décomposition de signed_integralis is_integral_v && is_signed_v.

Nous arrivons maintenant au concept de subsomption de contraintes . C'est un peu compliqué, mais l'idée de base est qu'une contrainte C1 est censée «subsumer» une contrainte C2 si la décomposition de C1 contient chaque sous-expression dans C2. Nous pouvons voir que integralne subsume pas signed_integral, mais signed_integral ne subsumer integral, car il contient tout integralfait.

Ensuite, nous arrivons à classer les entités contraintes:

Une déclaration D1 est au moins aussi contrainte qu'une déclaration D2 si * D1 et D2 sont tous deux des déclarations contraintes et les contraintes associées de D1 subsument celles de D2; ou * D2 n'a pas de contraintes associées.

Parce que signed_integralsubsume integral, le <signed_integral> wrapperest "au moins aussi contraint" que le <integral> wrapper. Cependant, l'inverse n'est pas vrai, car la subsomption n'est pas réversible.

Par conséquent, conformément à la règle pour les entités "plus contraintes":

Une déclaration D1 est plus contrainte qu'une autre déclaration D2 lorsque D1 est au moins aussi contraint que D2, et D2 n'est pas au moins aussi contraint que D1.

Étant donné que le <integral> wrappern'est pas au moins aussi contraint que <signed_integral> wrapper, ce dernier est considéré comme plus contraint que le premier.

Et donc, lorsque les deux pourraient s'appliquer tous les deux, la déclaration la plus contrainte l'emporte.


Sachez que les règles de subsomption de contraintes s'arrêtent lorsqu'une expression est rencontrée qui n'est pas a concept. Donc, si vous avez fait ça:

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

Dans ce cas, my_signed_integral ne subsume passtd::integral . Même s'il my_is_integral_vest défini de manière identique à std::is_integral_v, car il ne s'agit pas d'un concept, les règles de subsomption de C ++ ne peuvent pas le parcourir pour déterminer qu'elles sont identiques.

Les règles de subsomption vous encouragent donc à construire des concepts à partir d'opérations sur des concepts atomiques.

Nicol Bolas
la source
3

Avec Partial_ordering_of_constraints

On dit qu'une contrainte P subsume la contrainte Q s'il peut être prouvé que P implique Q jusqu'à l'identité des contraintes atomiques dans P et Q.

et

La relation de subsomption définit l'ordre partiel des contraintes, qui est utilisé pour déterminer:

  • le meilleur candidat viable pour une fonction non modèle en résolution de surcharge
  • l'adresse d'une fonction non modèle dans un ensemble de surcharge
  • la meilleure correspondance pour un argument de modèle de modèle
  • classement partiel des spécialisations de modèle de classe
  • classement partiel des modèles de fonctions

Et le concept std::signed_integralsubsume le std::integral<T>concept:

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

Votre code est donc correct, tout comme il std::signed_integralest plus "spécialisé".

Jarod42
la source