web-dev-qa-db-fra.com

Pourquoi std :: move empêche RVO?

Dans de nombreux cas, lors du retour d'un local à partir d'une fonction, RVO intervient. Cependant, je pensais que l'utilisation explicite de std::move imposerait au moins le déplacement lorsque RVO ne se produit pas, mais que RVO est toujours appliqué lorsque cela est possible. Cependant, il semble que ce ne soit pas le cas.

#include "iostream"

class HeavyWeight
{
public:
    HeavyWeight()
    {
        std::cout << "ctor" << std::endl;
    }

    HeavyWeight(const HeavyWeight& other)
    {
        std::cout << "copy" << std::endl;
    }

    HeavyWeight(HeavyWeight&& other)
    {
        std::cout << "move" << std::endl;
    }
};

HeavyWeight MakeHeavy()
{
    HeavyWeight heavy;
    return heavy;
}

int main()
{
    auto heavy = MakeHeavy();
    return 0;
}

J'ai testé ce code avec VC++ 11 et GCC 4.71, débogage et libération (-O2) config. Le ctor de copie n'est jamais appelé. Le ctor de déplacement est uniquement appelé par VC++ 11 dans la configuration de débogage. En fait, tout semble bien se passer avec ces compilateurs en particulier, mais à ma connaissance, RVO est facultatif.

Cependant, si j'utilise explicitement move:

HeavyWeight MakeHeavy()
{
    HeavyWeight heavy;
    return std::move(heavy);
}

le mouvement ctor est toujours appelé. Donc, essayer de le rendre "sûr" ne fait qu'empirer les choses.

Mes questions sont:
- Pourquoi std::move empêcher RVO?
- Quand est-il préférable "d'espérer pour le mieux" et de compter sur RVO, et quand devrais-je utiliser explicitement std::move? Ou, en d'autres termes, comment puis-je laisser l'optimisation du compilateur faire son travail et toujours appliquer le mouvement si RVO n'est pas appliqué?

52
cdoubleplusgood

Les cas où la copie et le déplacement d'élision sont autorisés se trouvent dans la section 12.8 §31 de la norme (version N3690):

Lorsque certains critères sont remplis, une implémentation est autorisée à omettre la construction copier/déplacer d'un objet de classe, même si le constructeur sélectionné pour l'opération de copie/déplacement et/ou le destructeur de l'objet ont des effets secondaires. Dans de tels cas, l'implémentation traite la source et la cible de l'opération de copie/déplacement omise comme simplement deux façons différentes de faire référence au même objet, et la destruction de cet objet se produit à la dernière des périodes où les deux objets auraient été détruit sans l'optimisation. Cette élision des opérations de copie/déplacement, appelée copie élision, est autorisée dans les circonstances suivantes (qui peuvent être combinées pour éliminer plusieurs copies):

  • dans une instruction return dans une fonction avec un type de retour de classe, lorsque l'expression est le nom d'un objet automatique non volatile (autre qu'une fonction ou un paramètre catch-clause) avec le même type non qualifié cv que le type de retour de la fonction, l'opération de copie/déplacement peut être omise en construisant l'objet automatique directement dans la valeur de retour de la fonction
  • [...]
  • lorsqu'un objet de classe temporaire qui n'a pas été lié à une référence (12.2) serait copié/déplacé vers un objet de classe avec le même type non qualifié cv, l'opération de copie/déplacement peut être omise en construisant l'objet temporaire directement dans la cible de la copie/du déplacement omis
  • [...]

(Les deux cas que j'ai omis font référence au cas de lancement et de capture d'objets d'exception que je considère moins importants pour l'optimisation.)

Par conséquent, dans une instruction de retour, une élision de copie ne peut se produire que si l'expression est le nom d'une variable locale. Si vous écrivez std::move(var) , alors ce n'est plus le nom d'une variable. Par conséquent, le compilateur ne peut pas éluder le mouvement, s'il doit être conforme à la norme.

Stephan T. Lavavej en a parlé à Going Native 201 et a expliqué exactement votre situation et pourquoi éviter std::move() ici. Commencez à regarder à la minute 38:04. Fondamentalement, lors du retour d'une variable locale du type de retour, elle est généralement traitée comme une valeur r, permettant ainsi le déplacement par défaut.

34
Ralph Tandetzky

comment puis-je laisser l'optimisation du compilateur faire son travail et toujours appliquer le mouvement si RVO n'est pas appliqué?

Comme ça:

HeavyWeight MakeHeavy()
{
    HeavyWeight heavy;
    return heavy;
}

Transformer le retour en mouvement est obligatoire.

16