In Herb Sutters CppCon 2014-Vortrag Zurück zu den Grundlagen: Moderner C ++ - Stil verweist er auf Folie 28 ( eine Webkopie der Folien finden Sie hier ) auf dieses Muster:
class employee {
std::string name_;
public:
void set_name(std::string name) noexcept { name_ = std::move(name); }
};
Er sagt, dass dies problematisch ist, weil beim Aufrufen von set_name () mit einer temporären Noexcept-Ness nicht stark ist (er verwendet den Ausdruck "Noexcept-ish").
Jetzt habe ich das obige Muster in meinem aktuellen C ++ - Code ziemlich häufig verwendet, hauptsächlich, weil ich dadurch nicht jedes Mal zwei Kopien von set_name () eingeben muss - ja, ich weiß, dass dies ein bisschen ineffizient sein kann, wenn ich jedes Mal eine Kopienkonstruktion erzwinge. aber hey ich bin ein fauler typer. Herbs Satz "Dieses Noexcept ist problematisch " macht mir jedoch Sorgen, da ich das Problem hier nicht verstehe: Der Verschiebungszuweisungsoperator von std :: string ist noexcept, ebenso wie sein Destruktor. Daher scheint mir set_name () oben noexcept garantiert zu sein. Ich sehe eine mögliche Ausnahme, die der Compiler vor set_name () auslöst, wenn er den Parameter vorbereitet, aber ich habe Mühe, dies als problematisch anzusehen.
Später auf Folie 32 stellt Herb klar fest, dass das Obige ein Anti-Muster ist. Kann mir jemand erklären, warum ich nicht faul schlechten Code geschrieben habe?
noexcept
hier ein Mythos ist, weil die Zuordnung, die auf der Seite des Angerufenen geschieht, werfen kann (z. B. beim Erstellen einesstd::string
aus rohen Zeichenfolgenliteral oder anderenstd::string
). Der Körper wirft also nicht, aber das Aufrufen dieser Funktion kann immer noch dazu führen, dass eine Ausnahme ausgelöst wird (std::bad_alloc
)noexcept
, aber das Aufrufen kann werfen. Die Tatsache, dass die Ausnahme ausgelöst wird, bevor der Funktionskörper selbst eingegeben wird, beginnt sich in das Gebiet "Wie viele Engel können auf dem Kopf einer Stecknadel tanzen" zu begeben.noexcept
sagt wirklich nur "Wenn das Aufrufen dieser Funktion nicht auslöst, verspreche ich, dass Sie keine Ausnahme bekommen", was wohl weniger nützlich ist, als es sein könnte.std::string
. Das ist dumm und macht die ganze Übung ziemlich sinnlos. Machen Sieconst&
im Allgemeinen keine einsamen Überlastungen. Abgesehen von der Sachenoexcept
sollten Herbs Argumente für einen Einzelnenconst&
für nichts anderes als überzeugenstd::string
.Antworten:
Andere haben die
noexcept
obigen Überlegungen behandelt.Herb verbrachte viel mehr Zeit im Gespräch über die Effizienzaspekte. Das Problem liegt nicht bei Zuweisungen, sondern bei unnötigen Freigaben. Wenn Sie eine kopieren
std::string
In einem anderen Fall verwendet die Kopierroutine den zugewiesenen Speicher der Zielzeichenfolge erneut, wenn genügend Speicherplatz für die zu kopierenden Daten vorhanden ist. Bei einer Verschiebungszuweisung muss der vorhandene Speicher der Zielzeichenfolge freigegeben werden, da er den Speicher von der Quellzeichenfolge übernimmt. Die Redewendung "Kopieren und Verschieben" erzwingt, dass die Freigabe immer erfolgt, auch wenn Sie keine temporäre Zuordnung übergeben. Dies ist die Quelle der schrecklichen Leistung, die später im Vortrag demonstriert wird. Sein Rat war, stattdessen einen const ref zu nehmen und wenn Sie feststellen, dass Sie ihn benötigen, eine Überladung für R-Wert-Referenzen zu haben. Dies gibt Ihnen das Beste aus beiden Welten: Kopieren Sie es in den vorhandenen Speicher für Nicht-Provisorien, vermeiden Sie die Freigabe, und verschieben Sie es für Provisorien, in denen Sie sich befinden.Das Obige gilt nicht für Konstruktoren, da in der Mitgliedsvariablen kein Speicher zum Freigeben vorhanden ist. Dies ist hilfreich, da Konstruktoren häufig mehr als ein Argument verwenden. Wenn Sie für jedes Argument const ref / r-value ref-Überladungen durchführen müssen, kommt es zu einer kombinatorischen Explosion von Konstruktorüberladungen.
Die Frage lautet nun: Wie viele Klassen gibt es, die beim Kopieren Speicher wie std :: string wiederverwenden? Ich vermute, dass std :: vector funktioniert, aber davon abgesehen bin ich mir nicht sicher. Ich weiß, dass ich noch nie eine Klasse geschrieben habe, die Speicher wie diesen wiederverwendet, aber ich habe viele Klassen geschrieben, die Zeichenfolgen und Vektoren enthalten. Das Befolgen von Herbs Ratschlägen schadet Ihnen nicht für Klassen, die keinen Speicher wiederverwenden. Sie kopieren zunächst mit der Kopierversion der Senkenfunktion. Wenn Sie feststellen, dass das Kopieren zu stark von der Leistung abhängt, werden Sie dies tun Machen Sie eine Überladung der R-Wert-Referenz, um die Kopie zu vermeiden (genau wie bei std :: string). Auf der anderen Seite hat die Verwendung von "Kopieren und Verschieben" einen nachgewiesenen Leistungseinbruch für std :: string und andere Typen, die Speicher wiederverwenden. und diese Typen werden wahrscheinlich im Code der meisten Leute häufig verwendet. Ich folge vorerst Herbs Rat, muss aber noch ein wenig darüber nachdenken, bevor ich das Problem für vollständig gelöst halte (es gibt wahrscheinlich einen Blog-Beitrag, für den ich keine Zeit habe, in all dem zu lauern).
quelle
Es wurden zwei Gründe in Betracht gezogen, warum das Übergeben von Werten besser sein könnte als das Übergeben von const-Referenzen.
Im Fall von Setzern für Mitglieder des Typs
std::string
entlarvte er die Behauptung, dass das Übergeben von Werten effizienter sei, indem er zeigte, dass das Übergeben von const-Referenzen typischerweise weniger Zuweisungen (zumindest fürstd::string
) ergab .Er entlarvte auch die Behauptung, dass dies dem Setter erlaubt,
noexcept
indem er zeigte, dass die Deklaration noexcept irreführend ist, da beim Kopieren des Parameters immer noch eine Ausnahme auftreten kann.Er kam daher zu dem Schluss, dass zumindest in diesem Fall die Übergabe der Konstantenreferenz der Übergabe des Wertes vorzuziehen sei. Er erwähnte jedoch, dass das Übergeben von Werten ein potenziell guter Ansatz für Konstrukteure sei.
Ich denke, dass das Beispiel für
std::string
allein nicht ausreicht, um es auf alle Typen zu verallgemeinern, aber es stellt die Praxis in Frage, teure zu kopierende, aber billig zu verschiebende Parameter zumindest aus Effizienz- und Ausnahmegründen nach Wert zu übergeben.quelle
name_.capacity() > name.length()
Sie nur wirklich einen Memcpy benötigen. Das allein scheint genug zu sein, um es als Anti-Muster zu bezeichnen und für zwei Überladungen zu argumentierennoexcept
, aber wie Herb im Vortrag betont, "nicht wirklich", da in vielen Fällen eine Kopie erstellt werden muss, um übergeben zu werden, und das Erstellen dieser Kopie kann werfen. Sicher, die Ausnahme tritt auf, bevor der Anruf getätigt wird, aber es passiert trotzdem.Herb hat den Sinn, dass die In-By-Wert-Zuweisung, wenn Sie bereits Speicher zugewiesen haben, ineffizient sein und eine unnötige Zuweisung verursachen kann. Das Vorbeigehen
const&
ist jedoch fast so schlecht, als ob Sie eine rohe C-Zeichenfolge nehmen und an die Funktion übergeben, eine unnötige Zuordnung erfolgt.Was Sie nehmen sollten, ist die Abstraktion des Lesens von einem String, nicht von einem String selbst, denn das ist es, was Sie brauchen.
Jetzt können Sie dies tun als
template
:class employee { std::string name_; public: template<class T> void set_name(T&& name) noexcept { name_ = std::forward<T>(name); } };
das ist einigermaßen effizient. Dann fügen Sie vielleicht etwas SFINAE hinzu:
class employee { std::string name_; public: template<class T> std::enable_if_t<std::is_convertible<T,std::string>::value> set_name(T&& name) noexcept { name_ = std::forward<T>(name); } };
Wir bekommen also Fehler an der Schnittstelle und nicht an der Implementierung.
Dies ist nicht immer praktisch, da die Implementierung öffentlich zugänglich gemacht werden muss.
Hier
string_view
kann eine Typklasse ins Spiel kommen:template<class C> struct string_view { // could be private: C const* b=nullptr; C const* e=nullptr; // key component: C const* begin() const { return b; } C const* end() const { return e; } // extra bonus utility: C const& front() const { return *b; } C const& back() const { return *std::prev(e); } std::size_t size() const { return e-b; } bool empty() const { return b==e; } C const& operator[](std::size_t i){return b[i];} // these just work: string_view() = default; string_view(string_view const&)=default; string_view&operator=(string_view const&)=default; // myriad of constructors: string_view(C const* s, C const* f):b(s),e(f) {} // known continuous memory containers: template<std::size_t N> string_view(const C(&arr)[N]):string_view(arr, arr+N){} template<std::size_t N> string_view(std::array<C, N> const& arr):string_view(arr.data(), arr.data()+N){} template<std::size_t N> string_view(std::array<C const, N> const& arr):string_view(arr.data(), arr.data()+N){} template<class... Ts> string_view(std::basic_string<C, Ts...> const& str):string_view(str.data(), str.data()+str.size()){} template<class... Ts> string_view(std::vector<C, Ts...> const& vec):string_view(vec.data(), vec.data()+vec.size()){} string_view(C const* str):string_view(str, str+len(str)) {} private: // helper method: static std::size_t len(C const* str) { std::size_t r = 0; if (!str) return r; while (*str++) { ++r; } return r; } };
Ein solches Objekt kann direkt aus einem
std::string
oder einem erstellt werden"raw C string"
und fast kostenlos speichern, was Sie wissen müssen, um daraus ein neues zu produzierenstd::string
.class employee { std::string name_; public: void set_name(string_view<char> name) noexcept { name_.assign(name.begin(),name.end()); } };
und da unsere jetzt
set_name
eine feste Schnittstelle hat (keine perfekte Vorwärtsschnittstelle), kann ihre Implementierung nicht sichtbar sein.Die einzige Ineffizienz besteht darin, dass Sie, wenn Sie einen Zeichenfolgenzeiger im C-Stil übergeben, seine Größe unnötig zweimal überschreiten (erstmaliges Suchen nach
'\0'
, zweites Kopieren). Auf der anderen Seite gibt dies Ihrem Ziel Informationen darüber, wie groß es sein muss, sodass es vorab zuweisen kann, anstatt es neu zuzuweisen.quelle
string_view
: Selbst wenn Sie diesen Zeiger direkt übergeben würdenstd::string::assign
, würde die Implementierung mit ziemlicher Sicherheit zwei Durchgänge benötigen, um die Länge zu scannen und trotzdem zu kopieren. Jedenfalls für Zeichenfolgen, die größer als die SSO-Größe sind.push_back
basierte Implementierung vor, die das Ziel löscht, dann jedes Zeichen pusht und'\0'
vor dem Stoppen nach prüft . Dies liest nur einmal. Die Unfähigkeit, den Zielpuffer vorab zuzuweisen, ist mit Kosten verbunden. Wenn Sie jedoch ständig Zeichenfolgen ähnlicher Länge zuweisen, kann eine solche Implementierung schneller sein. Astring_view<char>
kann das nicht duplizieren: Einetemplate
Implementierung könnte. In der Tat könnte einestd::basic_string<char>::operator=
Überladung fürchar const*
das eingegebene Zeichen wahrscheinlich nach\0
Kopieren suchen , bis es nicht mehr im Raum ist, und dann nach Größe suchen und möglicherweise die Größe ändern.range_view
,array_view
undstring_view
Klassen eine Menge Nutzen außerhalb dieses Problems haben: die Fähigkeit , nicht-Kopie Abschnitte von Bereichen zu nehmen, sind zusammenhängende Bereiche oder Strings sehr mächtig. Dies ist nur eine weitere Verwendung von ihnen.Sie haben zwei Möglichkeiten, diese Methoden aufzurufen.
rvalue
parametermove constructor
gibt es kein Problem ( solange höchstwahrscheinlich kein Parameterstd::string
vorliegt), solange der Parametertyp noexcept ist. Verwenden Sie in jedem Fall besser ein bedingtes noexcept (um sicherzugehen, dass die Parameter noexcept sind).lvalue
parameter würde in diesem Fall dercopy constructor
vom Parametertyp aufgerufen und es ist fast sicher, dass eine Zuordnung erforderlich ist (die werfen könnte).In solchen Fällen, in denen die Verwendung möglicherweise nicht verwendet wird, ist es besser, sie zu vermeiden. Der Client des geht
class
davon aus, dass keine Ausnahme wie angegeben ausgelöst wird, aber in gültiger, kompilierbarer, nicht verdächtiger Form ausgelöst werden kannC++11
.quelle
std::string
, und dastd::allocator
diespropagate_on_container_move_assignment
zutreffend ist, ist es wahr, dass die Verschiebungszuweisung von std :: string in der Praxis niemals geworfen wird, selbst wenn dienoexcept
Garantie in einem zukünftigen Standard entfernt wird. Dies gilt jedochstd::basic_string
im Allgemeinen nicht.