Szukaj…


Cel wydania kopii

W standardzie są miejsca, w których obiekt jest kopiowany lub przenoszony w celu zainicjowania obiektu. Eliminacja kopiowania (czasami nazywana optymalizacją wartości zwracanej) jest optymalizacją, w której, w pewnych szczególnych okolicznościach, kompilator może unikać kopiowania lub przenoszenia, nawet jeśli standard mówi, że musi się to zdarzyć.

Rozważ następującą funkcję:

std::string get_string()
{
  return std::string("I am a string.");
}

Zgodnie ze ścisłym brzmieniem standardu funkcja ta zainicjuje tymczasowy std::string , następnie skopiuje / przeniesie go do obiektu wartości zwracanej, a następnie zniszczy tymczasowy. Standard jest bardzo jasny, że tak interpretuje się kod.

Skasowanie kopii jest regułą, która pozwala kompilatorowi C ++ zignorować tworzenie tymczasowego i jego późniejszego kopiowania / niszczenia. Oznacza to, że kompilator może pobrać wyrażenie inicjujące tymczasowe i bezpośrednio zainicjować z niego wartość zwracaną przez funkcję. To oczywiście oszczędza wydajność.

Ma jednak dwa widoczne skutki dla użytkownika:

  1. Typ musi mieć wywoływany konstruktor kopiowania / przenoszenia. Nawet jeśli kompilator pomija kopiowanie / przenoszenie, typ musi być nadal w stanie zostać skopiowany / przeniesiony.

  2. Skutki uboczne konstruktorów kopiowania / przenoszenia nie są gwarantowane w okolicznościach, w których może nastąpić wybranie. Rozważ następujące:

C ++ 11
struct my_type
{
  my_type() = default;
  my_type(const my_type &) {std::cout <<"Copying\n";}
  my_type(my_type &&) {std::cout <<"Moving\n";}
};

my_type func()
{
  return my_type();
}

Co zadzwoni func ? Cóż, nigdy nie wypisze „Kopiowanie”, ponieważ tymczasowe jest wartością, a my_type jest ruchomym typem. Czy wyświetli się napis „Moving”?

Bez reguły eliminacji kopiowania byłoby to konieczne, aby zawsze drukować „Przenoszenie”. Ponieważ jednak istnieje reguła kopiowania kopii, konstruktor ruchu może zostać wywołany lub nie; zależy od implementacji.

Dlatego nie można polegać na wywołaniu konstruktorów kopiowania / przenoszenia w kontekstach, w których możliwa jest elucja.

Ponieważ elision jest optymalizacją, twój kompilator może nie obsługiwać elision we wszystkich przypadkach. I niezależnie od tego, czy kompilator wybierze konkretny przypadek, czy nie, typ musi nadal obsługiwać wybieraną operację. Jeśli więc wybrana jest konstrukcja kopii, typ musi nadal mieć konstruktor kopii, nawet jeśli nie zostanie wywołany.

Gwarantowana kopia kopii

C ++ 17

Zazwyczaj elision jest optymalizacją. Podczas gdy praktycznie każdy kompilator obsługuje kopiowanie wersji w najprostszych przypadkach, posiadanie wersji wciąż stanowi szczególne obciążenie dla użytkowników. Mianowicie, typ, który jest kopiowany / przenoszony, musi nadal mieć operację kopiowania / przenoszenia, która została pominięta.

Na przykład:

std::mutex a_mutex;
std::lock_guard<std::mutex> get_lock()
{
  return std::lock_guard<std::mutex>(a_mutex);
}

Może to być przydatne w przypadkach, gdy a_mutex to a_mutex który jest prywatnie przechowywany przez jakiś system, ale użytkownik zewnętrzny może chcieć mieć blokadę o zasięgu.

Jest to również niezgodne z prawem, ponieważ nie można skopiować ani przenieść std::lock_guard . Mimo że praktycznie każdy kompilator C ++ pomija kopiowanie / przenoszenie, standard nadal wymaga tego typu, aby ta operacja była dostępna.

Do C ++ 17.

C ++ 17 nakazuje elizację poprzez skuteczne przedefiniowanie samego znaczenia niektórych wyrażeń, tak aby nie było kopiowania / przenoszenia. Rozważ powyższy kod.

Zgodnie ze sformułowaniami sprzed C ++ 17 kod ten mówi, aby utworzyć tymczasowy, a następnie użyć tymczasowego do skopiowania / przejścia do wartości zwracanej, ale tymczasowa kopia może zostać pominięta. Zgodnie z brzmieniem C ++ 17 nie tworzy to wcale tymczasowego.

W C ++ 17 każde wyrażenie wartości , gdy jest używane do inicjalizacji obiektu tego samego typu co wyrażenie, nie generuje wartości tymczasowej. Wyrażenie bezpośrednio inicjuje ten obiekt. Jeśli zwrócisz wartość tego samego typu co wartość zwracana, wówczas typ nie musi mieć konstruktora kopiowania / przenoszenia. Dlatego zgodnie z zasadami C ++ 17 powyższy kod może działać.

Sformułowanie C ++ 17 działa w przypadkach, gdy typ wartości jest zgodny z inicjowanym typem. Biorąc get_lock uwagę get_lock powyżej, nie będzie to również wymagało kopiowania / przenoszenia:

std::lock_guard the_lock = get_lock();

Ponieważ wynik get_lock jest wyrażeniem prvalue używanym do inicjalizacji obiektu tego samego typu, kopiowanie lub przenoszenie nie będzie miało miejsca. To wyrażenie nigdy nie tworzy tymczasowości; służy do bezpośredniej inicjalizacji the_lock . Nie ma elekcji, ponieważ nie ma kopii / przeniesienia do elide.

Termin „gwarantowane usunięcie kopii” jest zatem czymś mylącym, ale taka jest nazwa funkcji zaproponowanej do standaryzacji w C ++ 17 . W ogóle nie gwarantuje wyborów; eliminuje całkowicie kopiowanie / przenoszenie, redefiniując C ++, tak aby nigdy nie było kopii / przeniesienia, które należałoby wymyślić.

Ta funkcja działa tylko w przypadkach obejmujących wyrażenie prvalue. W związku z tym stosuje się zwykłe reguły elekcji:

std::mutex a_mutex;
std::lock_guard<std::mutex> get_lock()
{
  std::lock_guard<std::mutex> my_lock(a_mutex);
  //Do stuff
  return my_lock;
}

Chociaż jest to prawidłowy przypadek dla usuwania kopii, reguły C ++ 17 nie eliminują w tym przypadku kopiowania / przenoszenia. Jako taki, typ musi nadal mieć konstruktor kopiuj / przenieś, aby użyć go do zainicjowania wartości zwracanej. A ponieważ lock_guard tego nie robi, jest to nadal błąd kompilacji. Implementacje mogą odmówić udostępnienia kopii przy przekazywaniu lub zwróceniu obiektu typu, który można w prosty sposób skopiować. Ma to na celu umożliwienie przenoszenia takich obiektów w rejestrach, które niektóre ABI mogą nakazać w swoich konwencjach wywoływania.

struct trivially_copyable {
    int a;  
};

void foo (trivially_copyable a) {}

foo(trivially_copyable{}); //copy elision not mandated

Wycofanie wartości zwracanej

Jeśli zwrócisz wyrażenie prvalue z funkcji, a wyrażenie prvalue ma ten sam typ, co typ zwracany przez funkcję, wówczas kopia z tymczasowego prvalue może zostać pominięta:

std::string func()
{
  return std::string("foo");
}

W tym przypadku prawie wszystkie kompilatory pominą tymczasową konstrukcję.

Wyznaczanie parametrów

Gdy przekazujesz argument do funkcji, a argument ten jest wyrażeniem wartości typu parametru typu funkcji, a ten typ nie jest odwołaniem, wówczas można pominąć konstrukcję wartości.

void func(std::string str) { ... }

func(std::string("foo"));

To mówi, aby utworzyć tymczasowy string , a następnie przenieść go do parametru parametru str . Skopiuj elision pozwala temu wyrażeniu na bezpośrednie utworzenie obiektu w str , zamiast używania tymczasowego + ruchu.

Jest to przydatna optymalizacja w przypadkach, w których konstruktor jest explicit . Na przykład moglibyśmy napisać powyższe jako func("foo") , ale tylko dlatego, że string ma niejawny konstruktor, który konwertuje z const char* na string . Gdyby ten konstruktor był explicit , bylibyśmy zmuszeni użyć tymczasowego do wywołania explicit konstruktora. Kopiowanie danych eliminuje nas z konieczności wykonywania niepotrzebnego kopiowania / przenoszenia.

Nazwane zwrócenie wartości zwracanej

Jeśli zwrócisz wyrażenie funkcji z funkcji, a ta wartość:

  • reprezentuje automatyczną zmienną lokalną dla tej funkcji, która zostanie zniszczona po return
  • zmienna automatyczna nie jest parametrem funkcji
  • a typ zmiennej jest tego samego typu co typ zwracany przez funkcję

Jeśli tak jest w każdym przypadku, można pominąć kopiowanie / przeniesienie z wartości:

std::string func()
{
  std::string str("foo");
  //Do stuff
  return str;
}

Bardziej złożone przypadki kwalifikują się do elekcji, ale im bardziej złożony jest przypadek, tym mniej prawdopodobne jest, że kompilator faktycznie go obejmie:

std::string func()
{
  std::string ret("foo");
  if(some_condition)
  {
    return "bar";
  }
  return ret;
}

Kompilator nadal może wybierać ret , ale szanse na to są mniejsze.

Jak wspomniano wcześniej, elucja nie jest dozwolona dla parametrów wartości.

std::string func(std::string str)
{
  str.assign("foo");
  //Do stuff
  return str; //No elision possible
}

Skopiuj dane inicjalizacji

Jeśli użyjesz wyrażenia prvalue do kopiowania, zainicjujesz zmienną, a zmienna ma ten sam typ co wyrażenie prvalue, kopiowanie można uniknąć.

std::string str = std::string("foo");

Inicjalizacja kopii skutecznie przekształca to w std::string str("foo"); (istnieją niewielkie różnice).

Działa to również z wartościami zwracanymi:

std::string func()
{
  return std::string("foo");
}

std::string str = func();

Bez usuwania kopii wywołałoby to 2 wywołania konstruktora ruchów std::string . Kopiowanie elision pozwala temu wywołać konstruktor ruchu 1 lub zero razy, a większość kompilatorów wybierze ten drugi.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow