Partage
  • Partager sur Facebook
  • Partager sur Twitter

[C++20] Concept pour tester si un nombre est nul

et une parenthèse sur std::cout

Sujet résolu
    4 avril 2021 à 22:24:19

    Bonsoir à tous,

    J'ai trouvé ce lien qui explique les fonctionnalités du C++20(*), et j'ai donc testé les concepts pour déterminer si un nombre est nul :

    template<typename ValueType>
    concept isNotNull = requires(const ValueType& value) {
    	1 / value;
    };

    Le problème, c'est qu'il teste si écrire 1 / value est valide au moment de la compilation.

    Si je fais 1 / 0; le compilateur me crache une erreur car 0 est un littéral (de même avec un modulo), alors que

    constexpr unsigned short zero{ 0u };
    1 / zero;

    ne cause aucune erreur de compilation (mais au moins un warning), ce qui fait que le concept est toujours vrai.

    Il faudrait donc que la valeur soit réécrite directement dans le requires, donc faire une sorte de macro C ou d'inline, mais je n'ai rien vu sur Internet qui en parle (d'autant plus que C++20 est encore tout récent).

    Soit ce code :

    class Number {
            template<typename ValueType>
    	Number& operator/(ValueType other) requires isNotNull<ValueType> {
    		// du code
    	}
    };
    
    int main() {
        Number n { 4 };
        std::cout << n / 0 << std::endl; // operator/ est exécuté quand même
    }



    * et qu'en est-il des constexpr vector / string, ce lien indique clairement que c'est (déjà) possible en C++20 ? Est-ce reporté à C++23 ?

    Petite parenthèse sur std::cout.

    Quand on fait 

    std::cout << quelqueChose << autreChose;

    on utilise bien l'opérateur <<, le même que le décalage binaire ?

    Si oui, il y a une incohérence, est-il logique (et acceptable) de "détourner" un opérateur qui vraisemblablement ne devrait prendre qu'un entier en paramètre pour lui faire accepter (presque) n'importe quoi d'autre ?

    Et dans ce cas, il n'y a aucun décalage au niveau des bits, si ?

    -
    Edité par Chi_Iroh 4 avril 2021 à 22:27:51

    • Partager sur Facebook
    • Partager sur Twitter
      4 avril 2021 à 22:58:54

      Ca ne semble pas être un bon prétexte d'utilisation des concepts. Si vraiment tu veux passer un truc qui n'est pas nul, tu passes par une assertion. Les concepts ont été imaginés pour des contraintes de types, ou intrinsèquement liées au type.

      Pour constexpr string/vector, il faut voir du côté du support de la norme par les compilos: https://en.cppreference.com/w/cpp/compiler_support

      Pour l'histoire de "<<", tu te mélanges les pinceaux. La surchage d'opérateurs nous donne le droit de gérer le comportement des opérateurs en fonction de types de données précis. D'un côté, "<<" sert au décalage de bits, d'un autre, il sert à faire "sortir" des données dans des objets capables de recevoir ces données. Il n'y a aucune incohérence, cet opérateur n'a pas été conçu "juste pour le décalage de bits".

      P.S.: "isNotNull" c'est plus le nom d'un trait que d'un concept.

      • Partager sur Facebook
      • Partager sur Twitter
        5 avril 2021 à 2:11:47

        Si de prime abord, c'est extrêmement séduisant, c'est tout autant extrêmement limité dans la pratique.

        Restons simple sans introduire de fonction complexe comme un sinus. Supposons que l'on enchaîne des fonctions pour arriver à un 1/(42-abs(x)). Si pour l'inversion, il nous faut le not-null, pour l'étape d'avant il faut le not-42, et celle d'avant un neither-42-nor-minus-42.

        On a vie fait d'exploser la quantité de concepts nécessaires. Cela me rappelle un "vieux" POC: https://openclassrooms.com/forum/sujet/types-opaques-domaines-de-definitions#message-91348503

        > Si oui, il y a une incohérence, est-il logique (et acceptable) de "détourner" un opérateur qui vraisemblablement ne devrait prendre qu'un entier en paramètre pour lui faire accepter (presque) n'importe quoi d'autre ?

        On n'est plus à ça prêt depuis le C, on détourne déjà l'opérateur de déréférencement pour lui faire faire des multiplications... Ou encore des additions avec l'opérateur de concaténation en C++, Python...

        • Partager sur Facebook
        • Partager sur Twitter
        C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
          5 avril 2021 à 21:14:38

          D'abord merci pour vos réponses.

          @Daimyo_, merci pour le lien, j'ai aussi trouvé celui-là (spécifique à VS).

          Je n'avais que vaguement vu les traits dans la STL (surtout char_traits en paramètre template de std::basic_string), et je me suis tourné vers std::enable_if qui avait l'air intéressant.

          Seulement, il y a un problème : il ne peut évaluer que des expressions constantes, donc forcément valeur dans un template et donc adieu la surcharge d'opérateurs.

          @lmghs, j'ai regardé votre lien et je n'ai absolument pas le niveau pour comprendre, ni le topic, ni le code :D

          Pour les assertions, je ne pense pas, j'ai créé deux fonctions pour les messages d'erreur au runtime, ça affiche un message d'erreur, et ça stoppe (ou pas) le programme.

          En soi, l'approche des assertions est logique, mais je voulais juste tester une nouvelle fonctionnalité.

          Merci à vous et bonne continuation.

          -
          Edité par Chi_Iroh 6 avril 2021 à 22:15:32

          • Partager sur Facebook
          • Partager sur Twitter
            6 avril 2021 à 22:15:38

             EDIT :

            J'ai essayé de faire un code avec un trait, je me suis inspiré de l'implémentation de la lib standard pour comprendre leur fonctionnement (std::remove_cv, std::is_arithmetic ...).

            C'est un peu lourd, mais ça fonctionne :

            #include <type_traits>
            
            template<auto val, typename ValueType = std::remove_cv_t<decltype(val)>> requires std::is_arithmetic<ValueType>::value
            struct isNull {
            	static constexpr bool value{ val == 0 };
            };
            
            template<auto val1, auto val2>
            constexpr auto div() requires std::is_same_v<std::remove_cvref_t<decltype(val1)>, std::remove_cvref_t<decltype(val2)>> && (!isNull<val2>::value) {
            	return val1 / val2;
            }
            
            #include <iostream>
            
            int main() {
            	std::cout << div<1u, 2u>() << std::endl; // 0
            	std::cout << div<4.f, 8.f>() << std::endl; // 0.5
            	// std::cout << div<1, 0>() << std::endl;  compilation error => constraints are not satisfied
            	return 0;
            }
             Des conseils ?
            • Partager sur Facebook
            • Partager sur Twitter
              8 avril 2021 à 8:48:20

              Je up le topic juste pour savoir ce que vous pensez quand à mon implémentation.

              Est-ce correct ?

              Y a-t-il des façon plus simples de le faire ?

              -
              Edité par Chi_Iroh 8 avril 2021 à 8:48:30

              • Partager sur Facebook
              • Partager sur Twitter
                8 avril 2021 à 21:40:06

                auto div(auto const& lhs, auto const& rhs) {
                  assert(rhs != 0);
                  return lhs / rhs;
                }

                Je pense pas qu'il faille plus que ça.

                Probablement même mieux:

                assert(b != 0)
                auto res = a / b;
                // avec un meilleur nommage, bien-sur

                -
                Edité par Daimyo_ 8 avril 2021 à 21:46:15

                • Partager sur Facebook
                • Partager sur Twitter
                  8 avril 2021 à 21:59:21

                  Merci pour ta réponse. Si ma question peut paraître insensée, c'est parce que je travaille sur un projet de fonctions mathématiques (je recommence tout ou presque, voir lien dans ma signature, car code assez mauvais), et j'ai prévu ça pour détecter ce genre d'erreur a la compilation (des que les constexpr string seront supportées (même si le preview de MSVC 16.10 les prend en charge)), tout en gardant une version runtime avec des tests et un affichage personnalisé. (J'aurais peut être du le préciser avant...)
                  • Partager sur Facebook
                  • Partager sur Twitter
                    8 avril 2021 à 22:27:08

                    Lu',

                    vouloir vérifier une telle assertion à la compilation implique que tous tes calculs doivent être faits à la compilation  (du moins tous ceux qui doivent effectuer une cette vérification) . Ecrire une bibliothèque mathématique qui fonctionne à la compilation est assez hardcore. Sans parler du fait qu'en général, les mathématiques en informatique s'utilisent sur des données que le programmeur ne connait pas au moment où il écrit son code. Dit autrement, ta bibliothèque sera limité à une utilisation extrêmement spécifique. Si tu souhaites quand même persister dans cette approche, tu peux regarder du coté de chez boost::hana pour avoir une première une idée de comment ça marche. 

                    -
                    Edité par eugchriss 8 avril 2021 à 22:28:41

                    • Partager sur Facebook
                    • Partager sur Twitter

                    Eug

                      8 avril 2021 à 23:08:46

                      assert est utilisable avec des fonctions constexpr. Le résultat obtenu n'est pas forcément des plus clairs, mais il est exploitable dans ce contexte.

                      Il y a un vieil article d'Eric Neibler qui aborde le sujet relativement aux fonctions constexpr et assert et les limitations de l'époque du C++20. Je pense que c'est toujours pertinent. J'ai souvenir avoir donné un lien depuis mon 3e billet sur la PpC. Et... C'est ça https://luchermitte.github.io/blog/2015/09/17/programmation-par-contrat-snippets-pour-le-c-plus-plus/#i3--pr--et-post-conditions-de-fonctions--constexpr

                      Accessoirement, une fonction constexpr peut être employée depuis un contexte dynamique, et les concepts ne serviront à rien ici. Un typage fort sera probablement plus utile, avec les limitations  indiquées précédemment. Cf mon précédent post que tu avais trouvé "pointu". Un truc comme ça: https://godbolt.org/z/rTxobfr4T et on voit, que c'est aussi beaucoup s'embêter pour rien que cela ne va pas couvrir les cas où c'est pas-2 qu'il faut recevoir...

                      Bref. Les concepts ne vont pas t'aider ici.

                      • Partager sur Facebook
                      • Partager sur Twitter
                      C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
                        9 avril 2021 à 12:04:59

                        Merci pour vos réponses.

                        @eugchriss, tu me suggères donc d'abandonner constexpr ?

                        Où y a-t-il des opérations où le mettre est pertinent (factorielle ?)

                        Et effectivement, ça a l'air assez complexe (je m'encombre déjà d'un namespace math::traits avec des variables template constexpr (accompagnés de plein de requires ici et là), voir ci-dessous)...

                        @lmghs, je vais essayer de m'inspirer de ton implémentation de la factorielle.

                        Code foireux que j'ai actuellement :

                        namespace math::traits {
                        
                        	template<typename ValueType>
                        	using BaseType = std::remove_cvref_t<ValueType>;
                        
                        	template<auto value>
                        	using ValueBaseType = BaseType<decltype(value)>;
                        
                        	template<typename ValueType>
                        	static constexpr bool isMathLibType{ utils::isIn<BaseType<ValueType>, Number> };
                        
                        	template<auto value>
                        	static constexpr bool isMathLibValue{ isMathLibValue<value> };
                        
                        	template<typename ValueType>
                        	static constexpr bool isMathType{ std::is_arithmetic_v<BaseType<ValueType>> || isMathLibType<BaseType<ValueType>> };
                        
                        	template<auto value>
                        	static constexpr bool isMathValue{ isMathType<decltype(value)> };
                        
                        	template<auto value> requires isMathValue<value>
                        	static constexpr bool isNull{ value == 0 };
                        
                        	template<auto value> requires isMathValue<value>
                        	static constexpr bool isStrictlyPositive{ value > 0 };
                        
                        	template<auto value> requires isMathValue<value>
                        	static constexpr bool isPositiveOrNull{ value >= 0 };
                        
                        	template<auto value> requires isMathValue<value>
                        	static constexpr bool isStrictlyNegative{ value < 0 };
                        
                        	template<auto value> requires isMathValue<value>
                        	static constexpr bool isNegativeOrNull{ value <= 0 };
                        
                        	template<auto value> requires isMathValue<value>
                        	static constexpr bool isInteger{ isMathLibValue<value> ? value.isInteger() : std::floorl(value) == value }; // will be constexpr when MathLib types will become constexpr (so when constexpr std::string will be supported)
                        
                        	template<auto value> requires isMathValue<value>
                        	static constexpr bool isNatural{ isPositiveOrNull<value> && isInteger<value> };
                        }
                        
                        



                        • Partager sur Facebook
                        • Partager sur Twitter
                          9 avril 2021 à 12:40:04

                          As-tu joué avec le code sur godbolt?

                          As-tu vu que le compilateur détecte déjà les divisions par 0 dans les expressions constantes?

                          Si cela a un sens d'avoir un renforcement de type sur les pointeurs pour avoir des zones où l'on a la certitude qu'un pointeur n'est pas nul, on ne peut pas étendre ces zones aux nombres à cause de `1/x-1`. Rien que là, si x est strictement positif, on perd ces propriétés sur `x-1`. Ce cas est simple. C'est pour cela que depuis le début j'insiste que d'avoir des propriétés fortes sur des nombres c'est séduisant, mais pas vraiment exploitable.

                          PS: Mais c'est quoi ces static? Il y a confusion avec le Java ici, non?

                          -
                          Edité par lmghs 9 avril 2021 à 12:41:06

                          • Partager sur Facebook
                          • Partager sur Twitter
                          C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
                            9 avril 2021 à 13:53:35

                            J'ai essayé ça

                            int main() {
                                constexpr unsigned zero{ 0u };
                                constexpr unsigned zeroDivisionError{ 7u / zero };
                                // compilation error
                                return 0;
                            }

                            et (à ma grande surprise, je dois l'admettre), ça ne compile pas (ce qui est logique).

                            Je vois ce que vous voulez dire quand vous dîtes que c'est très complexe de tester à la compilation chaque cas...

                            Alors non je n'ai jamais fait à proprement parler de Java (3-4 heures à tout casser avec Android Studio juste avant de démarrer C++, juste pour tester de l'Android), mais j'ai déjà aperçu des codes Java sur Internet, et effectivement ça déborde de static, override etc..

                            Si j'ai utilisé ce mot-clé ici, c'était pour ne calculer la variable qu'une seule fois, comme dans les classes ou les fonctions :

                            class Foo {
                                Type bar_{}; // chaque instance de Foo possède son propre membre bar_
                                static Type bar2_{}; // toutes les instances de Foo ont accès au membre bar2_
                            }

                            Donc si j'ai bien compris, la solution la plus pertinente est de virer tous les constexpr, requires et tout le bordel qui va avec et de les remplacer par des assertions au runtime (ce qui est effectivement logique si constexpr est non-pertinent) ?

                            • Partager sur Facebook
                            • Partager sur Twitter
                              9 avril 2021 à 14:39:45

                              On peut tout laisser en constexpr si l'application est plausible.

                              Ce qui par contre va vite virer au cauchemar, c'est de typer fortement tous les cas possibles, surtout si  l'objectif est d'intercepter des domaines de définitions (x >= 0, x != 0, etc). Le langage n'est pas pret pour ça AMA. On s'en sortira mieux avec les contrats en C++23 si tout va bien, avec les assertions en attendant.

                              Pour static, il a plusieurs utilités en C++:

                              - une variable globale ou un fonction libre globale déclarée static sera cachée dans l'unité de traduction (.cpp avec tous les #inclue & cie résolus) où elle apparaît. Cas hérité du C.

                              - une variable locale sera persistante entre 2 appels à la fonctions

                              - une variable membre ou une fonction membre seront de nouveaux globales mais leur nom sera décoré par le nom de la classe où elles sont déclarées. Elles ne seront pas cachés comme dans le cas des globales.

                              Et donc, le cas des variables statiques dans un espace de noms ben... c'est un cas bâtard du cas 1. Cela ne correspond à aucun cas d'utilisation désiré que je connaisse -- je n'exclue pas que des gens trouvent une application. Toujours est-il que je doute que cela soit ce que tu cherches à faire.

                              -
                              Edité par lmghs 9 avril 2021 à 14:42:08

                              • Partager sur Facebook
                              • Partager sur Twitter
                              C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.
                                9 avril 2021 à 14:58:40

                                Bon, j'essaie d'implémenter une factorielle au runtime: 

                                auto factorial(const auto& value) -> decltype(value) {
                                	if (!traits::isMathType<decltype(value)>) {
                                		handleError("Attempt to compute factorial of a non-math typed value (" + utils::toString(value) + ") !", __FILE__, __LINE__);
                                	}
                                	if (value < 0) {
                                		handleError("Attempt to compute factorial of a negative number :\n'" + utils::toString(value) + "'\n", __FILE__, __LINE__);
                                	}
                                	return (value > 0) ? (value * factorial(value - 1)) : 1;
                                }

                                et le compilo me fait gentiment remarquer qu'il n'apprécie pas la récursivité dans une fonction auto (d'où le -> decltype(value) qui ne résout pas le problème).

                                Donc la seule autre solution serait de faire une fonction avec un paramètre long double (le type builtin le plus large) et autant de surcharges que de types de ma bibliothèque (à moins de faire une classe mère abstraite MathType avec aucune fonction, mais pour passer un const MathType& en paramètre et n'avoir qu'une surcharge qui utilise l'héritage ?).

                                -
                                Edité par Chi_Iroh 9 avril 2021 à 15:00:49

                                • Partager sur Facebook
                                • Partager sur Twitter
                                  11 avril 2021 à 18:18:11

                                  Mea culpa, on peut faire des choses avec le C++20, mais c'est plus de strong-types que de concepts que l'on va véritablement avoir besoin AMA.

                                  Cela m'a inspiré de la sorte: https://godbolt.org/z/cfhPc3xjx (attention, il faut le GCC du trunk pour que cela compile). Mais il reste encore une limitation: comment convertir automatiquement 42.5 en `set<double, 42.5, 4.5>` ,

                                  Cf la discussion  que j'ai ouverte à ce sujet https://openclassrooms.com/forum/sujet/auto-deduction-de-non-type-template-param-double

                                  • Partager sur Facebook
                                  • Partager sur Twitter
                                  C++: Blog|FAQ C++ dvpz|FAQ fclc++|FAQ Comeau|FAQ C++lite|FAQ BS| Bons livres sur le C++| PS: Je ne réponds pas aux questions techniques par MP.

                                  [C++20] Concept pour tester si un nombre est nul

                                  × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
                                  × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
                                  • Editeur
                                  • Markdown