Affichage des articles dont le libellé est cpp. Afficher tous les articles
Affichage des articles dont le libellé est cpp. Afficher tous les articles

dimanche 1 septembre 2024

Oeuf de canard - Retour à SDL

La bonne vieille bibliothèque SDL m'avait manqué. Tellement que j'avais raté que se préparait une version 3 ! Pas encore stable, j'ai préféré l'éviter pour le moment et me baser sur quelque chose de propre. Voici donc pour me remettre dans le bain un nouvel oeuf de canard pour réagir aux événements déclenchés par nos manettes. Signe des temps, je fournis maintenant une image Docker qui dérive d'une Debian Trixie et installe les dépendances nécéssaires. Normalement, il suffit de faire:

./refresh.sh # Créé l'image Docker et la démarre
./exec.sh # Execute un shell dans l'image Docker
cd /src 
./bootstrap.sh # Mise en place des Makefiles
make # Compilation
/buildb/bin/control # Test!

Petite surprise, je n'ai pas réussi à faire fonctionner le branchement à chaud des manettes : autant SDL détecte bien lorsque l'on débranche, l'inverse ne semble pas vrai. Je n'ai pas très envie de plonger dans le code de la SDL pour chercher si je l'utlise mal ou si c'est une limitation de l'implémentation Linux, donc tant pis, on fera sans.

lundi 15 juillet 2024

Conférence C++ On sea

J'ai passé un vendredi à la conférence C++ On Sea, c'était très sympa, même si la météo ce jour là était particulièrement excécrable et que je me suis fait bien saucer à l'aller (et un peu au retour).

Je parle un petit peu de la technique sur Linuxfr, mais j'ai vraiment apprécié l'ambiance, le côté geek et bon enfant, le bingo du C++ où chacun avait un petit papier avec les termes à cocher quand on les entendait lors d'une présentation. Le déjeuner était plutôt pas mal, et au retour on s'est fait des amis et on s'est retrouvé avec un bon groupe avec qui causer dans le train. À refaire, même si je sens une certaine réticence du côté de mon employeur à nous laisser trop profiter de ce genre d'événement sur le temps de travail.

mercredi 12 juin 2024

GTest est sympa

Je n'ai pas l'habitude d'encenser Google, mais quand ils font des trucs bien, il faut aussi savoir le dire. Je suis très impressionné par la bibliothèque GTest, qui est un cadriciel pour écrire des tests unitaires. C'est très bien pensé, et plein de bonnes idées : d'excellents comparateurs, une intégration démentielle avec CMake, mais surtout, la cerise sur le gâteau: un affichage console particulièrement pratique. Regardez donc ce programme:

#include <gtest/gtest.h>

TEST(Test, message)
{
  std::string s1("abc");
  std::string s2("def\nghi\njkl");

  ASSERT_EQ(s1, s2);
}

Quand on le fait tourner, on obtient:

gtest.cpp:9: Failure
Expected equality of these values:
  s1
    Which is: "abc"
  s2
    Which is: "def\nghi\njkl"
With diff:
@@ -1,1 +1,3 @@
-abc
+def
+ghi
+jkl

Regardez moi ça ! Il m'indique le nom des deux variables, leurs valeurs, et un diff ligne par ligne. Mais surtout, il m'affiche la valeur des variables non pas comme l'afficherait std::cout ou std::format, mais comme on devrait l'avoir dans le code source ! Et ça, c'est vraiment magique. Quand on a un résultat bien complexe, avec des caractères échappés, des retours chariots et tout et tout, juste pouvoir copier la sortie du test unitaire pour la coller telle quelle dans le source du test, quel confort ! La petite idée simple, mais il fallait y penser.

samedi 9 mars 2024

C++ : Chercher un fichier dans un répertoire - Partie 2

Et on continue à creuser ! Malheureusement, même avec une recherche récursive écrite en libc, cela prend encore trop de temps sur le système cible. En effet, il s'agit d'un système de fichiers un peu particulier, qui est censé monter en charge très fort mais dont les accès aux métadonnées sont individuellement plutôt lents. Et je ne peux pas échapper au besoin de devoir indexer un système de répertoires contenant environ 100 000 fichiers.

Le système de fichiers veut un accès concurrent ? On va lui en donner un. Malheureusement, ce n'est pas si simple ! En effet, il faut créer un algorithme à la fois récursif et parallèle, ce qui est un nid à problèmes. Pour l'instant, j'entrevois une solution qui consisterait à construire une liste des répertoires non visités, les visiter de manière parallèle pour aller chercher à chaque fois le prochain niveau, et continuer tant qu'on trouve. Le fait de gérer les tâches à partir du thread principal simpilife beaucoup de choses, mais empêche un très bon parallelisme.

Quelle tuile.

mercredi 21 février 2024

C++ : Chercher un fichier dans un répertoire

Petite séance d'optimisation aujourd'hui, où nous tentions d'améliorer une routine qui cherche des fichiers dans un répertoire.

En bon dev C++, nous sommes allés chercher ce bon vieux std::filesystem::path et ce non moins bon vieux std::filesystem::recursive_directory_iterator. Sauf que ça prend des plombes. Mais vraiment. Sur un système de fichiers distant (NFS) et un répertoire de 5000 fichiers pas accédé depuis quelques temps, cela peut monter à des dizaines de secondes. Mais que se passe-t-il ?

Avant de se lancer dans de complexes manipulations avec vtune ou encore gprof, pstack nous indique que l'on passe beaucoup de temps dans l'appel à stat. En effet, std::filesystem::recursive_directory_iterator nous donne un std::filesystem::directory_entry qui contient tout un tas d'infos tout à fait intéressantes, mais qui sont chères à récupérer...

Alors que peut-on faire ?

Eh bien, revenir aux bases, et à cette bonne vieille libc, qui contient l'appel readdir, lequel justement ne récupère pas d'informations sur le fichier autre que son type (ce qui est pratique pour explorer récursivement un répertoire). Et n'oublions pas non plus glob, lequel est justement conçu pour aller chercher un fichier efficacement à partir d'un motif.

Testons, alors. Et sur notre répertoire de 5000 fichiers, cela donne maintenant:

filesystem 40s
readdir 30ms
glob 3ms

Glob est donc plus de 10000 fois plus rapide qu'une recherche manuelle !

Conclusion : méfiance, donc ! Malgré ses qualités, std::filesystem::path peut coûter très cher, et revenir à des appels plus bas niveau est parfois salutaire.

samedi 16 septembre 2023

Accolades et std::optional

La nouvelle manière unifiée d'initialiser les valeurs en C++, c'est super. Bim, on met deux accolades, et on a gagné. Vraiment ?

Sur le papier, c'est bath, on peut pas se tromper:

int a = {};
std::string b = {};
std::optional<int> c = {};
MonObjectQuIlEstBeau d = {};

Sauf qu'il a danger ! C'est que lorsque l'on fait un changement de type quelque part, mettons un paramètre de fonction, et que l'on s'attend à ce que le compilo nous indique gentiment tous les endroits à changer, eh bien il est possible en fait que le compilo accepte le changement sans broncher, mais en changeant la sémantique. Et boum ! C'est ce qui m'est arrivé la semaine dernière, heureusement rapidement détecté. Voyez plutôt cet innocent programme, l'on a une fonction f qui prend un optionel, et on l'appelle en initialisant notre optionel sur vide.

#include <optional>

void f(std::optional<int> a);

int main()
{
    f({});
    return 0;
}

Un peu plus tard, l'on change la signature de f:

void f(int a);

Et boum, l'on initialise maintenant a avec la valeur 0, ce qui pourrait bien être complètement faux.

Que faire alors ? Eh bien, utilisons std::nullopt ! Ainsi, notre appel f(std::nullopt) fonctionnera correctement dans le premier cas, et causera une erreur de compilation dans le deuxième.

Conclusion : mangez du {}, mais avec les optionels, préférez std::nullopt !

dimanche 6 août 2023

Noexcept - Ce n'est pas gratuit !

Je parle régulièrement de culte du cargo dans le domaine informatique, où l'on a souvent ses petites marottes que l'on applique sans comprendre. Et j'ai un chef dont la marotte c'est d'exiger l'ajout du mot-clé "noexcept" un peu partout, avec l'espoir d'augmenter les performances.

Et c'est le cas, parfois. Par exemple, une réallocation d'un std::vector après un dépassement de capacité lors de l'ajout d'un élément sera bien plus efficace si le type de l'élément a un move constructor designé comme noexcept, car dans ce cas, l'algorithme de réallocation du vecteur peut utiliser le move. Il faut donc bien se rendre compte qu'au sein du code de std::vector, il y a une condition qui vérifie à la compilation si le type a son move constructor designé ainsi, et décide d'utiliser l'algo efficace ou non. Ce n'est donc pas une optimisation du compilateur, mais bien un choix de code au sein de la bibliothèque standard, et c'est complètement indépendant du niveau d'optimisation, de l'inlining...

Et surtout, noexcept n'est pas gratuit ! Si le compilo ne peut déterminer si effectivement la fonction marquée noexcept ne lance pas d'exception (disons, elle appelle simplement une autre fonction pas inlinée qui n'est pas déclarée noexcept), il lui faut rajouter du code pour pouvoir attraper cette exception et terminer le programme à la place. Voyons ce que dit notre ami Godbolt sur ce tout petit programme:

void f();

void g()
{
    f();
}

L'assembleur généré est celui-là, c'est à dire un bête jump et puis c'est tout !

g():
        jmp     f()

En revanche, si l'on dit maintenant que g() est noexcept:

void f();

void g() noexcept
{
    f();
}

Alors l'assembleur généré nous montre bien que l'on fait un peu plus de travail, en poussant la frame sur la pile, afin que si une exception était lancée dans f(), il soit possible de l'arrêter dans g().

g():
        subq    $8, %rsp
        call    f()
        addq    $8, %rsp
        ret

Conclusion que ne renieraient pas Plic et Ploc : réfléchir avant d'agir !

lundi 10 juillet 2023

Architecture CPU et optimisation du compilo

Et on continue de jouer avec Godbolt, en particulier la capacité de gcc d'optimiser du code numérique. Prenons par exemple une version simplifiée de la résolution d'une équation quadratique :

#include <cmath>

double calc(double a, double b, double c)
{
    double delta = b * b - 4 * a * c;
    return (-b + sqrt(delta)) / (2 * a);
}

Je vous laisserai aller voir sur Godbolt, mais en gros, ça nous fait une 30aine d'instructions assembleur.

La première grosse optimisation, c'est la combinaison de -O3 et de -ffast-math. On passe à 18 lignes (dont 12 seulement font quelque chose d'intéressant), et le compilo utilise l'instruction assembleur sqrtsd plutôt que d'appeler le sqrt de la libc. Mais l'on peut faire encore mieux ! Avec un -march=skylake, on perd 2 instructions, et la machine passe alors sur des instructions vectorisées. Plus rapide ? Moins rapide ? Je suppose que ça va dépendre ! On retrouve un code équivalent en passant chez AMD et -march=znver3.

Maintenant, le souci, c'est que je si, comme dans mon cas, je dois compiler du code qui tournera à la fois sur des machines récentes de chez Intel et AMD, eh bien je suis obligé de garder une architecture générique et de perdre l'avantage de toutes ces instructions magiques... Quand bien même elles sont peut-être en commun ?

Frustrant, donc. Mais probablement pas catastrophique, car le code que j'optimise n'est de toutes façons pas très numérique, et se vectorise probablement mal. Tant pis !

dimanche 14 mai 2023

Question C++ toute bête

Qu'est ce qui est le plus rapide ?

bool a = ...
if (a)
{
  a = false;
}

ou

bool a = ...
a = false;

En gros: est-ce que la lecture est significativement moins chère que l'écriture, et que cela vaut donc le coup de vérifier avant d'écrire la valeur ? Pour l'instant, je l'ignore. M'en vais faire tourner quelques benchmarks pour tenter d'élucider cette question...

lundi 1 mai 2023

Godbolt

Je vous ai déjà dit à quel point ce site était une tuerie ? L'idée de base est géniale : fournir une interface web permettant de tester un programme sur une grande variété de compilateurs, d'en voir l'assembleur généré, et le résultat. Avec une grande quantité de languages (C++, D, Java, Objective-C, Ocaml...) et une grande quantité de compilos (Pour le c++, nous avons pratiquement chaque version de gcc, de llvm, et du compilo de Microsoft, plus tout un tas de variations).

C'est vraiment très, très sympa pour bidouiller, tester une fonctionnalité à venir, vérifier un bug, faire de la micro-optimisation, ou juste partager une idée de code avec quelqu'un d'autre, parce qu'en plus chaque programme est enregistré via un lien unique. Très, très pratique.

jeudi 23 février 2023

C++20 - std::source_location

J'ai eu l'occasion d'expérimenter avec la structure std::source_location, qui est très bien fichue puisqu'elle s'initialise avec les informations courantes de fonction, de fichier source, et de ligne et de colonne dans le source.

Là où c'est très sympa, c'est qu'il est possible en utilisant un paramètre par défaut d'obtenir dans une fonction les informations de l'appelant ! C'est particulièrement pratique, par exemple, pour implémenter une fonction de log qui ne soit pas une immonde macro.

#include <source_location>
#include <iostream>
#include <string>

void log(std::source_location location = 
         std::source_location::current())
{
    std::cout << location.function_name() << std::endl;
}

void f()
{
    log();
}

template<typename T>
void g()
{
    log();
}

int main()
{
    f();
    g<std::string>();
    g<int>();
    return 0;
}

Et voilà le résultat :

void f()
void g() [with T = std::__cxx11::basic_string<char>]
void g() [with T = int]

Là où ça devient intéressant, c'est quand on veut le combiner avec une fonction de log variadique, de type std::format. Puisque l'on ne peut pas mettre le paramètre par défaut à la fin, car c'est ambigu, il faut ruser, et transformer le premier paramètre, typiquement la chaîne de formattage, en un objet spécial qui prend en premier paramètre la chaîne de formattage et en deuxième le paramètre std::source_location par défaut. Puis combiner tout cela avec du consteval et l'empaqueter dans un std::identity_type_t afin qu'il ne participe pas à la résolution des types variadiques, sinon ça ne marche pas !

Je posterai à l'occasion un exemple un peu plus complet. D'ici là, compilez bien.

mercredi 14 décembre 2022

Méthodes monadiques sur std::optional

Voilà que C++ se prend pour Haskell ! Maintenant, nous avons une belle interface monadique pour std::optional, les méthodes and_then, transform, or_else, qui permettent de chainer des appels sur un optionel. Je me suis demandé si le compilo pouvait optimiser entre les appels, et j'ai donc demandé au fidèle godbolt ce quí il en pensait. Le résultat est impressionnant. Voici deux fonctions qui font la même chose, l'une utilisant l'approche monadique (g++ 12.2, options -O3 -std=c++23):

#include <optional>

int f(const std::optional<int>& opt)
{
    return opt
    .transform([](auto&& val) { return val + 3;})
    .transform([](auto&& val) { return val - 11;})
    .value_or(7);
}

int g(const std::optional<int>& opt)
{
    if (opt)
    {
        return *opt + 3 - 11;
    }
    else
    {
        return 7;
    }
}

G++, sans broncher, nous sort, dans les deux cas, le même code optimisé aux petits oignons:

        cmpb    $0, 4(%rdi)
        movl    $7, %eax
        je      .L1
        movl    (%rdi), %eax
        subl    $8, %eax
.L1:
        ret

À noter que clang, encore mieux, nous débarasse carrément du saut conditionnel !

        movl    (%rdi), %ecx
        addl    $-8, %ecx
        cmpb    $0, 4(%rdi)
        movl    $7, %eax
        cmovnel %ecx, %eax
        retq

Ils sont forts, ces concepteurs de compilos. La conclusion, c'est donc que oui, le compilo peut optimiser à travers les appels monadiques. Mangez-en donc !

mercredi 7 décembre 2022

VulkanSceneGraph version 1.0

Je ne me souvenais pourtant pas de m'être abonné aux sorties de VulkanSceneGraph, mais j'avais la hype quand j'ai reçu un e-mail annonçant la version 1.0 de la nouvelle mouture du graphe de scène, mais basé sur Vulkan.

J'étais tellement enthousiaste que j'ai pondu une petite trilogie d'articles sur linuxfr.org : tout d'abord, une petite retrospective des techniques de rendu, moment nostalgie avec tous ces vieux jeux, puis un petit aperçu sur les graphes de scène, et enfin la présentation de VulkanSceneGraph.

Avec un support gltf aux petits oignons, je n'ai plus qu'à espérer un rapide développement des fonctionnalités d'animation, et je n'aurai plus d'excuses pour ne pas faire un truc avec tout ça.

samedi 6 août 2022

Grogne estivale

Je poste un petit coup de gueule sur LinuxFR. Il me tenait à coeur de batailler contre un certain manque de profondeur que l'on retrouve parfois, où pointer l'approche de grandes firmes des technologies de l'information semble suffisant pour justifier un choix technique ou méthodologique. J'en ai encore dans mon sac, en particulier sur les "12 factors app" (je ne mets pas de lien, ils ne le méritent pas), et sur la plaie que sont les gestionnaires de paquets particuliers à un language (Conda, je te regarde), par opposition aux paquets des distributions qui, je trouve, sont pour le coup une manière que je trouve particulièrement élégante et efficace de gérer ses dépendances.

Bref, profitez bien de votre été, mettez de la crème solaire, bisous !

dimanche 23 janvier 2022

C++20 - std::jthread

Coucou ! Cela faisait un petit moment que nous n'avions pas parlé de C++. Ça tombe bien, il y a plein de choses à dire. En particulier, causons aujourd'hui de std::jthread.

Ce n'est certes pas une révolution : il s'agit juste d'une version un petit peu plus évoluée que std::thread, qui embarque un indicateur qui permet de demander l'arrêt du thread, et qui a la propriété de demander cet arrêt, et de joindre le thread, dans le destructeur. Rien qui ne soit aisé à écrire soi-même, mais c'est du sucre syntaxique tout ce qu'il y a de plus agréable, et je trouve que cela s'approche plus de la philosophie RAII : là où l'ancien std::thread était vraiment un contrôle sur un thread, mais pouvait être détruit bien avant le thread, le nouveau std::jthread a plus une sémantique de possession, en s'assurant que le thread ne survivra pas à l'objet.

Subtilité supplémentaire: le foncteur passé à std::jthread peut prendre un paramètre supplémentaire de type std::stop_token qui permet de savoir si son propre thread doit s'arrêter.

Mais assez de parlotte, voyons le code:

#include <thread>
#include <iostream>

int main()
{
  std::jthread t
    ([](std::stop_token stoken)
     {
       while (!stoken.stop_requested())
       {
         std::cout << "Coucou !" << std::endl;
       }
     });

  using namespace std::literals::chrono_literals;
  std::this_thread::sleep_for(2s);

  return 0;
}

Grace à notre ami le std::jthread, pas besoin d'avoir un atomic qui traine, de bien penser à joindre à la fin du programme, et tout le toutim habituel. Le std::jthread nous prend par la main et s'assure que tout est bien nettoyé derrière. Sympa !

mercredi 21 avril 2021

Short string optimisation - Comment l'observer ?

Alors, on l'a, ou on l'a pas ? Si vous avez un compilo récent sur un système d'exploitation raisonable, la SSO, vous l'avez. Si vous avez un compilo pas si récent sur un système d'exploitation trop raisonable (Red Hat, je te regarde, la compatibilité, c'est bien, mais je veux mon SSO !), alors vous ne l'avez pas. Mais comment être sûr ? Eh bien, logiquement, si vous avez la SSO, cela veut dire que la capacité de votre chaîne est strictement supérieure à zéro, même si elle vient d'être créée vide. Alors, essayons !

#include <string>
#include <iostream>


int main()
{
  std::cout << std::string().capacity() << std::endl;
  return 0;
}

On compile, on exécute, et chez moi ça donne 15. Yeah !

mardi 6 octobre 2020

C++ 20 et modules

Ouh là là, c'est prise de tête ! J'ai tenté de comprendre comment fonctionnent les modules C++20, et c'est un vieux bazar. Pour comprendre toutes les subtilités de la fonctionnalité, je vous renvoie à l'excellent blog de vector<bool>, et en particulier aux articles suivants:

Lecture très saine, mais plutôt ardue. Je ne suis plus trop convaincu de l'intérêt de la fonctionnalité ! Les questions qui demeurent : comment est-ce que les systèmes de build vont traiter la chose ? Et au delà de la spécification touffue, quelle devrait être la doctrine d'utilisation ?

Pas d'expérimentations pour le moment, car gcc ne sait pas encore faire !

mercredi 30 septembre 2020

Le comité C++ prend enfin les vrais problèmes à bras le corps !

J'avoue, il m'arrive parfois de me planter lamentablement et d'initialiser une std::string avec un pointeur null. Comme ça marche avec un char *, on ne fait pas attention, et hop !

std::string a(nullptr);

Le souci, c'est que c'est un comportement non défini. Dans la pratique, ça crashe. Mais ne craignez rien ! Cette proposition suggère la mise en place d'une surcharge supprimée prenant un nullptr:

constexpr basic_string(nullptr_t) = delete;

Simple, mais génial: quand on passe un pointeur null, c'est cette surcharge, meilleure, qui est choisie, et ensuite le compilo hurle parce que la méthode est supprimée. Alors certes, ça ne couvre pas le cas où l'on passe une variable pointeur initialisée à null, parce que le compilo, même s'il pouvait déduire à la compilation que le pointeur est nul, doit quand même choisir la surcharge char *. Mais ça couvre quand même tout un tas de cas stupides qui ne peuvent clairement pas marcher.

Je soutiens unilatéralement cette proposition, et j'espère qu'elle atterrira dans C++23 !

vendredi 7 août 2020

C++17- variants, visiteurs, et duck typing

Je continue à m'amuser avec les visiteurs de variants, et je trouve d'autres manières de les utiliser. En particulier, il est possible de faire du duck typing, ou typage implicite, de manière très simple et très naturelle.

C'est à dire ? Eh bien, imaginez que vous ayez des objets qui ont des méthodes et des attributs en commun. Le duck typing, c'est de considérer une interface implicite avec ces méthodes et ces attributs. Ne pas avoir à créer une interface implicite, c'est en particulier la possibilité de travailler avec des objets que l'on ne contrôle pas (bibliothèques tierces, par exemple).

Voyons ça avec un petit exemple. Nous avons 3 classes, qui ont en commun la méthode makeNoise(), et l'on veut pouvoir les mettre dans une collection et appeler cette méthode. En programmation objet classique, l'on aurait une interface Noisy, par exemple, et l'on dériverait chaque classe de cette interface. Mais disons pour le besoin de l'exercice que ce n'est pas possible. Eh bien, collons les dans un variant, puis appelons la méthode makeNoise à l'aide d'un visiteur et d'une lambda générique. Et hop, ça marche !

#include <iostream>
#include <variant>
#include <vector>

class Duck
{
public:
  void walk()
  {
    std::cout << "Walk!" << std::endl;
  }
  
  void makeNoise()
  {
    std::cout << "Coin !" << std::endl;
  }
};

class Cat
{
public:
  void purr()
  {
    std::cout << "Rrrron, rrron" << std::endl;
  }
  
  void makeNoise()
  {
    std::cout << "Miaou !" << std::endl;
  }
};

class Dog
{
public:
  void moveTail()
  {
    std::cout << "Tail moving" << std::endl;
  }
  
  void makeNoise()
  {
    std::cout << "Ouaf !" << std::endl;
  }
};

int main()
{
  using AnimalsT = std::variant<Duck, Cat, Dog>;
  
  std::vector<AnimalsT> animals({Cat(), Dog(), Cat(), Duck()});

  for(auto && animal : animals)
  {
    std::visit([](auto && arg)
               {
                 arg.makeNoise();
               },
      animal);
  }
  
  return 0;
}

Le résultat est bien évidemment:

Miaou !
Ouaf !
Miaou !
Coin !

vendredi 31 juillet 2020

C++ 17 - De la magie avec std::visit et if constexpr

Le if constepr, voilà une fonctionnalité qu'elle est intéressante ! Elle permet en effet d'évaluer des conditions durant la phase de compilation, mais mieux, elle permet d'éviter la compilation des autres branches et donc du code qui sinon pourrait de pas compiler. C'est pas clair ? Voyons un exemple. Imaginons une fonction "increment" qui puisse prendre n'importe quel type numérique et l'incrémenter. On veut aussi que cela marche avec une chaîne, et dans ce cas il faut transformer la chaîne en entier, l'incrémenter, puis en refaire une chaîne. Bien sûr, la meilleure manière de faire est d'avoir une fonction template pour le cas général et une fonction prenant une std::string pour ce cas particulier, mais voyons quand même ce que cela donne.

En particulier, il est évident que ce code ne compile pas. En effet, inc(5) va provoquer une erreur de compilation dans la branche où on appelle std::stoi sur l'entier, et inc(std::string(8)) va provoquer une erreur dans la branche où on l'incrémente de 1.

#include <type_traits>
#include <string>
#include <iostream>

template<typename T>
T inc(T value)
{
  if(std::is_same_v<T, std::string>)
  {
    return std::to_string(std::stoi(value) + 1);
  }
  else
  {
    return value + 1;
  }
}

int main()
{
  std::cout << inc(5) << std::endl;
  std::cout << inc(std::string("8")) << std::endl;
  
  return 0;
}

Mais miracle, si l'on remplace notre if par un if constepr...

template<typename T>
T inc(T value)
{
  if constexpr(std::is_same_v<T, std::string>)
  {
    return std::to_string(std::stoi(value) + 1);
  }
  else
  {
    return value + 1;
  }
}

Alors ça compile gentiment ! À la compilation, la condition est évaluée, et le code non pertinent est simplement éliminé. Génial, non ?

Voici un exemple un peu plus utile. Nous avons un variant, et voulons en extraire n'importe quelle valeur arithmétique en la convertissant vers un double. Avec std::visit et if constexpr, c'est plutôt simple. Dans notre visiteur, nous pouvons directement vérifier si le type est arithmétique à l'aide d'un constexpr (sur le type decay, pour enlever les modificateurs constant et référence), et l'on peut ensuite utiliser directement le type. Sans le if constexpr, il aurait fallu créer une série de fonctions avec des std::enable_if de partout. C'est donc beaucoup, beaucoup mieux. Les if constexpr, c'est bon, mangez-en.

#include <type_traits>
#include <string>
#include <iostream>
#include <variant>

using VariantT = std::variant<std::string, int, double, unsigned short>;

bool getArithmeticValue(const VariantT & variant, double & value)
{
  return std::visit
    ([&value](auto && arg)
     {
       using T = std::decay_t<decltype(arg)>;
       if constexpr(std::is_arithmetic_v<T>)
       {
         value = static_cast<double>(arg);
         return true;
       }
       else
       {
         return false;
       }
     },
     variant);
}

int main()
{
  VariantT v1(std::string("abc"));
  VariantT v2(int(5));
  VariantT v3(double(2.3));

  double v = 0;
  std::cout << getArithmeticValue(v1, v) << std::endl;
  std::cout << getArithmeticValue(v2, v) << std::endl;
  std::cout << v << std::endl;
  std::cout << getArithmeticValue(v3, v) << std::endl;
  std::cout << v << std::endl;
  
  return 0;
}