web-dev-qa-db-fra.com

Propager correctement une variable `decltype (auto)` à partir d'une fonction

(Ceci est un suivi de " Existe-t-il des cas d'utilisation réalistes pour les variables` decltype (auto) `? ")

Considérez le scénario suivant - Je veux passer une fonction f à une autre fonction invoke_log_return Qui:

  1. Appelez f;

  2. Imprimez quelque chose sur stdout;

  3. Renvoie le résultat de f, en évitant les copies/déplacements inutiles et en autorisant l'élimination de la copie.

Notez que si f lance, rien ne doit être imprimé sur stdout. Voici ce que j'ai jusqu'à présent:

template <typename F>
decltype(auto) invoke_log_return(F&& f)
{
    decltype(auto) result{std::forward<F>(f)()};
    std::printf("    ...logging here...\n");

    if constexpr(std::is_reference_v<decltype(result)>)
    {
        return decltype(result)(result);
    }
    else
    {
        return result;
    }
}

Examinons les différentes possibilités:

  • Lorsque f renvoie a prvalue:

    • result sera un objet;

    • invoke_log_return(f) sera un prvalue (éligible pour élision de copie).

  • Lorsque f renvoie un lvalue ou xvalue:

    • result sera une référence;

    • invoke_log_return(f) sera un lvalue ou xvalue.

Vous pouvez voir une application de test ici sur godbolt.org . Comme vous pouvez le voir, g++ Exécute NRVO pour le cas prvalue, contrairement à clang++.

Des questions:

  • Est-ce le moyen le plus court possible de renvoyer "parfaitement" une variable decltype(auto) d'une fonction? Existe-t-il un moyen plus simple d'atteindre ce que je veux?

  • Le modèle if constexpr { ... } else { ... } Peut-il être extrait dans une fonction séparée? La seule façon de l'extraire semble être une macro.

  • Y a-t-il une bonne raison pour laquelle clang++ N'effectue pas NRVO pour le cas prvalue ci-dessus? Devrait il doit être signalé comme une amélioration potentielle, ou l'optimisation NRVO de g++ n'est-elle pas légale ici?


Voici une alternative utilisant un assistant on_scope_success (Comme suggéré par Barry Revzin):

template <typename F>
struct on_scope_success : F
{
    int _uncaught{std::uncaught_exceptions()};

    on_scope_success(F&& f) : F{std::forward<F>(f)} { }

    ~on_scope_success()
    {
        if(_uncaught == std::uncaught_exceptions()) {
            (*this)();
        }
    }
};

template <typename F>
decltype(auto) invoke_log_return_scope(F&& f)
{
    on_scope_success _{[]{ std::printf("    ...logging here...\n"); }};
    return std::forward<F>(f)();
}

Alors que invoke_log_return_scope Est beaucoup plus court, cela nécessite un modèle mental différent du comportement de la fonction et l'implémentation d'une nouvelle abstraction. Étonnamment, g++ Et clang++ Effectuent tous les deux RVO/copy-elision avec cette solution.

exemple en direct sur godbolt.org

Un inconvénient majeur de cette approche, comme mentionné par Ben Voigt , est que la valeur de retour de f ne peut pas faire partie du message de journal.

24
Vittorio Romeo

Nous pouvons utiliser une version modifiée de std::forward: (Le nom en avant est évité pour éviter les problèmes ADL)

template <typename T>
T my_forward(std::remove_reference_t<T>& arg)
{
    return std::forward<T>(arg);
}

Ce modèle de fonction est utilisé pour transmettre une variable decltype(auto). Il peut être utilisé comme ceci:

template <typename F>
decltype(auto) invoke_log_return(F&& f)
{
    decltype(auto) result{std::forward<F>(f)()};
    std::printf("    ...logging here...\n");
    return my_forward<decltype(result)>(result);
}

De cette façon, si std::forward<F>(f)() retourne

  • une valeur, puis result est une non-référence et invoke_log_return renvoie un type non-référence;

  • une lvalue, puis result est une lvalue-reference, et invoke_log_return renvoie un type de référence lvalue;

  • une xvalue, puis result est une rvalue-reference, et invoke_log_return renvoie un type de référence rvalue.

(Essentiellement copié de mon https://stackoverflow.com/a/57440814 )

1
L. F.

C'est la façon la plus simple et la plus claire de l'écrire:

template <typename F>
auto invoke_log_return(F&& f)
{ 
    auto result = f();
    std::printf("    ...logging here... %s\n", result.foo());    
    return result;
}

Le GCC obtient le bon (pas de copies ou mouvements inutiles) résultat attendu:

    s()

in main

prvalue
    s()
    ...logging here... Foo!

lvalue
    s(const s&)
    ...logging here... Foo!

xvalue
    s(s&&)
    ...logging here... Foo!

Donc, si le code est clair, a toujours les mêmes fonctionnalités mais n'est pas optimisé pour fonctionner autant que les concurrents, c'est un échec d'optimisation du compilateur et clang devrait le faire. C'est le genre de problème qui a beaucoup plus de sens résolu dans l'outil plutôt que l'implémentation de la couche application.

https://gcc.godbolt.org/z/50u-hT

0