34 votes

Gestion de l'affectation des vides dans la programmation générique C++

J'ai un code C++ qui enveloppe un lambda arbitraire et renvoie le résultat du lambda.

template <typename F>
auto wrapAndRun(F fn) -> decltype(F()) {
    // foo();
    auto result = fn();
    // bar();
    return result;
}

Cela fonctionne à moins que F renvoie à void ( error: variable has incomplete type 'void' ). J'ai pensé à utiliser un ScopeGuard à exécuter bar mais je ne veux pas bar à exécuter si fn lancés. Des idées ?

P.S. J'ai découvert plus tard qu'il y a une proposition visant à corriger cette incohérence .

0 votes

Quelle version de C++, 14 ou 17 ?

0 votes

Il s'agit d'un projet personnel, donc même c++2x convient.

2 votes

J'ai eu le même problème auparavant, la même solution s'applique. stackoverflow.com/questions/24468397/ Il ne s'agit que de C++11, qui pourrait probablement être modernisé.

23voto

Nir Friedman Points 3165

Vous pouvez écrire une classe wrapper simple qui gère cette partie :

template <class T>
struct CallAndStore {
    template <class F>
    CallAndStore(F f) : t(f()) {}
    T t;
    T get() { return std::forward<T>(t); }
};

Et se spécialiser :

template <>
struct CallAndStore<void> {
    template <class F>
    CallAndStore(F f) { f(); }
    void get() {}
};

Vous pouvez améliorer la convivialité avec une petite fonction d'usine :

template <typename F>
auto makeCallAndStore(F&& f) -> CallAndStore<decltype(std::declval<F>()())> {
    return {std::forward<F>(f)};
}

Alors, utilisez-la.

template <typename F>
auto wrapAndRun(F fn) {
    // foo();
    auto&& result = makeCallAndStore(std::move(fn));
    // bar();
    return result.get();
}

Edit : avec le std::forward moulage à l'intérieur get Cette méthode semble également gérer correctement le retour d'une référence à partir d'une fonction.

0 votes

J'aime mieux votre solution mais y a-t-il des limitations sur le type de retour basé sur le fait qu'il doit être stocké dans votre cas ?

0 votes

Désolé, quelle est exactement la limitation ? Il faut juste qu'il soit mobile (laissez-moi ajouter un std::move).

1 votes

Peut-être pas des limitations mais des différences, par exemple je ne suis pas sûr que le destructeur de T peut être évité dans votre cas car le RVO ne peut être appliqué

15voto

Mário Feroldi Points 1918

Le nouveau C++17 if constexpr peut être utile ici. Vous pouvez choisir de renvoyer fn() au moment de la compilation :

#include <type_traits>

template <typename F>
auto wrapAndRun(F fn) -> decltype(fn())
{
    if constexpr (std::is_same_v<decltype(fn()), void>)
    {
        foo();
        fn();
        bar();
    }
    else
    {
        foo();
        auto result = fn();
        bar();
        return result;
    }
}

Comme vous l'avez dit, C++2a est également une option, vous pourriez aussi utiliser des concepts, en mettant une contrainte sur la fonction :

template <typename F>
  requires requires (F fn) { { fn() } -> void }
void wrapAndRun(F fn)
{
    foo();
    fn();
    bar();
}

template <typename F>
decltype(auto) wrapAndRun(F fn)
{
    foo();
    auto result = fn();
    bar();
    return result;
}

1 votes

Cela peut-il être fait sans répéter les appels à foo y bar ?

1 votes

@NirFriedman Oui, mais je pense que le compromis n'est pas bon. Après tout, ce code ne souffre pas de la répétition, et il est assez simple.

1 votes

Désolé, quel est le compromis exactement ? En outre, on suppose qu'il y a littéralement deux appels de fonction. Dans le cas de l'OP ou dans le cas général, il peut y avoir plus de code, que l'utilisateur peut ou non avoir envie de factoriser dans une fonction.

6voto

Kilian Points 125

Vous pouvez jeter un coup d'oeil au ScopeGuard d'Alexandrescu : ScopeGuard.h Il exécute le code uniquement lorsqu'il n'y a pas eu d'exception.

template<class Fn>
decltype(auto) wrapAndRun(Fn&& f) {
  foo();
  SCOPE_SUCCESS{ bar(); }; //Only executed at scope exit when there are no exceptions.
  return std::forward<Fn>(f)();
}

Donc en cas d'absence d'erreur, l'ordre d'exécution est : 1. foo(), 2. f(), 3. bar(). Et en cas d'exception, l'ordre d'exécution est : 1. foo(), 2. f().

1 votes

C'est intéressant. Je n'avais pas réalisé qu'il était possible d'écrire uncaught_exceptions (remarquez le s) avant 17 dans l'espace utilisateur ; c'est une condition préalable pour des choses comme SCOPE_SUCCESS. Bonne réponse !

4voto

Massimiliano Janes Points 5021

Une autre astuce pourrait être d'exploiter l'opérateur virgule, quelque chose comme :

struct or_void {};

template<typename T>
T&& operator,( T&& x, or_void ){ return std::forward<T>(x); }

template <typename F>
auto wrapAndRun(F fn) -> decltype(fn()) {
    // foo();
    auto result = ( fn(), or_void() );
    // bar();
    return decltype(fn())(result);
}

8 votes

@MassimilianoJanes Surcharger l'opérateur virgule est une de ces choses qu'une bonne partie de la communauté C++ (moi y compris) vous dira de ne jamais faire. Oui, j'ai compris l'idée, mais c'est à 100% le genre de surcharge de magie noire que tant, tant de personnes et de guides de style ont recommandé de ne pas faire. S'il n'y avait vraiment, vraiment pas d'autre moyen, cela pourrait valoir la peine d'être considéré, mais il y a déjà 4 solutions ici qui sont toutes préférables.

5 votes

@NirFriedman respectueusement, je ne suis pas d'accord ; d'abord, l'opérateur virgule es utilisées dans des bibliothèques existantes largement utilisées (boost, Eigen pour citer les premières qui me viennent à l'esprit), donc prétendre que tout le monde dire à jamais l'utiliser, est tout simplement faux. Deuxièmement, les raisons pour lesquelles les gens devraient (à juste titre) éviter de surcharger l'opérateur virgule ne s'appliquent pas ici, vous pouvez facilement rendre cela sûr.

5 votes

Troisièmement, nous ne connaissons pas l'objectif final et la portée du code du PO ; il pourrait simplement avoir besoin d'une solution technique à un problème, enfouie dans les détails de mise en œuvre. Cette réponse est une solution valide à la question du PO, point final. A mon avis, la déclasser juste parce qu'elle vous semble mauvaise va à l'encontre du sens du vote.

4voto

marcin_j Points 12237

Solution avec SFINAE, l'idée est de faire en sorte qu'une fonction interne void renvoie effectivement int - avec un peu de chance, le compilateur optimisera cela. En extérieur wrapAndRun retournera le même type que la fonction enveloppée.

http://coliru.stacked-crooked.com/a/e84ff8f74b3b6051

#include <iostream>

template <typename F>
auto wrapAndRun1(F fn) -> std::enable_if_t<!std::is_same_v<std::result_of_t<F()>, void>, std::result_of_t<F()>> {
    return fn();
}

template <typename F>
auto wrapAndRun1(F fn) -> std::enable_if_t<std::is_same_v<std::result_of_t<F()>, void>, int> {
    fn();
    return 0;
}

template <typename F>
auto wrapAndRun(F fn) -> std::result_of_t<F()> {
    // foo();
    [[maybe_unused]] auto result = wrapAndRun1(fn);    
    // bar();        
    if constexpr (std::is_void_v<std::result_of_t<F()>>)
        return;
    else
        return result;
}

int main()
{
    wrapAndRun([] { std::cout << "with return \n"; return 0; });
    wrapAndRun([] { std::cout << "no return \n"; });
}

0 votes

Je pense que l'OP veut courir bar() dans l'affaire fn() le type de retour est void

0 votes

Je ne sais pas pourquoi la partie supérieure de enable_if semble avoir une certaine répétition ? Dans tous les cas, je pense que la réponse gagnerait à écrire un fichier séparé de type returns_void trait. Cependant, l'inconvénient majeur ici est que vous devez répéter foo y bar à deux endroits (et d'ailleurs l'un d'eux était déjà oublié).

1 votes

La répétition du code est hideuse et je ne pense pas que ce soit une bonne solution si elle peut être évitée.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X