web-dev-qa-db-fra.com

Comment appeler explicitement une méthode générant des exceptions en C++?

J'ai un cours simple:

class A {
 public:
  bool f(int* status = nullptr) noexcept {
    if (status) *status = 1;
    return true;
  }
  void f() {
    throw std::make_pair<int, bool>(1, true);
  }
};

int main() {
  A a;
  a.f(); // <- Ambiguity is here! I want to call 'void f()'
}

Je veux résoudre l'ambiguïté d'un appel de méthode en faveur de la méthode de lancement d'exception par n'importe quel moyen.

La raison d'être de cette interface:

  • Pour avoir les interfaces noexcept(true) et noexcept(false),
  • Pour permettre éventuellement d’obtenir des informations supplémentaires via un pointeur dans la variante noexcept(false) - alors que la variante noexcept(true) intègre toujours ces informations dans une exception.

Est-ce possible? Les suggestions pour une meilleure interface sont également les bienvenues.

22
abyss.7

Avoir des fonctions avec ce type de signatures est évidemment une mauvaise conception, comme vous l'avez découvert. Les vraies solutions sont d’avoir des noms différents pour eux ou de perdre l’argument par défaut et ont déjà été présentées dans d’autres réponses.

Cependant, si vous êtes coincé avec une interface que vous ne pouvez pas modifier ou juste pour le plaisir, voici comment vous pouvez appeler explicitement void f():

L'astuce consiste à utiliser le casting du pointeur de la fonction pour résoudre l'ambiguïté:

a.f(); // <- ambiguity is here! I want to call 'void f()'

(a.*(static_cast<void (A::*)()>(&A::f)))(); // yep... that's the syntax... yeah...

Ok, donc ça marche, mais n'écris jamais un code comme ça!

Il existe des moyens de le rendre plus lisible.

Utilisez un pointeur:

// create a method pointer:
auto f_void = static_cast<void (A::*)()>(&A::f);

// the call is much much better, but still not as simple as `a.f()`
(a.*f_void)();

Créer un lambda ou une fonction libre

auto f_void = [] (A& a)
{
    auto f_void = static_cast<void (A::*)()>(&A::f);
    (a.*f_void)();
};

// or

void f_void(A& a)
{
    auto f_void = static_cast<void (A::*)()>(&A::f);
    (a.*f_void)();
};


f_void(a);

Je ne sais pas si c'est nécessaire mieux. La syntaxe d’appel est certes plus simple, mais elle risque d’être source de confusion car nous passons d’une syntaxe d’appel de méthode à une syntaxe d’appel de fonction libre.

42
bolov

Les deux versions f ont des significations différentes. 

Ils devraient avoir deux noms différents, comme:

  • f pour celui qui lance, parce que l'utiliser signifie que vous avez confiance en la réussite et que l'échec serait une exception dans le programme.
  • try_f() ou tryF() pour la réponse basée sur le retour d'erreur, car son utilisation signifie que l'échec de l'appel est un résultat attendu.

Deux conceptions différentes doivent être reflétées dans la conception avec deux noms différents.

28
Guillaume Fouillet

Parce que cela me semble fondamentalement évident, il se peut que je manque quelque chose ou que je ne comprenne pas bien votre question. Cependant, je pense que cela fait exactement ce que vous voulez:

#include <utility>

class A {
 public:
  bool f(int* status) noexcept {
    if (status) *status = 1;
    return true;
  }
  void f() {
    throw std::make_pair<int, bool>(1, true);
  }
};

int main() {
  A a;
  a.f(); // <- now calls 'void f()'
  a.f(nullptr);  // calls 'bool f(int *)'
}

J'ai simplement supprimé l'argument par défaut de la variante noexcept. Il est encore possible d'appeler la variante noexcept en passant nullptr en tant qu'argument, ce qui semble être un excellent moyen d'indiquer que vous souhaitez appeler cette variante particulière de la fonction. Après tout, il va falloir que some syntactic marqueur indiquant quelle variante vous souhaitez appeler!

15
davmac

Je suis d'accord avec les suggestions d'autres utilisateurs pour simplement supprimer l'argument par défaut.

Un argument de poids en faveur d’une telle conception est qu’elle correspondrait à la nouvelle bibliothèque de systèmes de fichiers C++ 17, dont les fonctions offrent généralement aux appelants le choix entre des exceptions et des paramètres de référence d’erreur.

Voir par exemple std::filesystem::file_size , qui a deux surcharges, l'une d'elles étant noexcept:

std::uintmax_t file_size( const std::filesystem::path& p );

std::uintmax_t file_size( const std::filesystem::path& p,
                          std::error_code& ec ) noexcept;

L'idée derrière cette conception (qui provient de Boost.Filesystem) est presque identique à la vôtre, à l'exception de l'argument par défaut. Supprimez-le et faites-le comme un composant flambant neuf de la bibliothèque standard, dont le design ne peut évidemment pas être complètement endommagé.

5
Christian Hackl

C++ a déjà un type spécifiquement utilisé comme argument pour lever la ambiguïté entre les variantes d'une fonction lancée et non lancée: std::nothrow_t. Vous pouvez l'utiliser.

#include <new>

class A {
 public:
  bool f(std::nothrow_t, int* status = nullptr) noexcept {
    if (status) *status = 1;
    return true;
  }
  void f() {
    throw std::make_pair<int, bool>(1, true);
  }
};

int main() {
  A a;
  a.f(); // Calls 'void f()'
  a.f(std::nothrow); // Calls 'void f(std::nothrow_t, int*)'
}

Bien que je préférerais toujours une interface où le nom distingue les variantes, ou éventuellement une distinction où la distinction n'est pas nécessaire.

2
Sebastian Redl

En C++ 14, il est ambigu, car noexcept fait pas une partie de la signature de la fonction. Ceci étant dit...

Vous avez une interface très étrange. Bien que f(int* status = nullptr) soit étiqueté noexcept, étant donné qu'il a un jumeau qui ne lève une exception, vous n'accordez pas réellement à l'appelant une garantie d'exception logique. Il semble que vous souhaitiez simultanément f pour toujours réussir tout en levant une exception si la condition préalable n'est pas remplie (status a une valeur valide, c'est-à-dire pas nullptr). Mais si f jette, dans quel état se trouve l'objet? Vous voyez, votre code est très difficile à raisonner.

Je vous recommande de regarder std::optional à la place. Cela indiquera au lecteur ce que vous essayez réellement de faire.

2
user9183966

Voici une méthode purement compilée .

Cela peut être utile si votre compilateur a des difficultés à optimiser les appels de pointeur de fonction.

#include <utility>

class A {
 public:
  bool f(int* status = nullptr) noexcept {
    if (status) *status = 1;
    return true;
  }
  void f() {
    throw std::make_pair<int, bool>(1, true);
  }
};

template<void (A::*F)()>
struct NullaryFunction {
  static void invoke(A &obj) {
    return (obj.*F)();
  }
};

int main() {
  A a;
  // a.f(); // <- Ambiguity is here! I want to call 'void f()'
  NullaryFunction<&A::f>::invoke(a);
}
0
Mehrdad