Wanneer is een const-referentie beter dan pass-by-waarde in C++11?

Ik heb wat pre-C++11 code waarin ik constverwijzingen gebruik om grote parameters zoals vector‘s veel door te geven. Een voorbeeld is als volgt:

int hd(const vector<int>& a) {
   return a[0];
}

Ik heb gehoord dat je met nieuwe C++11-functies de vectorals volgt op waarde kunt doorgeven zonder prestatiehits.

int hd(vector<int> a) {
   return a[0];
}

Bijvoorbeeld dit antwoordzegt

De verplaatsingssemantiek van C++11 maakt het doorgeven en retourneren op waarde veel aantrekkelijker, zelfs voor complexe objecten.

Is het waar dat de bovenstaande twee opties qua prestaties hetzelfde zijn?

Zo ja, wanneer is het gebruik van const-referentie zoals in optie 1 beter dan optie 2? (d.w.z. waarom moeten we nog steeds const-referenties gebruiken in C++11).

Een reden waarom ik het vraag is dat const-referenties de afleiding van sjabloonparameters bemoeilijken, en het zou een stuk eenvoudiger zijn om alleen pass-by-waarde te gebruiken, als het hetzelfde is met const-referentie qua prestatie.


Antwoord 1, autoriteit 100%

De algemene vuistregel voor het doorgeven van waarde is wanneer u toch een kopiezou maken. Dat wil zeggen dat in plaats van dit te doen:

void f(const std::vector<int>& x) {
    std::vector<int> y(x);
    // stuff
}

waar je eerst een const-ref doorgeeft en dankopieert, moet je dit in plaats daarvan doen:

void f(std::vector<int> x) {
    // work with x instead
}

Dit was gedeeltelijk waar in C++03, en is nuttiger geworden met verplaatsingssemantiek, aangezien de kopie kanworden vervangen door een verplaatsing in het geval van pass-by-val wanneer de functie wordt aangeroepen met een rwaarde.

Anders, als je alleen maar de gegevens wilt lezen, is het doorgeven van de const-referentie nog steeds de beste, efficiënte manier.


Antwoord 2, autoriteit 15%

Er is een groot verschil. U krijgt een kopie van de interne array van een vector, tenzij deze op het punt stond dood te gaan.

int hd(vector<int> a) {
   //...
}
hd(func_returning_vector()); // internal array is "stolen" (move constructor is called)
vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8};
hd(v); // internal array is copied (copy constructor is called)

C++11 en de introductie van rvalu-referenties veranderden de regels over het retourneren van objecten zoals vectoren – nu kunt u dat doen(zonder u zorgen te maken over een gegarandeerde kopie). Er zijn echter geen basisregels om ze als argument te aannemenals argument – je moet ze nog steeds op basis van referentie nemen, tenzij je echt een echte kopie nodig hebt – dan op waarde nemen.


Antwoord 3, autoriteit 12%

De verplaatsingssemantiek van C++11 maakt doorgeven en teruggeven op waarde veel aantrekkelijker, zelfs voor complexe objecten.

Het voorbeeld dat u geeft, is echter een voorbeeld van een pass-by-waarde

int hd(vector<int> a) {

Dus C++11 heeft hier geen invloed op.

Zelfs als u correct ‘hd’ had gedeclareerd om een ​​rwaarde te nemen

int hd(vector<int>&& a) {

het kan goedkoper zijn dan pass-by-value, maar het uitvoeren van een succesvolle zet(in tegenstelling tot een simpele std::movedie helemaal geen effect kan hebben) kan duurder zijn dan een eenvoudige pass-by-referentie. Er moet een nieuwe vector<int>worden geconstrueerd en deze moet eigenaar worden van de inhoud van a. We hebben niet de oude overhead van het moeten toewijzen van een nieuwe reeks elementen en het kopiëren van de waarden, maar we moeten nog steeds de gegevensvelden van vectoroverdragen.

Belangrijker nog, in het geval van een succesvolle verhuizing, zou atijdens dit proces worden vernietigd:

std::vector<int> x;
x.push(1);
int n = hd(std::move(x));
std::cout << x.size() << '\n'; // not what it used to be

Beschouw het volgende volledige voorbeeld:

struct Str {
    char* m_ptr;
    Str() : m_ptr(nullptr) {}
    Str(const char* ptr) : m_ptr(strdup(ptr)) {}
    Str(const Str& rhs) : m_ptr(strdup(rhs.m_ptr)) {}
    Str(Str&& rhs) {
      if (&rhs != this) {
        m_ptr = rhs.m_ptr;
        rhs.m_ptr = nullptr;
      }
    }
    ~Str() {
      if (m_ptr) {
        printf("dtor: freeing %p\n", m_ptr)
        free(m_ptr);
        m_ptr = nullptr;
      }
    }
};
void hd(Str&& str) {
  printf("str.m_ptr = %p\n", str.m_ptr);
}
int main() {
  Str a("hello world"); // duplicates 'hello world'.
  Str b(a); // creates another copy
  hd(std::move(b)); // transfers authority for b to function hd.
  //hd(b); // compile error
  printf("after hd, b.m_ptr = %p\n", b.m_ptr); // it's been moved.
}

Als algemene regel:

  • Geef waarde door voor triviale objecten,
  • Geef waarde door als de bestemming een veranderlijke kopie nodig heeft,
  • Geef de waarde door als u altijdeen kopie moet maken,
  • Ga voorbij aan const-referentie voor niet-triviale objecten waarbij de kijker alleen de inhoud/status hoeft te zien, maar niet aanpasbaar hoeft te zijn,
  • Verplaats wanneer de bestemming een veranderlijke kopie van een tijdelijke/geconstrueerde waarde nodig heeft (bijv. std::move(std::string("a") + std::string("b"))).
  • Verplaats wanneer u de locatie van de objectstatus nodig heeft, maar bestaande waarden/gegevens wilt behouden en de huidige houderwilt vrijgeven.

Antwoord 4, autoriteit 9%

Onthoud dat als je geen r-waarde doorgeeft, het doorgeven van een waarde zou resulteren in een volledige kopie. Dus over het algemeen kan het doorgeven van waarde leiden tot een prestatiehit.


Antwoord 5, autoriteit 4%

Uw voorbeeld is gebrekkig. C++11 geeft je geen zet met de code die je hebt, en er zou een kopie gemaakt worden.

U kunt echter een zet krijgen door de functie te declareren om een ​​rvalue-referentie te nemen en er vervolgens een door te geven:

int hd(vector<int>&& a) {
   return a[0];
}
// ...
std::vector<int> a = ...
int x = hd(std::move(a));

Dat veronderstelt dat u de variabele aniet meer in uw functie zult gebruiken, behalve om deze te vernietigen of om er een nieuwe waarde aan toe te kennen. Hier cast std::movede waarde naar een rvalue-referentie, waardoor de verplaatsing mogelijk is.

Const-referenties maken het mogelijk om tijdelijk tijdelijke bestanden te maken. Je kunt iets doorgeven dat geschikt is voor een impliciete constructor, en er wordt een tijdelijke gemaakt. Het klassieke voorbeeld is een char-array die wordt geconverteerd naar const std::string&maar met std::vectorkan een std::initializer_listworden omgezet.

Dus:

int hd(const std::vector<int>&); // Declaration of const reference function
int x = hd({1,2,3,4});

En je kunt de tijdelijke natuurlijk ook verplaatsen:

int hd(std::vector<int>&&); // Declaration of rvalue reference function
int x = hd({1,2,3,4});

Other episodes