Suche…


Zweck der Auslassung von Kopien

Es gibt Stellen im Standard, an denen ein Objekt kopiert oder verschoben wird, um ein Objekt zu initialisieren. Copy elision (manchmal auch als Rückgabewertoptimierung bezeichnet) ist eine Optimierung, bei der ein Compiler unter bestimmten Umständen das Kopieren oder Verschieben vermeiden darf, obwohl der Standard dies für erforderlich hält.

Betrachten Sie die folgende Funktion:

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

Gemäß dem strengen Wortlaut des Standards initialisiert diese Funktion einen temporären std::string , kopiert / verschiebt diesen in das Rückgabewertobjekt und zerstört dann das temporäre. Der Standard ist sehr klar, dass der Code auf diese Weise interpretiert wird.

Copy elision ist eine Regel, die es einem C ++ - Compiler erlaubt , die Erstellung des temporären Elements und die anschließende Kopie / Zerstörung zu ignorieren . Das heißt, der Compiler kann den Initialisierungsausdruck für das temporäre Element übernehmen und den Rückgabewert der Funktion direkt daraus initialisieren. Dies spart offensichtlich Leistung.

Es hat jedoch zwei sichtbare Auswirkungen auf den Benutzer:

  1. Der Typ muss über den Copy / Move-Konstruktor verfügen, der aufgerufen worden wäre. Selbst wenn der Compiler das Kopieren / Verschieben aufhebt, muss der Typ kopiert / verschoben werden können.

  2. Nebenwirkungen von Copy / Move-Konstruktoren können unter Umständen nicht garantiert werden, in denen eine Entscheidung getroffen werden kann. Folgendes berücksichtigen:

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();
}

Was wird der Aufruf von func tun? Nun, es wird niemals "Kopieren" gedruckt, da der temporäre Wert ein r- my_type ist und my_type ein beweglicher Typ ist. Wird es "Moving" drucken?

Ohne die Kopiereliminierungsregel müsste dies immer "Moving" gedruckt werden. Da die Kopierelisierungsregel jedoch existiert, kann der Bewegungskonstruktor aufgerufen werden oder nicht. es ist implementierungsabhängig.

Daher können Sie sich nicht auf den Aufruf von Copy / Move-Konstruktoren in Kontexten verlassen, in denen eine Entscheidung möglich ist.

Da elision eine Optimierung ist, unterstützt Ihr Compiler möglicherweise nicht in allen Fällen elision. Unabhängig davon, ob der Compiler einen bestimmten Fall auswählt oder nicht, muss der Typ die ausgeführte Operation trotzdem unterstützen. Wenn also eine Kopierkonstruktion entfernt wird, muss der Typ noch einen Kopierkonstruktor haben, auch wenn er nicht aufgerufen wird.

Garantierte Kopiereinstellung

C ++ 17

Normalerweise ist die Entscheidung eine Optimierung. Während praktisch jeder Compiler in den einfachsten Fällen eine Kopierentscheidung unterstützt, belasten die Benutzer dennoch die Elizierung. Der Typ, dessen Kopie / Verschiebung ausgesondert wird, muss immer noch über die Kopier- / Verschiebeoperation verfügen, die ausgelesen wurde.

Zum Beispiel:

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

Dies kann in Fällen nützlich sein, in denen a_mutex ein Mutex ist, der sich in einem privaten Besitz befindet, ein externer Benutzer jedoch eine Sperrung für bestimmte Bereiche wünschen möchte.

Dies ist auch nicht zulässig, da std::lock_guard nicht kopiert oder verschoben werden kann. Obwohl praktisch jeder C ++ - Compiler für das Kopieren / Verschieben zuständig ist, muss der Typ für diesen Typ verfügbar sein.

Bis C ++ 17.

In C ++ 17 müssen Sie die Eliminierung von Befehlen vornehmen, indem Sie die Bedeutung bestimmter Ausdrücke effektiv neu definieren, sodass kein Kopieren / Verschieben stattfindet. Betrachten Sie den obigen Code.

Bei der Formulierung vor C ++ 17 heißt es in diesem Code, eine temporäre Datei zu erstellen und dann die temporäre Kopie zum Kopieren / Verschieben in den Rückgabewert zu verwenden. Die temporäre Kopie kann jedoch entfernt werden. Unter dem Wortlaut von C ++ 17 wird dadurch überhaupt keine temporäre erstellt.

In C ++ 17 generiert ein Prvalue-Ausdruck bei der Initialisierung eines Objekts desselben Typs wie der Ausdruck kein temporäres Objekt. Der Ausdruck initialisiert das Objekt direkt. Wenn Sie einen pr-Wert desselben Typs zurückgeben, der dem Rückgabewert entspricht, muss der Typ keinen Kopier- / Verschiebungskonstruktor haben. Und unter den C ++ 17-Regeln kann der obige Code funktionieren.

Die Formulierung von C ++ 17 funktioniert in Fällen, in denen der Typ des prvalue mit dem zu initialisierenden Typ übereinstimmt. Bei gegebenem get_lock oben ist auch keine Kopie / Verschiebung erforderlich:

std::lock_guard the_lock = get_lock();

Da das Ergebnis von get_lock ein prvalue-Ausdruck ist, der zum Initialisieren eines Objekts desselben Typs verwendet wird, erfolgt kein Kopieren oder Verschieben. Dieser Ausdruck schafft niemals eine temporäre; es wird verwendet, um the_lock direkt zu initialisieren. Es gibt keine Entscheidung, weil es keine Kopie / Bewegung gibt, um elide elide zu sein.

Der Begriff "garantierte Kopienauswahl" ist daher etwas falsch. Dies ist jedoch der Name des Features, wie es für die C ++ 17-Standardisierung vorgeschlagen wird . Es garantiert überhaupt keine Ausscheidung; Dadurch entfällt das Kopieren / Verschieben insgesamt, wodurch C ++ neu definiert wird, so dass niemals ein Kopiervorgang ausgeführt wurde.

Diese Funktion funktioniert nur in Fällen, in denen ein Prvalue-Ausdruck enthalten ist. Als solches verwendet dies die üblichen Ausscheidungsregeln:

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;
}

Dies ist zwar ein gültiger Fall für das Kopieren von Kopien, aber C ++ 17-Regeln beseitigen in diesem Fall das Kopieren / Verschieben nicht. Daher muss der Typ noch einen Kopier- / Verschiebungskonstruktor enthalten, der zum Initialisieren des Rückgabewerts verwendet werden kann. Und da lock_guard dies nicht tut, ist dies immer noch ein Kompilierungsfehler. Implementierungen dürfen die Ausgabe von Kopien verweigern, wenn sie ein Objekt mit trivial kopierbarem Typ übergeben oder zurückgeben. Dies ermöglicht das Verschieben solcher Objekte in Registern, die einige ABIs möglicherweise in ihren Aufrufkonventionen festlegen.

struct trivially_copyable {
    int a;  
};

void foo (trivially_copyable a) {}

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

Rückgabewert elision

Wenn Sie einen prvalue-Ausdruck aus einer Funktion zurückgeben und der prvalue-Ausdruck denselben Typ wie der Rückgabetyp der Funktion hat, kann die Kopie aus dem temporären prvalue-Objekt entfernt werden:

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

So ziemlich alle Compiler werden in diesem Fall die temporäre Konstruktion übernehmen.

Parameterauswahl

Wenn Sie ein Argument an eine Funktion übergeben und das Argument ein prvalue-Ausdruck des Parametertyps der Funktion ist und dieser Typ keine Referenz ist, kann die Konstruktion des prvalue aufgehoben werden.

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

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

Dies bedeutet, dass Sie eine temporäre string erstellen und diese dann in den Funktionsparameter str . Copy elision erlaubt es diesem Ausdruck, das Objekt direkt in str zu erstellen, anstatt eine temporäre + Verschiebung zu verwenden.

Dies ist eine nützliche Optimierung für Fälle, in denen ein Konstruktor als explicit deklariert wird. Zum Beispiel hätten wir das Obige als func("foo") schreiben können, aber nur, weil string einen impliziten Konstruktor hat, der von const char* in einen string konvertiert. Wenn dieser Konstruktor explicit , müssen wir den explicit Konstruktor mit einem temporären Aufruf aufrufen. Copy elision erspart uns das unnötige Kopieren / Verschieben.

Benannte Rückgabewerte

Wenn Sie einen lvalue-Ausdruck von einer Funktion zurückgeben, und diesen lvalue:

  • stellt eine automatische Variable lokal für diese Funktion dar, die nach der return
  • Die automatische Variable ist kein Funktionsparameter
  • und der Typ der Variablen ist derselbe Typ wie der Rückgabetyp der Funktion

Wenn dies alles der Fall ist, kann das Kopieren / Bewegen vom Wert abgelöst werden:

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

Für komplexere Fälle kann eine Entscheidung getroffen werden, aber je komplexer der Fall ist, desto unwahrscheinlicher wird es sein, dass der Compiler die Entscheidung trifft.

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

Der Compiler konnte noch elide ret , aber die Chancen, sie so tun , nach unten gehen.

Wie bereits erwähnt, ist für Werteparameter keine Entscheidung zulässig.

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

Kopieren Sie die Initialisierung

Wenn Sie zum Kopieren einer Variablen einen Prvalue-Ausdruck verwenden und diese Variable denselben Typ wie der Prvalue-Ausdruck hat, kann das Kopieren abgebrochen werden.

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

Die Initialisierung beim Kopieren wandelt dies effektiv in std::string str("foo"); (es gibt geringfügige Unterschiede).

Dies funktioniert auch mit Rückgabewerten:

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

std::string str = func();

Ohne Copy-Elision würde dies 2 Aufrufe des Move-Konstruktors von std::string hervorrufen. Die Kopierentscheidung erlaubt den Aufruf des Bewegungskonstruktors 1 oder null Mal, und die meisten Compiler entscheiden sich für Letzteres.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow