web-dev-qa-db-fra.com

Comment utiliseriez-vous le <T> attendu d'Alexandrescu avec des fonctions vides?

J'ai donc rencontré cette idée très intéressante (IMHO) d'utiliser une structure composite composée d'une valeur de retour et d'une exception - Expected<T>. Il pallie de nombreuses faiblesses des méthodes traditionnelles de traitement des erreurs (exceptions, codes d'erreur).

Voir le l'exposé de Andrei Alexandrescu (Traitement d'erreur systématique en C++) } et ses diapositives .

Les exceptions et les codes d'erreur ont essentiellement les mêmes scénarios d'utilisation, avec des fonctions qui renvoient quelque chose et celles qui n'en renvoient pas. Expected<T>, en revanche, semble ne cibler que les fonctions qui renvoient des valeurs.

Donc, mes questions sont:

  • L'un de vous a-t-il essayé Expected<T> en pratique?
  • Comment appliqueriez-vous cet idiome à des fonctions ne retournant rien (c'est-à-dire des fonctions vides)?

Mise à jour:

Je suppose que je devrais clarifier ma question. La spécialisation Expected<void> a du sens, mais je suis plus intéressée par la façon dont elle serait utilisée - l'idiome d'utilisation cohérente. L'implémentation elle-même est secondaire (et facile).

Par exemple, Alexandrescu donne cet exemple (un peu édité):

string s = readline();
auto x = parseInt(s).get(); // throw on error
auto y = parseInt(s); // won’t throw
if (!y.valid()) {
    // ...
}

Ce code est "propre" de manière à ce qu'il coule naturellement. Nous avons besoin de la valeur - nous l'obtenons. Cependant, avec expected<void>, il faudrait capturer la variable renvoyée et effectuer une opération dessus (comme .throwIfError() ou quelque chose), ce qui n’est pas aussi élégant. Et évidemment, .get() n'a pas de sens avec vide.

Ainsi, à quoi ressemblerait votre code si vous aviez une autre fonction, par exemple toUpper(s), qui modifie la chaîne in-situ et n'a pas de valeur de retour?

34
Alex

Avez-vous essayé Expected? en pratique?

C'est assez naturel, je l'ai utilisé avant même d'avoir vu cette présentation.

Comment appliqueriez-vous cet idiome à des fonctions ne retournant rien (c'est-à-dire des fonctions vides)?

Le formulaire présenté dans les diapositives a des implications subtiles:

  • L'exception est liée à la valeur.
  • Vous pouvez gérer l'exception comme bon vous semble.
  • Si la valeur est ignorée pour certaines raisons, l'exception est supprimée.

Cela ne tient pas si vous avez expected<void>, car comme personne ne s'intéresse à la valeur void, l'exception est toujours ignorée. Je forcerais ceci comme je forcerais la lecture de expected<T> dans la classe Alexandrescus, avec des assertions et une fonction membre suppress explicite. Renvoyer l'exception du destructeur n'est pas autorisé pour de bonnes raisons, il faut donc le faire avec des assertions.

template <typename T> struct expected;

#ifdef NDEBUG // no asserts
template <> class expected<void> {
  std::exception_ptr spam;
public:
  template <typename E>
  expected(E const& e) : spam(std::make_exception_ptr(e)) {}
  expected(expected&& o) : spam(std::move(o.spam)) {}
  expected() : spam() {}

  bool valid() const { return !spam; }
  void get() const { if (!valid()) std::rethrow_exception(spam); }
  void suppress() {}
};
#else // with asserts, check if return value is checked
      // if all assertions do succeed, the other code is also correct
      // note: do NOT write "assert(expected.valid());"
template <> class expected<void> {
  std::exception_ptr spam;
  mutable std::atomic_bool read; // threadsafe
public:
  template <typename E>
  expected(E const& e) : spam(std::make_exception_ptr(e)), read(false) {}
  expected(expected&& o) : spam(std::move(o.spam)), read(o.read.load()) {}
  expected() : spam(), read(false) {}

  bool valid() const { read=true; return !spam; }
  void get() const { if (!valid()) std::rethrow_exception(spam); }
  void suppress() { read=true; }

  ~expected() { assert(read); }
};
#endif

expected<void> calculate(int i)
{
  if (!i) return std::invalid_argument("i must be non-null");
  return {};
}

int main()
{
  calculate(0).suppress(); // suppressing must be explicit
  if (!calculate(1).valid())
    return 1;
  calculate(5); // assert fails
}
11
ipc

Même si cela peut paraître nouveau pour quelqu'un qui se concentre uniquement sur les langues C-ish, pour ceux d'entre nous qui avaient un avant-goût des langues prenant en charge les types de somme, ce n'est pas le cas.

Par exemple, dans Haskell, vous avez:

data Maybe a = Nothing | Just a

data Either a b = Left a | Right b

Où le | lit ou et le premier élément (Nothing, Just, Left, Right) est simplement une "balise". Essentiellement, les types de somme ne sont que unions discriminantes.

Ici, vous devriez avoir Expected<T> quelque chose comme: Either T Exception avec une spécialisation pour Expected<void> qui s'apparente à Maybe Exception.

13
Matthieu M.

Comme l’a dit Matthieu M., c’est quelque chose de relativement nouveau en C++, mais rien de nouveau pour de nombreux langages fonctionnels. 

Je voudrais ajouter mes 2 centimes ici: une partie des difficultés et des différences réside peut-être dans l’approche «procédurale vs fonctionnelle». Et j'aimerais utiliser Scala (car je connais à la fois Scala et C++, et j’ai le sentiment qu’il dispose d’une fonction (Option) plus proche de Expected<T>) pour illustrer cette distinction.

En Scala, vous avez l'option [T], qui est soit Some (t), soit None. En particulier, il est également possible d'avoir Option [Unit], qui équivaut moralement à Expected<void>.

Dans Scala, le modèle d'utilisation est très similaire et construit autour de 2 fonctions: isDefined () et get (). Mais il a aussi une fonction "map ()".

J'aime penser que "map" est l'équivalent fonctionnel de "isDefined + get":

if (opt.isDefined)
   opt.get.doSomething

devient

val res = opt.map(t => t.doSomething)

"propager" l'option au résultat

Je pense qu’ici, dans ce style fonctionnel d’utilisation et de composition, se trouve la réponse à votre question:

Ainsi, à quoi ressemblerait votre code si vous aviez une autre fonction, par exemple toUpper (s), qui modifie la chaîne sur place et n'a pas de valeur de retour?

Personnellement, je ne modifierais PAS la chaîne en place, ou du moins je ne retournerai rien. Je considère Expected<T> comme un concept "fonctionnel", qui nécessite un modèle fonctionnel pour fonctionner correctement: toUpper (s) aurait besoin de renvoyer une nouvelle chaîne ou de se retourner après modification:

auto s = toUpper(s);
s.get(); ...

ou, avec une carte semblable à Scala

val finalS = toUpper(s).map(upperS => upperS.someOtherManipulation)

si vous ne souhaitez pas suivre un itinéraire fonctionnel, vous pouvez simplement utiliser isDefined/valid et écrire votre code de manière plus procédurale:

auto s = toUpper(s);
if (s.valid())
    ....

Si vous suivez cette route (peut-être parce que vous en avez besoin), il existe un point "vide par rapport à l'unité": historiquement, void n'était pas considéré comme un type, mais "aucun type" (void foo () était considéré comme un Pascal procédure). L'unité (telle qu'utilisée dans les langages fonctionnels) est plutôt vue comme un type signifiant "un calcul". Donc, renvoyer une Option [Unité] a plus de sens, étant vu comme "un calcul qui fait éventuellement quelque chose". Et dans Expected<void>, void prend un sens similaire: un calcul qui, lorsqu'il fonctionne comme prévu (en l'absence de cas exceptionnels), se termine simplement (en ne renvoyant rien). Au moins, IMO!

Ainsi, utiliser Expected ou Option [Unit] peut être vu comme des calculs pouvant produire un résultat, ou non. Les chaîner va prouver que c'est difficile:

auto c1 = doSomething(s); //do something on s, either succeed or fail
if (c1.valid()) {
   auto c2 = doSomethingElse(s); //do something on s, either succeed or fail
   if (c2.valid()) { 
        ...

Pas très propre.

La carte à Scala le rend un peu plus propre

doSomething(s) //do something on s, either succeed or fail
   .map(_ => doSomethingElse(s) //do something on s, either succeed or fail
   .map(_ => ...)

Ce qui est meilleur, mais encore loin d'être idéal. Ici, la monade Maybe gagne clairement ... mais c'est une autre histoire ..

5
Lorenzo Dematté

Je réfléchis à la même question depuis que j'ai regardé cette vidéo. Et jusqu’à présent, je n’ai trouvé aucun argument convaincant pour avoir attendu, c’est ridicule pour moi et cela va à l’encontre de la clarté et de la propreté. Je viens avec ce qui suit:

  • Attendu est bon car il a soit une valeur, soit des exceptions, nous ne sommes pas obligés d'utiliser try {} catch () pour chaque fonction jetable. Alors utilisez-le pour chaque fonction de projection qui a une valeur de retour
  • Toute fonction qui ne lance pas doit être marquée avec noexcept. Chaque.
  • Toute fonction qui ne retourne rien et qui n'est pas marquée noexcept doit être entourée de try {} catch {}

Si ces déclarations tiennent, nous avons des interfaces auto-documentées, faciles à utiliser, avec un seul inconvénient: nous ne savons pas quelles exceptions peuvent être levées sans jeter un coup d'œil dans les détails de la mise en œuvre.

Attendu imposerait des surcoûts au code car si vous avez une exception dans les entrailles de votre implémentation de classe (par exemple, des méthodes privées très profondes), vous devez le récupérer dans votre méthode d’interface et renvoyer Expected. Bien que je pense que cela soit tout à fait tolérable pour les méthodes qui ont la notion de retourner quelque chose, je pense que cela alourdit les méthodes qui, par définition, n’ont aucune valeur de retour. En outre, pour moi, il est tout à fait contre nature de renvoyer une chose de quelque chose qui n'est pas censé renvoyer quoi que ce soit.

2
ixSci

Il devrait être traité avec les diagnostics du compilateur. De nombreux compilateurs émettent déjà des diagnostics d'avertissement basés sur les utilisations attendues de certaines constructions de bibliothèque standard. Ils devraient émettre un avertissement pour avoir ignoré un expected<void>.

0
Omnifarious