Comment élider la copie lors du chaînage?

10

Je crée une classe de type chaînage, comme le petit exemple ci-dessous. Il semble que lors du chaînage des fonctions membres, le constructeur de copie soit invoqué. Existe-t-il un moyen de se débarrasser de l'appel du constructeur de copie? Dans mon exemple de jouet ci-dessous, il est évident que je ne traite que des temporaires et donc il "devrait" (peut-être pas selon les normes, mais logiquement) être une élision. Le deuxième meilleur choix, pour copier l'élision, serait d'appeler le constructeur de déplacement, mais ce n'est pas le cas.

class test_class {
    private:
    int i = 5;
    public:
    test_class(int i) : i(i) {}
    test_class(const test_class& t) {
        i = t.i;
        std::cout << "Copy constructor"<< std::endl;
    }
    test_class(test_class&& t) {
        i = t.i;
        std::cout << "Move constructor"<< std::endl;
    }
    auto& increment(){
        i++;
        return *this;
    }
};
int main()
{
    //test_class a{7};
    //does not call copy constructor
    auto b = test_class{7};
    //calls copy constructor
    auto b2 = test_class{7}.increment();
    return 0;
}

Edit: Quelques clarifications. 1. Cela ne dépend pas du niveau d'optimisation. 2. Dans mon vrai code, j'ai des objets plus complexes (par exemple alloués par tas) que des entiers

DDaniel
la source
Quel niveau d'optimisation utilisez-vous pour la compilation?
JVApen
2
auto b = test_class{7};n'appelle pas le constructeur de copie car il est vraiment équivalent à test_class b{7};et les compilateurs sont assez intelligents pour reconnaître ce cas et peuvent donc facilement éluder toute copie. On ne peut pas faire la même chose b2.
Un programmeur dude
Dans l'exemple illustré, il peut ne pas y avoir de différence réelle entre le déplacement et la copie et tout le monde ne le sait pas. Si vous y colliez quelque chose comme un gros vecteur, cela pourrait être différent. Normalement, déplacer n'a de sens que pour les types utilisant des ressources (comme utiliser beaucoup de mémoires de tas, etc.) - est-ce le cas ici?
darune
L'exemple semble artificiel. Avez-vous réellement des E / S ( std::cout) dans votre ctor de copie? Sans cela, la copie devrait être optimisée.
rustyx
@rustyx, supprimez le std :: cout et rendez le constructeur de la copie explicite. Cela démontre que l'élision de copie n'est pas dépendante de std :: cout.
DDaniel

Réponses:

7
  1. Réponse partielle (elle ne se construit pas b2sur place, mais transforme la construction de copie en construction de déplacement): vous pouvez surcharger la incrementfonction membre sur la catégorie de valeur de l'instance associée:

    auto& increment() & {
        i++;
        return *this;
    }
    
    auto&& increment() && {
        i++;
       return std::move(*this);
    }
    

    Ce qui provoque

    auto b2 = test_class{7}.increment();

    déplacer-construire b2car test_class{7}est temporaire et la &&surcharge de test_class::incrementest appelée.

  2. Pour une véritable construction sur place (c'est-à-dire même pas une construction de déplacement), vous pouvez transformer toutes les fonctions membres spéciales et non spéciales en constexprversions. Ensuite, vous pouvez faire

    constexpr auto b2 = test_class{7}.increment();

    et vous n'avez ni un déménagement ni une copie à payer. C'est évidemment possible pour le simple test_class, mais pas pour un scénario plus général qui ne permet pas les constexprfonctions membres.

lubgr
la source
La deuxième alternative rend également b2non modifiable.
Un programmeur du
1
@Someprogrammerdude Bon point. Je suppose que ce constinitserait la façon d'y aller.
lubgr
À quoi servent les esperluettes après le nom de la fonction? Je n'avais jamais vu ça auparavant.
Philip Nelson
1
@PhilipNelson ce sont des qualificatifs de référence et peuvent dicter ce qui thisest identique aux fonctions constmembres
kmdreko
1

Fondamentalement, l'attribution d'une à une nécessite l'invocation d'un constructeur, c'est-à-dire une copie ou un déplacement . Ceci est différent de la où il est connu des deux côtés de la fonction d'être le même objet distinct. Une peut également faire référence à un objet partagé un peu comme un pointeur.


Le moyen le plus simple est probablement de rendre le constructeur de copie entièrement optimisé. Le paramètre de valeur est déjà optimisé par le compilateur, c'est juste celui std::coutqui ne peut pas être optimisé.

test_class(const test_class& t) = default;

(ou supprimez simplement le constructeur de copie et de déplacement)

exemple en direct


Étant donné que votre problème concerne essentiellement la référence, une solution ne renvoie probablement pas de référence à l'objet si vous souhaitez arrêter la copie de cette manière.

  void increment();
};

auto b = test_class{7};//does not call copy constructor
b.increment();//does not call copy constructor

Une troisième méthode repose simplement sur la suppression de la copie en premier lieu - cependant cela nécessite une réécriture ou une encapsulation de l'opération dans une fonction et évitant ainsi complètement le problème (je sais que ce n'est peut-être pas ce que vous voulez, mais pourrait être un solution aux autres utilisateurs):

auto b2 = []{test_class tmp{7}; tmp.increment().increment().increment(); return tmp;}(); //<-- b2 becomes 10 - copy constructor not called

Une quatrième méthode utilise un mouvement à la place, soit explicitement invoqué

auto b2 = std::move(test_class{7}.increment());

ou comme on le voit dans cette réponse .

Darune
la source
@ O'Neil pensait à un autre cas - ty, corrigé
darune