30 votes

Quelle est la méthode la plus efficace pour renvoyer et réinitialiser une variable membre ?

Quelle est la manière la plus efficace de mettre en œuvre GetDeleteObjects ci-dessous ?

class Foo {
public:
  std::vector<Bar> GetDeleteObjects();
private:
  std::vector<Bar> objects_;
}

std::vector<Bar> Foo::GetDeleteObjects() {
  std::vector<Bar> result = objects_;
  objects_.clear();
  return result;
}

Actuellement, c'est au moins la copie de objects_ vers result qui est exécutée. Cela peut-il être accéléré avec std::move par exemple ?

0 votes

C++14 : return std::exchange(objects_, {});

33voto

40two Points 8224

Vous pourriez intervertir les vecteurs :

std::vector<Bar>
Foo::GetDeleteObjects() {
  std::vector<Bar> result;
  result.swap(objects_);
  return result;
}

0 votes

Comme l'a montré @Richard Hodges, cette solution et celle avec std::move sont équivalents en termes de performances, je préfère celui-ci car je le trouve un peu plus intuitif. Il convient toutefois de noter qu'elle réinitialise également la capacité de objects_ à 0 (en utilisant swap avec un vecteur vide est le moyen canonique que je connais pour réinitialiser la capacité), donc si vous pensez que vous allez ajouter beaucoup d'éléments à objects_ Encore une fois, vous pouvez stocker la capacité d'abord et vous assurer que vous n'avez pas besoin d'un autre système de stockage. reserve cet espace avant de revenir.

18voto

Dietmar Kühl Points 70604

Vous pouvez utiliser la construction de mouvement pour les types sensibles au mouvement tels que std::vector<T> :

std::vector<Bar>
Foo::GetDeleteObjects() {
     std::vector<Bar> result(std::move(objects_));
     objects_.clear(); // objects_ left in unspecified state after move
     return result;
}

Le transfert au cours du déplacement des constructions a très probablement déjà réinitialisé les pointeurs et le fichier clear() ne fera rien. Étant donné qu'il n'y a aucune garantie quant à l'état d'un objet déplacé, il est malheureusement nécessaire de clear() .

0 votes

Est-il garanti que cela fonctionne (pour tout objet capable de se déplacer) ? Habituellement, le déplacement est utilisé pour des objets temporaires, qui seront supprimés immédiatement, c'est-à-dire que la seule garantie est que vous pouvez les déstructurer. Dans votre exemple, vous utilisez clear() qui est défini pour std::vector mais pas de manière générale. Je pense donc que votre solution fonctionne pour std::vector mais pas pour types conscients des mouvements en général (contrairement à ce que vous affirmez).

2 votes

@Walter : le déménagement fonctionne en général pour transférer des objets. L'état de l'original dépend de l'implémentation qui peut donner des garanties plus ou moins fortes que ce que fait la bibliothèque C++ standard. Pour les classes de la bibliothèque C++ standard, l'objet déplacé est laissé dans un état valide mais non spécifié, ce qui fait que l'objet déplacé n'est pas transféré. clear() nécessaires. Pour d'autres types d'objets, une approche différente pour restaurer l'objet déplacé dans un état connu peut être nécessaire. Le déplacement est "généralement" utilisé pour les objets temporaires car le compilateur sait implicitement qu'il peut se déplacer à partir de ces objets, alors que dans le cas contraire, il doit être explicite.

0 votes

@Walter : Je ne ferais pas une telle supposition. En fin de compte, je me suis débarrassé de l'idée qu'un downvote errant est significatif. De plus, j'ai déjà reçu le chapeau de Carl Fredricksen, ce qui ne me dérange pas :-)

12voto

Richard Hodges Points 1972

Les trois autres réponses sont correctes et je n'ai donc rien à ajouter ici en termes de réponse à la question, mais puisque l'OP est intéressé par l'efficacité, j'ai compilé toutes les suggestions dans clang avec -O3.

Il n'y a pratiquement rien entre deux des solutions, mais la std::exchange se distingue par la production d'un code plus efficace sur mon compilateur, avec l'avantage supplémentaire qu'il est idiomatiquement parfait.

J'ai trouvé les résultats intéressants :

donné :

std::vector<Bar> Foo::GetDeleteObjects1() {
    std::vector<Bar> tmp;
    tmp.swap(objects_);
    return tmp;
}

se traduit par des résultats :

__ZN3Foo17GetDeleteObjects1Ev:
    .cfi_startproc
    pushq   %rbp
Ltmp0:
    .cfi_def_cfa_offset 16
Ltmp1:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp2:
    .cfi_def_cfa_register %rbp
    movq    $0, 8(%rdi)          ; construct tmp's allocator
    movq    $0, (%rdi)           ;... shame this wasn't optimised away
    movups  (%rsi), %xmm0        ; swap
    movups  %xmm0, (%rdi)
    xorps   %xmm0, %xmm0         ;... but compiler has detected that
    movups  %xmm0, (%rsi)        ;... LHS of swap will always be empty
    movq    16(%rsi), %rax       ;... so redundant fetch of LHS is elided
    movq    %rax, 16(%rdi)
    movq    $0, 16(%rsi)         ;... same here
    movq    %rdi, %rax
    popq    %rbp
    retq

donné :

std::vector<Bar>
Foo::GetDeleteObjects2() {
    std::vector<Bar> tmp = std::move(objects_);
    objects_.clear();
    return tmp;
}

se traduit par des résultats :

__ZN3Foo17GetDeleteObjects2Ev:
    .cfi_startproc
    pushq   %rbp
Ltmp3:
    .cfi_def_cfa_offset 16
Ltmp4:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp5:
    .cfi_def_cfa_register %rbp
    movq    $0, 8(%rdi)         ; move-construct ... shame about these
    movq    $0, (%rdi)          ; ... redundant zero-writes
    movups  (%rsi), %xmm0       ; ... copy right to left ...
    movups  %xmm0, (%rdi)
    movq    16(%rsi), %rax
    movq    %rax, 16(%rdi)
    movq    $0, 16(%rsi)      ; zero out moved-from vector ...
    movq    $0, 8(%rsi)       ; ... happens to be identical to clear()
    movq    $0, (%rsi)        ; ... so clear() is optimised away
    movq    %rdi, %rax    
    popq    %rbp
    retq

enfin, étant donné :

std::vector<Bar>
Foo::GetDeleteObjects3() {
    return std::exchange(objects_, {});
}

Le résultat est très plaisant :

__ZN3Foo17GetDeleteObjects3Ev:
    .cfi_startproc
    pushq   %rbp
Ltmp6:
    .cfi_def_cfa_offset 16
Ltmp7:
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
Ltmp8:
    .cfi_def_cfa_register %rbp
    movq    $0, (%rdi)            ; move-construct the result
    movq    (%rsi), %rax
    movq    %rax, (%rdi)
    movups  8(%rsi), %xmm0
    movups  %xmm0, 8(%rdi)
    movq    $0, 16(%rsi)          ; zero out the source
    movq    $0, 8(%rsi)
    movq    $0, (%rsi)
    movq    %rdi, %rax
    popq    %rbp
    retq

Conclusion :

La méthode std::exchange est à la fois parfaite d'un point de vue idiomatique et efficace d'un point de vue optimal.

9voto

ecatmur Points 64173

L'expression idiomatique serait d'utiliser std::exchange (depuis C++14) :

std::vector<Bar> Foo::GetDeleteObjects() {
  return std::exchange(objects_, {});
}

Notez que cela suppose que l'attribution d'une valeur initialisée à un vector équivaut à appeler clear ; ce sera le cas à moins que vous n'utilisiez des allocateurs avec état avec la fonction propagate_on_container_move_assignment Dans ce cas, il convient de réutiliser explicitement l'allocateur :

std::vector<Bar> Foo::GetDeleteObjects() {
  return std::exchange(objects_, std::vector<Bar>(objects_.get_allocator()));
}

0 votes

Il est intéressant de noter que cette méthode produit également le code le plus efficace sur clang. Je vais mettre à jour ma réponse en conséquence. upvoted.

-1voto

Vincent Pang Points 34

Mise à jour :
Richard a raison. Après avoir jeté un coup d'œil à la définition de std::move Il traite un pointeur au lieu d'une valeur réelle, ce qui est plus intelligent que ce que je pensais. La technique ci-dessous est donc obsolète.

Ancien (obsolète) :
Vous pouvez utiliser des pointeurs

class Foo {
public:
  Foo();
  std::vector<Bar> GetDeleteObjects();

private:
  std::vector<Bar> objects1_;
  std::vector<Bar> objects2_;

  std::vector<Bar> *currentObjects_;
  std::vector<Bar> *deletedObjects_;
}

Foo::Foo() :
  currentObjects_(&objects1_)
  , deletedObjects_(&objects2_)
{
}

Foo::GetDeleteObjects() {
  deletedObjects_->clear();

  std::swap(currentObjects_, deletedObjects_);

  return *deletedObjects;
}

0 votes

Std::move a rendu cette technique obsolète.

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