Première impression de concepts



J'ai décidé de m'occuper de la nouvelle fonctionnalité C ++ 20 - concepts.

Les concepts (ou concepts , comme l'écrit le Wiki russophone) est une fonctionnalité très intéressante et utile qui fait défaut depuis longtemps.

Essentiellement, il tape des arguments de modèle.

Le principal problème des modèles avant C ++ 20 est que vous pouvez y substituer n'importe quoi, y compris quelque chose pour lequel ils n'ont pas été conçus du tout. Autrement dit, le système de modèles était complètement non typé. En conséquence, des messages d'erreur incroyablement longs et complètement illisibles se sont produits lors du passage du mauvais paramètre au modèle. Ils ont essayé de lutter contre cela avec l'aide de différents hacks linguistiques, que je ne veux même pas mentionner (même si j'ai dû faire face).

Les concepts sont conçus pour corriger ce malentendu. Ils ajoutent un système de saisie aux modèles, et c'est très puissant. Et maintenant, comprenant les caractéristiques de ce système, j'ai commencé à étudier les matériaux disponibles sur Internet.

Franchement, je suis un peu choqué :) Le C ++ est un langage déjà compliqué, mais au moins il y a une excuse: c'est arrivé. La métaprogrammation sur des modèles a été découverte, non définie, lors de la conception d'un langage. Et puis, lors du développement des prochaines versions du langage, ils ont été obligés de s'adapter à cette «découverte», car beaucoup de code était écrit dans le monde. Les concepts sont une opportunité fondamentalement nouvelle. Et, il me semble, une certaine opacité est déjà présente dans leur mise en œuvre. Peut-être est-ce une conséquence de la nécessité de prendre en compte l'énorme quantité de capacités héritées? Essayons de le comprendre ...

informations générales


Un concept est une nouvelle entité de langage basée sur la syntaxe du modèle. Un concept a un nom, des paramètres et un corps - un prédicat qui renvoie une valeur logique constante (c'est-à-dire calculée au stade de la compilation) en fonction des paramètres du concept. Comme ça:

template<int I> 
concept Even = I % 2 == 0;  

template<typename T>
concept FourByte = sizeof(T)==4;

Techniquement, les concepts sont très similaires aux modèles d'expressions constexpr comme bool:

template<int I>
constexpr bool EvenX = I % 2 == 0; 

template<typename T>
constexpr bool FourByteX = sizeof(T)==4;

Vous pouvez même utiliser des concepts dans des expressions courantes:

bool b1 = Even<2>; 

En utilisant


L'idée principale des concepts est qu'ils peuvent être utilisés à la place du nom de type ou des mots-clés de classe dans les modèles. Comme les métatypes ("types pour types"). Ainsi, la saisie statique est introduite dans les modèles.

template<FourByte T>
void foo(T const & t) {}

Maintenant, si nous utilisons int comme paramètre de modèle, alors le code dans la grande majorité des cas sera compilé; et si double, un message d'erreur court et compréhensible sera émis. Saisie simple et claire des modèles, jusqu'à présent tout va bien.

a besoin


Il s'agit d'un nouveau mot clé "contextuel" C ++ 20 avec un double objectif: requiert une clause et requiert une expression. Comme nous le verrons plus loin, ces économies de mots clés étranges entraînent une certaine confusion.

nécessite une expression


Tout d'abord, considérez requiert une expression. L'idée est assez bonne: ce mot a un bloc entre accolades, dont le code à l'intérieur est évalué pour la compilation. Certes, le code ne devrait pas être écrit en C ++, mais dans un langage spécial, proche de C ++, mais ayant ses propres caractéristiques (c'est la première bizarrerie, il était tout à fait possible de faire juste du code C ++).

Si le code est correct - l'expression requise renvoie true, sinon false. Le code lui-même, bien sûr, n'entre jamais dans la génération de code, tout comme les expressions de taille ou de type de declt.

Malheureusement, le mot est contextuel et ne fonctionne qu'à l'intérieur des modèles, c'est-à-dire qu'à l'extérieur du modèle, cela ne compile pas:

bool b = requires { 3.14 >> 1; };

Et dans le modèle - veuillez:

template<typename T>
constexpr bool Shiftable = requires(T i) { i>>1; };

Et cela fonctionnera:

bool b1 = Shiftable<int>; // true
bool b2 = Shiftable<double>; // false

L'utilisation principale de l'expression requiert la création de concepts. Par exemple, voici comment vous pouvez vérifier la présence de champs et de méthodes dans un type. Un cas très populaire.

template <typename T>
concept Machine = 
  requires(T m) {  //   `m` ,   Machine
	m.start();     //    `m.start()` 
	m.stop();      //   `m.stop()`
};  

Soit dit en passant, toutes les variables qui peuvent être requises dans le code testé (pas seulement les paramètres du modèle) doivent être déclarées entre parenthèses requiert une expression. Pour une raison quelconque, déclarer une variable n'est tout simplement pas possible.

La vérification de type à l'intérieur nécessite


C'est là que les différences entre le code requis et le C ++ standard commencent. Pour vérifier les types retournés, une syntaxe spéciale est utilisée: l'objet est pris entre accolades, une flèche est placée, et après cela, un concept est écrit que le type doit satisfaire. De plus, l'utilisation de types directement n'est pas autorisée.

Vérifiez que le retour de la fonction peut être converti en int:

requires(T v, int i) {
  { v.f(i) } -> std::convertible_to<int>;
}  

Vérifiez que la fonction de retour est exactement int:

requires(T v, int i) {
  { v.f(i) } -> std::same_as<int>; 
}  

(std :: same_as et std :: convertible_to sont des concepts de la bibliothèque standard).

Si vous n'incluez pas une expression dont le type est vérifié entre accolades, le compilateur ne comprend pas ce qu'il attend de lui et interprète la chaîne entière comme une seule expression qui doit être vérifiée pour la compilation.

nécessite à l'intérieur nécessite


Le mot clé require a une signification spéciale à l'intérieur des expressions require. Les expressions-requis imbriquées (déjà sans accolades) ne sont pas vérifiées pour la compilation, mais pour l'égalité vraie ou fausse. Si une telle expression s'avère fausse, alors l'expression englobante se révèle immédiatement fausse (et une analyse de compilation supplémentaire est interrompue). Forme générale:

requires { 
  expression;         // expression is valid
  requires predicate; // predicate is true
};

En tant que prédicat, par exemple, des concepts ou des traits de type définis précédemment peuvent être utilisés. Exemple:

requires(Iter it) {
  //     (   Iter   *  ++)
  *it++;
 
  //    -  
  requires std::convertible_to<decltype(*it++), typename Iter::value_type>;
 
  //    -  
  requires std::is_convertible_v<decltype(*it++), typename Iter::value_type>;
}

Dans le même temps, les expressions require imbriquées sont autorisées avec du code entre accolades, dont la validité est vérifiée. Cependant, si vous écrivez simplement une expression require dans une autre, la validité de l'expression imbriquée (tout dans son ensemble, y compris le mot clé nested requires) sera simplement vérifiée:

requires (T v) { 
  requires (typename T::value_type x) { ++x; }; //     , 
												//     !
};  

Par conséquent, une forme étrange avec double nécessite est apparue:

requires (T v) { 
  requires requires (typename T::value_type x) { ++x; }; //       "++x"
};  

Voici une séquence d'évasion amusante de «nécessite».

Soit dit en passant, une autre combinaison de deux nécessite cette clause de temps (voir ci-dessous) et l'expression:

template <typename T>
  requires requires(T x, T y) { bool(x < y); }
bool equivalent(T const& x, T const& y)
{
  return !(x < y) && !(y < x);
};

nécessite une clause


Passons maintenant à une autre utilisation du mot require - pour déclarer les restrictions d'un type de modèle. Il s'agit d'une alternative à l'utilisation de noms de concept au lieu de nom de type. Dans l'exemple suivant, les trois méthodes sont équivalentes:

//  require
template<typename Cont>
	requires Sortable<Cont>
void sort(Cont& container);

//   require (  )
template<typename Cont>
void sort(Cont& container) requires Sortable<Cont>;

//    typename
template<Sortable Cont>
void sort(Cont& container)  

La déclaration require peut utiliser plusieurs prédicats combinés par des opérateurs logiques.

template <typename T>
  requires is_standard_layout_v<T> && is_trivial_v<T>
void fun(T v); 
 
int main()
{
  std::string s;
 
  fun(1);  // ok
  fun(s);  // compiler error
}

Cependant, inversez simplement l'une des conditions, car une erreur de compilation se produit:

template <typename T>
  requires is_standard_layout_v<T> && !is_trivial_v<T>
void fun(T v); 

Voici un exemple qui ne compilera pas non plus

template <typename T>
  requires !is_trivial_v<T>
void fun(T v);	

La raison en est les ambiguïtés qui surviennent lors de l'analyse de certaines expressions. Par exemple, dans un tel modèle:

template <typename T> 
  requires (bool)&T::operator short unsigned int foo();

on ne sait pas à quoi attribuer non signé - l'opérateur ou le prototype de la fonction foo (). Par conséquent, les développeurs ont décidé que sans parenthèses, car les arguments nécessitent une clause, seul un ensemble très limité d'entités peut être utilisé - vrais ou faux littéraux, noms de champ de type bool de la forme valeur, valeur, T :: valeur, ns :: trait :: valeur, Noms de concept de type Concept et requiert des expressions. Tout le reste doit être placé entre parenthèses:

template <typename T>
  requires (!is_trivial_v<T>)
void fun(T v);

À propos des fonctionnalités de prédicat dans la clause requires


Prenons un autre exemple.

template <typename T>
  requires is_trivial_v<typename T::value_type> 
void fun(T v); 

Dans cet exemple, require utilise un trait qui dépend du type imbriqué value_type. On ne sait pas à l'avance si un type arbitraire a un tel type imbriqué qui peut être transmis au modèle. Si vous passez, par exemple, un type int simple à un tel modèle, il y aura une erreur de compilation, cependant, si nous avons deux spécialisations du modèle, il n'y aura pas d'erreur; juste une autre spécialisation sera choisie.

template <typename T>
  requires is_trivial_v<typename T::value_type> 
void fun(T v) { std::cout << "1"; } 
 
template <typename T>
void fun(T v) { std::cout << "2"; } 
 
int main()
{
  fun(1);  // displays: "2"
}

Ainsi, la spécialisation est rejetée non seulement lorsque le prédicat de la clause require renvoie false, mais également lorsqu'il s'avère incorrect.

Les parenthèses autour du prédicat sont un rappel important que dans la clause require, l'inverse du prédicat n'est pas l'opposé du prédicat lui-même. Donc,

requires is_trivial_v<typename T::value_type> 

signifie que le trait est correct et renvoie vrai.

!is_trivial_v<typename T::value_type> 

signifierait «le trait est correct et renvoie faux» La
véritable inversion logique du premier prédicat n'est PAS («le trait est correct et retourne vrai») == «le trait est INCORRECT ou retourne faux» - ceci est réalisé d'une manière légèrement plus complexe - grâce à une définition explicite du concept:

template <typename T>
concept value_type_valid_and_trivial 
  = is_trivial_v<typename T::value_type>; 
 
template <typename T>
  requires (!value_type_valid_and_trivial<T>)
void fun(T v); 

Conjonction et disjonction


Les opérateurs de conjonction et de disjonction logiques semblent comme d'habitude, mais fonctionnent en fait un peu différemment qu'en C ++ normal.

Considérez deux extraits de code très similaires.

Le premier est un prédicat sans parenthèses:

template <typename T, typename U>
  requires std::is_trivial_v<typename T::value_type>
		|| std::is_trivial_v<typename U::value_type>
void fun(T v, U u); 

La seconde est entre parenthèses:

template <typename T, typename U>
  requires (std::is_trivial_v<typename T::value_type>
		 || std::is_trivial_v<typename U::value_type>)
void fun(T v, U u); 

La différence est uniquement entre parenthèses. Mais pour cette raison, dans le deuxième modèle, il n'y a pas deux contraintes réunies par une "clause d'exigence", mais une contrainte par un OU logique habituel.

Cette différence est la suivante. Considérez le code

std::optional<int> oi {};
int i {};
fun(i, oi);

Ici, le modèle est instancié par les types int et std :: facultatif.

Dans le premier cas, le type int :: value_type n'est pas valide et la première limitation n'est donc pas satisfaite.

Mais le type optional :: value_type est valide, le deuxième trait renvoie true, et comme il y a un opérateur OR entre les contraintes, le prédicat entier est satisfait dans son ensemble.

Dans le second cas, il s'agit d'une expression unique contenant un type non valide, à cause de laquelle elle est invalide en général et le prédicat n'est pas satisfait. Des crochets si simples changent imperceptiblement la signification de ce qui se passe.

En conclusion


Bien sûr, toutes les fonctionnalités des concepts ne sont pas présentées ici. Je ne suis simplement pas allé plus loin. Mais comme première impression - une idée très intéressante et une mise en œuvre confuse quelque peu étrange. Et une syntaxe amusante avec répétition oblige, ce qui confond vraiment. Y a-t-il vraiment si peu de mots en anglais que vous avez dû utiliser un mot à des fins complètement différentes?

L'idée avec du code compilé est définitivement bonne. Il est même quelque peu similaire à la «quasi-citation» dans les macros de syntaxe. Mais valait-il la peine de mélanger la syntaxe spéciale pour vérifier les types de retour? À mon humble avis, pour cela, il serait simplement nécessaire de créer un mot-clé distinct.

Mélange implicite des concepts «vrai / faux» et «compile / ne compile pas» dans un seul tas, et par conséquent, les blagues avec des crochets sont également erronées. Ce sont des concepts différents, et ils doivent exister strictement dans des contextes différents (bien que je comprenne d'où ils viennent - de la règle SFINAE, où le code non compilé excluait silencieusement la spécialisation de la considération). Mais si le but des concepts est de rendre le code aussi explicite que possible, cela valait-il la peine de faire glisser toutes ces choses implicites dans de nouvelles fonctionnalités?

L'article a été écrit principalement sur la base de
akrzemi1.wordpress.com/2020/01/29/requires-expression
akrzemi1.wordpress.com/2020/03/26/requires-clause
(il y a beaucoup plus d'exemples et de fonctionnalités intéressantes)
avec mes ajouts de d'autres sources,
tous les exemples peuvent être vérifiés surwandbox.org

All Articles