Soll ich eine std :: -Funktion als const-Referenz übergeben?


140

Angenommen, ich habe eine Funktion, die Folgendes übernimmt std::function:

void callFunction(std::function<void()> x)
{
    x();
}

Soll ich xstattdessen durch const-reference gehen?:

void callFunction(const std::function<void()>& x)
{
    x();
}

Ändert sich die Antwort auf diese Frage abhängig davon, was die Funktion damit macht? Zum Beispiel, wenn es sich um eine Klassenelementfunktion oder einen Konstruktor handelt, der die std::functionin einer Elementvariablen speichert oder initialisiert .


1
Wahrscheinlich nicht. Ich weiß es nicht genau, aber ich würde erwarten sizeof(std::function), dass es nicht mehr als ist 2 * sizeof(size_t), was die kleinste Größe ist, die Sie jemals für eine konstante Referenz in Betracht ziehen würden.
Mats Petersson

12
@Mats: Ich denke nicht, dass die Größe des std::functionWrappers so wichtig ist wie die Komplexität des Kopierens. Wenn es sich um tiefe Kopien handelt, kann dies weitaus teurer sein als vorgeschlagen sizeof.
Ben Voigt

Solltest du movedie Funktion in?
Yakk - Adam Nevraumont

operator()()ist constso eine const Referenz sollte funktionieren. Aber ich habe std :: function noch nie benutzt.
Neel Basu

@ Yakk Ich übergebe einfach ein Lambda direkt an die Funktion.
Sven Adbring

Antworten:


78

Wenn Sie Leistung wünschen, übergeben Sie den Wert, wenn Sie ihn speichern.

Angenommen, Sie haben eine Funktion namens "Führen Sie dies im UI-Thread aus".

std::future<void> run_in_ui_thread( std::function<void()> )

Das führt einen Code im "UI" -Thread aus und signalisiert dann, futurewann fertig. (Nützlich in UI-Frameworks, in denen sich der UI-Thread dort befindet, wo Sie mit UI-Elementen herumspielen sollen.)

Wir haben zwei Unterschriften, die wir in Betracht ziehen:

std::future<void> run_in_ui_thread( std::function<void()> ) // (A)
std::future<void> run_in_ui_thread( std::function<void()> const& ) // (B)

Nun werden wir diese wahrscheinlich wie folgt verwenden:

run_in_ui_thread( [=]{
  // code goes here
} ).wait();

Dadurch wird ein anonymer Abschluss (ein Lambda) erstellt, ein std::functionOut daraus erstellt, an die run_in_ui_threadFunktion übergeben und gewartet, bis die Ausführung im Hauptthread abgeschlossen ist.

In Fall (A) wird das std::functiondirekt aus unserem Lambda konstruiert, das dann innerhalb des verwendet wird run_in_ui_thread. Das Lambda wird movein den std::functionZustand gebracht, so dass jeder bewegliche Zustand effizient in ihn hineingetragen wird.

Im zweiten Fall wird ein Temporär std::functionerstellt, das Lambda wird darin moveeingefügt, dann dieses Temporärstd::function als Referenz innerhalb des verwendet run_in_ui_thread.

So weit, so gut - die beiden arbeiten identisch. Außer das run_in_ui_threadwird eine Kopie seines Funktionsarguments erstellen, um es zur Ausführung an den UI-Thread zu senden! (Es wird zurückgegeben, bevor es damit fertig ist, daher kann es nicht einfach einen Verweis darauf verwenden.) Für Fall (A) legen wir einfach movedas std::functionin seine Langzeitlagerung. In Fall (B) sind wir gezwungen, das zu kopierenstd::function .

Dieser Laden macht die Wertübergabe optimaler. Wenn die Möglichkeit besteht, dass Sie eine Kopie von speichern std::function, übergeben Sie den Wert. Ansonsten ist jeder Weg ungefähr gleichwertig: Der einzige Nachteil des Nebenwerts besteht darin, dass Sie denselben sperrigen nehmen std::functionund eine Submethode nach der anderen verwenden. Ansonsten ist a moveso effizient wie a const&.

Nun gibt es einige andere Unterschiede zwischen den beiden, die meistens auftreten, wenn wir einen anhaltenden Zustand innerhalb der haben std::function.

Angenommen, das std::functionObjekt wird mit einem gespeichert operator() const, es enthält jedoch auch einige mutableDatenelemente, die geändert werden (wie unhöflich!).

In diesem std::function<> const&Fall werden die mutablegeänderten Datenelemente aus dem Funktionsaufruf weitergegeben. In diesem std::function<>Fall werden sie nicht.

Dies ist ein relativ seltsamer Eckfall.

Sie möchten std::functionwie jeder andere möglicherweise schwere, billig bewegliche Typ behandeln. Umzug ist billig, Kopieren kann teuer sein.


Der semantische Vorteil von "Wert übergeben, wenn Sie ihn speichern" besteht, wie Sie sagen, darin, dass die Funktion vertraglich die Adresse des übergebenen Arguments nicht beibehalten kann. Aber ist es wirklich wahr, dass "Wenn das nicht so ist, ist ein Umzug so effizient wie ein const &"? Ich sehe immer die Kosten für einen Kopiervorgang plus die Kosten für den Verschiebevorgang. Beim Vorbeigehen const&sehe ich nur die Kosten für den Kopiervorgang.
Ceztko

1
@ceztko In beiden Fällen (A) und (B) wird das Temporäre std::functionaus dem Lambda erstellt. In (A) wird das Temporäre in das Argument zu elidiert run_in_ui_thread. In (B) wird ein Verweis auf das Temporäre an weitergegeben run_in_ui_thread. Solange Ihre std::functions aus Lambdas als Provisorien erstellt werden, gilt diese Klausel. Der vorige Absatz befasst sich mit dem Fall, in dem das std::functionweiterhin besteht. Wenn wir nicht speichern, erstellen Sie einfach aus einem Lambda function const&und functionhaben genau den gleichen Overhead.
Yakk - Adam Nevraumont

Ah ich sehe! Dies hängt natürlich davon ab, was außerhalb von passiert run_in_ui_thread(). Gibt es nur eine Unterschrift mit der Aufschrift "Referenzübergabe, aber ich werde die Adresse nicht speichern"?
Ceztko

@ceztko Nein, gibt es nicht.
Yakk - Adam Nevraumont

@ Yakk-AdamNevraumont, wenn vollständiger wäre, um eine andere Option abzudecken, um an rvalue ref vorbeizukommen:std::future<void> run_in_ui_thread( std::function<void()>&& )
Pavel P.

33

Wenn Sie sich Sorgen um die Leistung machen und keine virtuelle Mitgliedsfunktion definieren, sollten Sie diese höchstwahrscheinlich überhaupt nicht verwenden std::function.

Wenn Sie den Funktortyp zu einem Vorlagenparameter machen, können Sie ihn besser optimieren, als die std::functionFunktorlogik einzuschließen. Der Effekt dieser Optimierungen wird wahrscheinlich die Bedenken hinsichtlich der Weitergabe von Kopien und Indirektionen bei der Übergabe erheblich überwiegen std::function.

Schneller:

template<typename Functor>
void callFunction(Functor&& x)
{
    x();
}

Ich mache mir eigentlich überhaupt keine Sorgen um die Leistung. Ich dachte nur, dass es gängige Praxis ist, const-Referenzen dort zu verwenden, wo sie verwendet werden sollten (Strings und Vektoren fallen mir ein).
Sven Adbring

13
@Ben: Ich denke, die modernste Hippie-freundliche Art, dies umzusetzen, besteht darin std::forward<Functor>(x)();, die Wertekategorie des Funktors zu erhalten, da es sich um eine "universelle" Referenz handelt. In 99% der Fälle wird dies jedoch keinen Unterschied machen.
GManNickG

1
@ Ben Voigt also für Ihren Fall, würde ich die Funktion mit einem Zug aufrufen? callFunction(std::move(myFunctor));
Arias_JC

2
@arias_JC: Wenn der Parameter ein Lambda ist, ist er bereits ein r-Wert. Wenn Sie einen l-Wert haben, können Sie ihn entweder verwenden, std::movewenn Sie ihn nicht mehr benötigen, oder direkt übergeben, wenn Sie das vorhandene Objekt nicht verlassen möchten. Regeln callFunction<T&>()zum Reduzieren von Referenzen stellen sicher, dass ein Parameter vom Typ T&nicht vorhanden ist T&&.
Ben Voigt

1
@ BoltzmannBrain: Ich habe mich entschieden, diese Änderung nicht vorzunehmen, da sie nur für den einfachsten Fall gültig ist, wenn die Funktion nur einmal aufgerufen wird. Meine Antwort lautet auf die Frage: "Wie soll ich ein Funktionsobjekt übergeben?" und nicht auf eine Funktion beschränkt, die nichts anderes tut, als diesen Funktor genau einmal bedingungslos aufzurufen.
Ben Voigt

24

Wie in C ++ 11 üblich, hängt die Übergabe von value / reference / const-reference davon ab, was Sie mit Ihrem Argument tun. std::functionist nicht anders.

Durch Übergeben eines Werts können Sie das Argument in eine Variable verschieben (normalerweise eine Mitgliedsvariable einer Klasse):

struct Foo {
    Foo(Object o) : m_o(std::move(o)) {}

    Object m_o;
};

Wenn Sie wissen, dass Ihre Funktion ihr Argument verschiebt, ist dies die beste Lösung. Auf diese Weise können Ihre Benutzer steuern, wie sie Ihre Funktion aufrufen:

Foo f1{Object()};               // move the temporary, followed by a move in the constructor
Foo f2{some_object};            // copy the object, followed by a move in the constructor
Foo f3{std::move(some_object)}; // move the object, followed by a move in the constructor

Ich glaube, Sie kennen die Semantik von (Nicht-) Konstantenreferenzen bereits, daher werde ich den Punkt nicht näher erläutern. Wenn ich weitere Erklärungen dazu hinzufügen muss, fragen Sie einfach und ich werde aktualisieren.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.