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

Aucun commentaire: