En C ++, est-ce toujours une mauvaise pratique de renvoyer un vecteur à partir d'une fonction?

103

Version courte: il est courant de renvoyer des objets volumineux, tels que des vecteurs / tableaux, dans de nombreux langages de programmation. Ce style est-il maintenant acceptable en C ++ 0x si la classe a un constructeur de mouvement, ou les programmeurs C ++ le considèrent-ils bizarre / moche / abomination?

Version longue: en C ++ 0x est-ce toujours considéré comme une mauvaise forme?

std::vector<std::string> BuildLargeVector();
...
std::vector<std::string> v = BuildLargeVector();

La version traditionnelle ressemblerait à ceci:

void BuildLargeVector(std::vector<std::string>& result);
...
std::vector<std::string> v;
BuildLargeVector(v);

Dans la version la plus récente, la valeur renvoyée par BuildLargeVectorest une rvalue, donc v serait construit en utilisant le constructeur de déplacement de std::vector, en supposant que (N) RVO n'a pas lieu.

Même avant C ++ 0x, la première forme était souvent "efficace" à cause de (N) RVO. Cependant, (N) RVO est à la discrétion du compilateur. Maintenant que nous avons des références rvalue, il est garanti qu'aucune copie profonde n'aura lieu.

Edit : La question n'est vraiment pas une question d'optimisation. Les deux formes présentées ont des performances presque identiques dans les programmes du monde réel. Alors que, dans le passé, la première forme aurait pu avoir des performances plus mauvaises d'un ordre de grandeur. En conséquence, la première forme était pendant longtemps une odeur de code majeure dans la programmation C ++. Plus maintenant, j'espère?

Nate
la source
18
Qui a jamais dit que c'était une mauvaise forme au départ?
Edward Strange
7
C'était certainement une mauvaise odeur de code dans les «vieux jours», d'où je viens. :-)
Nate
1
J'espère bien que oui! J'aimerais voir le passage par valeur devenir plus populaire. :)
sellibitze

Réponses:

73

Dave Abrahams a une analyse assez complète de la vitesse de passage / retour des valeurs .

Réponse courte, si vous devez renvoyer une valeur, renvoyez une valeur. N'utilisez pas de références de sortie car le compilateur le fait quand même. Bien sûr, il y a des mises en garde, vous devriez donc lire cet article.

Peter Alexander
la source
24
"le compilateur le fait quand même": le compilateur n'est pas obligé de faire ça == incertitude == mauvaise idée (besoin de 100% de certitude). "analyse complète" Il y a un énorme problème avec cette analyse - elle repose sur des fonctionnalités de langage non documentées / non standard dans un compilateur inconnu ("Bien que l'élision de copie ne soit jamais requise par le standard"). Donc, même si cela fonctionne, ce n'est pas une bonne idée de l'utiliser - il n'y a absolument aucune garantie qu'il fonctionnera comme prévu, et il n'y a aucune garantie que chaque compilateur fonctionnera toujours de cette façon. S'appuyer sur ce document est une mauvaise pratique de codage, IMO. Même si vous perdez des performances.
SigTerm
5
@SigTerm: C'est un excellent commentaire !!! la plupart des articles référencés sont trop vagues pour même être envisagés pour une utilisation en production. Les gens pensent que tout ce qu'un auteur qui a écrit un livre en profondeur rouge est un évangile et doit être respecté sans aucune réflexion ni analyse supplémentaire. ATM, il n'y a pas de compilateur sur le marché qui fournit une copie aussi variée que les exemples qu'Abrahams utilise dans l'article.
Hippicoder
13
@SigTerm, il y a beaucoup de choses que le compilateur n'est pas obligé de faire, mais vous supposez qu'il le fait quand même. Les compilateurs ne sont pas "obligés" de passer x / 2à x >> 1for ints, mais vous supposez que ce sera le cas. La norme ne dit pas non plus comment les compilateurs sont tenus d'implémenter des références, mais vous supposez qu'elles sont gérées efficacement à l'aide de pointeurs. La norme ne dit pas non plus sur les v-tables, vous ne pouvez donc pas être sûr que les appels de fonction virtuelle sont efficaces non plus. Essentiellement, vous devez parfois faire confiance au compilateur.
Peter Alexander
16
@Sig: Très peu de choses sont réellement garanties, sauf la sortie réelle de votre programme. Si vous voulez être sûr à 100% de ce qui va se passer 100% du temps, alors vous feriez mieux de passer carrément à une autre langue.
Dennis Zickefoose
6
@SigTerm: Je travaille sur un "scénario réel". Je teste ce que fait le compilateur et je travaille avec ça. Il n'y a pas de "peut travailler plus lentement". Cela ne fonctionne tout simplement pas plus lentement car le compilateur implémente RVO, que la norme l'exige ou non. Il n'y a pas de si, de mais ou de peut-être, c'est juste un simple fait.
Peter Alexander
37

Au moins IMO, c'est généralement une mauvaise idée, mais pas pour des raisons d'efficacité. C'est une mauvaise idée car la fonction en question doit généralement être écrite comme un algorithme générique qui produit sa sortie via un itérateur. Presque tout code qui accepte ou renvoie un conteneur au lieu d'opérer sur des itérateurs doit être considéré comme suspect.

Ne vous méprenez pas: il y a des moments où il est logique de faire circuler des objets de type collection (par exemple, des chaînes) mais pour l'exemple cité, je considérerais que passer ou renvoyer le vecteur est une mauvaise idée.

Jerry Coffin
la source
6
Le problème avec l'approche itératrice est qu'elle vous oblige à créer des modèles de fonctions et de méthodes, même lorsque le type d'élément de collection est connu. C'est irritant, et lorsque la méthode en question est virtuelle, impossible. Notez que je ne suis pas en désaccord avec votre réponse en soi, mais en pratique, cela devient un peu lourd en C ++.
jon-hanson
22
Je ne suis pas d’accord. L'utilisation d'itérateurs pour la sortie est parfois appropriée, mais si vous n'écrivez pas un algorithme générique, les solutions génériques fournissent souvent une surcharge inévitable qui est difficile à justifier. À la fois en termes de complexité du code et de performances réelles.
Dennis Zickefoose
1
@Dennis: Je dois dire que mon expérience a été tout le contraire: j'écris un bon nombre de choses en tant que modèles même si je connais à l'avance les types impliqués, car cela est plus simple et améliore les performances.
Jerry Coffin le
9
Je retourne personnellement un conteneur. L'intention est claire, le code est plus simple, je ne me soucie pas beaucoup de la performance quand je l'écris (j'évite juste la pessimisation précoce). Je ne sais pas si l'utilisation d'un itérateur de sortie rendrait mon intention plus claire ... et j'ai autant que possible besoin de code non-modèle, car dans un grand projet, les dépendances tuent le développement.
Matthieu M.
1
@Dennis: Je dirai que conceptuellement, vous ne devriez jamais "construire un conteneur plutôt qu'écrire dans une plage". Un conteneur n'est que cela - un conteneur. Votre préoccupation (et celle de votre code) devrait concerner le contenu, pas le conteneur.
Jerry Coffin le
18

L'essentiel est:

Copy Elision et RVO peuvent éviter les "copies effrayantes" (le compilateur n'est pas obligé de mettre en œuvre ces optimisations, et dans certaines situations, il ne peut pas être appliqué)

Les références C ++ 0x RValue permettent une implémentation de chaîne / vecteur qui garantit cela.

Si vous pouvez abandonner les anciennes implémentations de compilateurs / STL, renvoyez les vecteurs librement (et assurez-vous que vos propres objets le prennent également en charge). Si votre base de code doit prendre en charge des compilateurs «inférieurs», respectez l'ancien style.

Malheureusement, cela a une influence majeure sur vos interfaces. Si C ++ 0x n'est pas une option et que vous avez besoin de garanties, vous pouvez utiliser à la place des objets comptés par référence ou copie à l'écriture dans certains scénarios. Cependant, ils ont des inconvénients avec le multithreading.

(Je souhaite qu'une seule réponse en C ++ soit simple et directe et sans conditions).

Peterchen
la source
11

En effet, depuis C ++ 11, le coût de la copie du std::vectordisparaît dans la plupart des cas.

Cependant, il faut garder à l'esprit que le coût de construction du nouveau vecteur (puis de sa destruction ) existe toujours, et l'utilisation de paramètres de sortie au lieu de renvoyer par valeur est toujours utile lorsque vous souhaitez réutiliser la capacité du vecteur. Ceci est documenté comme une exception dans F.20 des directives de base C ++.

Comparons:

std::vector<int> BuildLargeVector1(size_t vecSize) {
    return std::vector<int>(vecSize, 1);
}

avec:

void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
    v.assign(vecSize, 1);
}

Maintenant, supposons que nous devions appeler ces méthodes numIterfois dans une boucle serrée et effectuer une action. Par exemple, calculons la somme de tous les éléments.

En utilisant BuildLargeVector1, vous feriez:

size_t sum1 = 0;
for (int i = 0; i < numIter; ++i) {
    std::vector<int> v = BuildLargeVector1(vecSize);
    sum1 = std::accumulate(v.begin(), v.end(), sum1);
}

En utilisant BuildLargeVector2, vous feriez:

size_t sum2 = 0;
std::vector<int> v;
for (int i = 0; i < numIter; ++i) {
    BuildLargeVector2(/*out*/ v, vecSize);
    sum2 = std::accumulate(v.begin(), v.end(), sum2);
}

Dans le premier exemple, de nombreuses allocations / désallocations dynamiques inutiles se produisent, qui sont évitées dans le deuxième exemple en utilisant un paramètre de sortie à l'ancienne, en réutilisant la mémoire déjà allouée. La valeur de cette optimisation dépend du coût relatif de l'allocation / désallocation par rapport au coût du calcul / de la mutation des valeurs.

Référence

Jouons avec les valeurs de vecSizeet numIter. Nous garderons vecSize * numIter constant pour que "en théorie", cela prenne le même temps (= il y a le même nombre d'affectations et d'ajouts, avec exactement les mêmes valeurs), et le décalage horaire ne peut provenir que du coût de allocations, désallocations et meilleure utilisation du cache.

Plus précisément, utilisons vecSize * numIter = 2 ^ 31 = 2147483648, car j'ai 16 Go de RAM et ce nombre garantit que pas plus de 8 Go sont alloués (sizeof (int) = 4), en veillant à ne pas échanger sur le disque ( tous les autres programmes étaient fermés, j'avais ~ 15 Go disponibles lors de l'exécution du test).

Voici le code:

#include <chrono>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <vector>

class Timer {
    using clock = std::chrono::steady_clock;
    using seconds = std::chrono::duration<double>;
    clock::time_point t_;

public:
    void tic() { t_ = clock::now(); }
    double toc() const { return seconds(clock::now() - t_).count(); }
};

std::vector<int> BuildLargeVector1(size_t vecSize) {
    return std::vector<int>(vecSize, 1);
}

void BuildLargeVector2(/*out*/ std::vector<int>& v, size_t vecSize) {
    v.assign(vecSize, 1);
}

int main() {
    Timer t;

    size_t vecSize = size_t(1) << 31;
    size_t numIter = 1;

    std::cout << std::setw(10) << "vecSize" << ", "
              << std::setw(10) << "numIter" << ", "
              << std::setw(10) << "time1" << ", "
              << std::setw(10) << "time2" << ", "
              << std::setw(10) << "sum1" << ", "
              << std::setw(10) << "sum2" << "\n";

    while (vecSize > 0) {

        t.tic();
        size_t sum1 = 0;
        {
            for (int i = 0; i < numIter; ++i) {
                std::vector<int> v = BuildLargeVector1(vecSize);
                sum1 = std::accumulate(v.begin(), v.end(), sum1);
            }
        }
        double time1 = t.toc();

        t.tic();
        size_t sum2 = 0;
        {
            std::vector<int> v;
            for (int i = 0; i < numIter; ++i) {
                BuildLargeVector2(/*out*/ v, vecSize);
                sum2 = std::accumulate(v.begin(), v.end(), sum2);
            }
        } // deallocate v
        double time2 = t.toc();

        std::cout << std::setw(10) << vecSize << ", "
                  << std::setw(10) << numIter << ", "
                  << std::setw(10) << std::fixed << time1 << ", "
                  << std::setw(10) << std::fixed << time2 << ", "
                  << std::setw(10) << sum1 << ", "
                  << std::setw(10) << sum2 << "\n";

        vecSize /= 2;
        numIter *= 2;
    }

    return 0;
}

Et voici le résultat:

$ g++ -std=c++11 -O3 main.cpp && ./a.out
   vecSize,    numIter,      time1,      time2,       sum1,       sum2
2147483648,          1,   2.360384,   2.356355, 2147483648, 2147483648
1073741824,          2,   2.365807,   1.732609, 2147483648, 2147483648
 536870912,          4,   2.373231,   1.420104, 2147483648, 2147483648
 268435456,          8,   2.383480,   1.261789, 2147483648, 2147483648
 134217728,         16,   2.395904,   1.179340, 2147483648, 2147483648
  67108864,         32,   2.408513,   1.131662, 2147483648, 2147483648
  33554432,         64,   2.416114,   1.097719, 2147483648, 2147483648
  16777216,        128,   2.431061,   1.060238, 2147483648, 2147483648
   8388608,        256,   2.448200,   0.998743, 2147483648, 2147483648
   4194304,        512,   0.884540,   0.875196, 2147483648, 2147483648
   2097152,       1024,   0.712911,   0.716124, 2147483648, 2147483648
   1048576,       2048,   0.552157,   0.603028, 2147483648, 2147483648
    524288,       4096,   0.549749,   0.602881, 2147483648, 2147483648
    262144,       8192,   0.547767,   0.604248, 2147483648, 2147483648
    131072,      16384,   0.537548,   0.603802, 2147483648, 2147483648
     65536,      32768,   0.524037,   0.600768, 2147483648, 2147483648
     32768,      65536,   0.526727,   0.598521, 2147483648, 2147483648
     16384,     131072,   0.515227,   0.599254, 2147483648, 2147483648
      8192,     262144,   0.540541,   0.600642, 2147483648, 2147483648
      4096,     524288,   0.495638,   0.603396, 2147483648, 2147483648
      2048,    1048576,   0.512905,   0.609594, 2147483648, 2147483648
      1024,    2097152,   0.548257,   0.622393, 2147483648, 2147483648
       512,    4194304,   0.616906,   0.647442, 2147483648, 2147483648
       256,    8388608,   0.571628,   0.629563, 2147483648, 2147483648
       128,   16777216,   0.846666,   0.657051, 2147483648, 2147483648
        64,   33554432,   0.853286,   0.724897, 2147483648, 2147483648
        32,   67108864,   1.232520,   0.851337, 2147483648, 2147483648
        16,  134217728,   1.982755,   1.079628, 2147483648, 2147483648
         8,  268435456,   3.483588,   1.673199, 2147483648, 2147483648
         4,  536870912,   5.724022,   2.150334, 2147483648, 2147483648
         2, 1073741824,  10.285453,   3.583777, 2147483648, 2147483648
         1, 2147483648,  20.552860,   6.214054, 2147483648, 2147483648

Résultats de référence

(Intel i7-7700K à 4,20 GHz; 16 Go de DDR4 à 2400 MHz; Kubuntu 18.04)

Notation: mem (v) = v.size () * sizeof (int) = v.size () * 4 sur ma plateforme.

Sans surprise, lorsque numIter = 1(c'est-à-dire, mem (v) = 8 Go), les heures sont parfaitement identiques. En effet, dans les deux cas, nous n'allouons qu'une seule fois un énorme vecteur de 8 Go en mémoire. Cela prouve également qu'aucune copie ne s'est produite lors de l'utilisation de BuildLargeVector1 (): je n'aurais pas assez de RAM pour faire la copie!

Lorsque numIter = 2, réutiliser la capacité vectorielle au lieu de réallouer un deuxième vecteur est 1,37x plus rapide.

Quand numIter = 256, réutiliser la capacité vectorielle (au lieu d'allouer / désallouer un vecteur encore et encore 256 fois ...) est 2,45x plus rapide :)

Nous pouvons remarquer que time1 est à peu près constant de numIter = 1à numIter = 256, ce qui signifie qu'allouer un énorme vecteur de 8 Go est à peu près aussi coûteux que d'allouer 256 vecteurs de 32 Mo. Cependant, allouer un vecteur énorme de 8 Go est certainement plus coûteux que d'allouer un vecteur de 32 Mo, donc la réutilisation de la capacité du vecteur offre des gains de performances.

De numIter = 512(mem (v) = 16MB) à numIter = 8M(mem (v) = 1kB) est le point idéal: les deux méthodes sont exactement aussi rapides et plus rapides que toutes les autres combinaisons de numIter et vecSize. Cela a probablement à voir avec le fait que la taille du cache L3 de mon processeur est de 8 Mo, de sorte que le vecteur tient à peu près complètement dans le cache. Je n'explique pas vraiment pourquoi le saut soudain de time1est pour mem (v) = 16 Mo, il semblerait plus logique de se produire juste après, quand mem (v) = 8 Mo. Notez que étonnamment, dans ce sweet spot, ne pas réutiliser la capacité est en fait légèrement plus rapide! Je n'explique pas vraiment cela.

Quand les numIter > 8Mchoses commencent à devenir moche. Les deux méthodes sont plus lentes, mais le retour du vecteur par valeur est encore plus lent. Dans le pire des cas, avec un vecteur ne contenant qu'une seule intcapacité, la réutilisation de la capacité au lieu de renvoyer par valeur est 3,3 fois plus rapide. Cela est probablement dû aux coûts fixes de malloc () qui commencent à dominer.

Notez que la courbe pour le temps2 est plus douce que la courbe pour le temps1: non seulement la réutilisation de la capacité vectorielle est généralement plus rapide, mais peut-être plus important encore, elle est plus prévisible .

Notez également que dans le sweet spot, nous avons pu effectuer 2 milliards d'ajouts d'entiers 64 bits en ~ 0,5 s, ce qui est tout à fait optimal sur un processeur 4,2 GHz 64 bits. Nous pourrions faire mieux en parallélisant le calcul afin d'utiliser les 8 cœurs (le test ci-dessus n'utilise qu'un seul cœur à la fois, ce que j'ai vérifié en relançant le test tout en surveillant l'utilisation du processeur). Les meilleures performances sont obtenues lorsque mem (v) = 16 ko, qui est l'ordre de grandeur du cache L1 (le cache de données L1 pour le i7-7700K est 4x32 ko).

Bien sûr, les différences deviennent de moins en moins pertinentes au fur et à mesure que vous devez effectuer des calculs sur les données. Voici les résultats si nous remplaçons sum = std::accumulate(v.begin(), v.end(), sum);par for (int k : v) sum += std::sqrt(2.0*k);:

Benchmark 2

Conclusions

  1. L'utilisation de paramètres de sortie au lieu de renvoyer par valeur peut fournir des gains de performances en réutilisant la capacité.
  2. Sur un ordinateur de bureau moderne, cela ne semble applicable qu'aux grands vecteurs (> 16 Mo) et aux petits vecteurs (<1 Ko).
  3. Évitez d'allouer des millions / milliards de petits vecteurs (<1 Ko). Si possible, réutilisez la capacité ou, mieux encore, concevez votre architecture différemment.

Les résultats peuvent différer sur d'autres plates-formes. Comme d'habitude, si les performances comptent, écrivez des benchmarks pour votre cas d'utilisation spécifique.

Boris Dalstein
la source
6

Je pense toujours que c'est une mauvaise pratique, mais il convient de noter que mon équipe utilise MSVC 2008 et GCC 4.1, donc nous n'utilisons pas les derniers compilateurs.

Auparavant, la plupart des hotspots affichés dans vtune avec MSVC 2008 se résumaient à la copie de chaînes. Nous avions un code comme celui-ci:

String Something::id() const
{
    return valid() ? m_id: "";
}

... notez que nous avons utilisé notre propre type String (cela était nécessaire car nous fournissons un kit de développement logiciel dans lequel les auteurs de plugins pourraient utiliser différents compilateurs et donc des implémentations différentes et incompatibles de std :: string / std :: wstring).

J'ai fait un simple changement en réponse à la session de profilage d'échantillonnage de graphe d'appel montrant que String :: String (const String &) prend beaucoup de temps. Les méthodes comme dans l'exemple ci-dessus ont été les plus grands contributeurs (en fait, la session de profilage a montré que l'allocation de mémoire et la désallocation étaient l'un des plus gros hotspots, le constructeur de copie String étant le principal contributeur pour les allocations).

Le changement que j'ai effectué était simple:

static String null_string;
const String& Something::id() const
{
    return valid() ? m_id: null_string;
}

Pourtant, cela a fait toute la différence! Le hotspot a disparu lors des sessions suivantes du profileur, et en plus de cela, nous effectuons de nombreux tests unitaires approfondis pour suivre les performances de nos applications. Toutes sortes de temps de test de performance ont chuté de manière significative après ces simples changements.

Conclusion: nous n'utilisons pas les derniers compilateurs absolus, mais nous ne pouvons toujours pas sembler dépendre du compilateur qui optimise la copie pour renvoyer par valeur de manière fiable (du moins pas dans tous les cas). Ce n'est peut-être pas le cas pour ceux qui utilisent des compilateurs plus récents comme MSVC 2010. J'attends avec impatience le moment où nous pourrons utiliser C ++ 0x et utiliser simplement des références rvalue et ne jamais avoir à s'inquiéter du fait que nous pessimisons notre code en retournant des complexes classes par valeur.

[Edit] Comme Nate l'a souligné, RVO s'applique au retour des temporaires créés à l'intérieur d'une fonction. Dans mon cas, il n'y avait pas de tels temporaires (sauf pour la branche invalide où nous construisons une chaîne vide) et donc RVO n'aurait pas été applicable.

puant472
la source
3
C'est le problème: RVO dépend du compilateur, mais un compilateur C ++ 0x doit utiliser la sémantique de déplacement s'il décide de ne pas utiliser RVO (en supposant qu'il existe un constructeur de déplacement). L'utilisation de l'opérateur trigraphe annule RVO. Voir cpp-next.com/archive/2009/09/move-it-with-rvalue-references auquel Peter a fait référence. Mais votre exemple n'est pas éligible pour la sémantique de déplacement de toute façon car vous ne renvoyez pas un fichier temporaire.
Nate
@ Stinky472: renvoyer un membre par valeur allait toujours être plus lent que la référence. Les références Rvalue seraient toujours plus lentes que de renvoyer une référence au membre d'origine (si l'appelant peut prendre une référence au lieu d'avoir besoin d'une copie). De plus, il y a encore plusieurs fois que vous pouvez enregistrer, sur des références rvalue, car vous avez du contexte. Par exemple, vous pouvez faire String newstring; newstring.resize (string1.size () + string2.size () + ...); nouvelle chaîne + = chaîne1; nouvelle chaîne + = chaîne2; etc. C'est encore une économie substantielle sur les valeurs.
Puppy
@DeadMG une économie substantielle par rapport à l'opérateur binaire + même avec les compilateurs C ++ 0x implémentant RVO? Si c'est le cas, c'est dommage. Là encore, cela a du sens puisque nous finissons par devoir créer un temporaire pour calculer la chaîne concaténée alors que + = peut concaténer directement à newstring.
stinky472
Que diriez-vous d'un cas comme: string newstr = str1 + str2; Sur un compilateur implémentant la sémantique de déplacement, il semble que cela devrait être aussi rapide ou même plus rapide que: string newstr; newstr + = str1; newstr + = str2; Pas de réserve, pour ainsi dire (je suppose que vous vouliez dire réserver au lieu de redimensionner).
stinky472
5
@Nate: Je pense que vous confondez les trigraphes comme <::ou ??!avec l' opérateur conditionnel ?: (parfois appelé l' opérateur ternaire ).
fredoverflow
3

Juste pour pinailler un peu: il n'est pas courant dans de nombreux langages de programmation de renvoyer des tableaux à partir de fonctions. Dans la plupart d'entre eux, une référence au tableau est renvoyée. En C ++, l'analogie la plus proche serait de retournerboost::shared_array

Nemanja Trifunovic
la source
4
@Billy: std :: vector est un type valeur avec une sémantique de copie. La norme C ++ actuelle n'offre aucune garantie que (N) RVO sera jamais appliqué, et dans la pratique, il existe de nombreux scénarios réels dans lesquels ce n'est pas le cas.
Nemanja Trifunovic
3
@Billy: Encore une fois, il existe des scénarios très réels où même les derniers compilateurs n'appliquent pas NRVO: efnetcpp.org/wiki/Return_value_optimization#Named_RVO
Nemanja Trifunovic
3
@Billy ONeal: 99% ne suffit pas, il vous faut 100%. La loi de Murphy - "si quelque chose peut mal tourner, ce sera le cas". L'incertitude est bonne si vous avez affaire à une sorte de logique floue, mais ce n'est pas une bonne idée pour écrire un logiciel traditionnel. S'il y a même 1% de possibilité que le code ne fonctionne pas comme vous le pensez, alors vous devriez vous attendre à ce que ce code introduise un bogue critique qui vous fera virer. De plus, ce n'est pas une fonctionnalité standard. Utiliser des fonctionnalités non documentées est une mauvaise idée - si dans un an à compter de la date de connaissance, le compilateur abandonnera la fonctionnalité (ce n'est pas requis par la norme, non?), Vous serez en difficulté.
SigTerm
4
@SigTerm: Si nous parlions d'exactitude du comportement, je serais d'accord avec vous. Cependant, nous parlons d'une optimisation des performances. De telles choses sont bien avec moins de 100% de certitude.
Billy ONeal
2
@Nemanja: Je ne vois pas sur quoi on "s'appuie" ici. Votre application fonctionne de la même manière, peu importe si RVO ou NRVO est utilisé. S'ils sont utilisés, cela fonctionnera plus rapidement. Si votre application est trop lente sur une plate-forme particulière et que vous l'avez tracée pour renvoyer la valeur de copie, alors modifiez-la, mais cela ne change pas le fait que la meilleure pratique consiste toujours à utiliser la valeur de retour. Si vous devez absolument vous assurer qu'aucune copie n'a lieu, enveloppez le vecteur dans un shared_ptret appelez-le un jour.
Billy ONeal
2

Si les performances sont un réel problème, vous devez comprendre que la sémantique de déplacement n'est pas toujours plus rapide que la copie. Par exemple, si vous avez une chaîne qui utilise l' optimisation des petites chaînes, alors pour les petites chaînes, un constructeur de déplacement doit effectuer exactement la même quantité de travail qu'un constructeur de copie ordinaire.

Motti
la source
1
NRVO ne disparaît pas simplement parce que des constructeurs de mouvements ont été ajoutés.
Billy ONeal
1
@Billy, vrai mais non pertinent, la question était de savoir si C ++ 0x a changé les meilleures pratiques et NRVO n'a pas changé en raison de C ++ 0x
Motti