2016-07-19 26 views
7

Betrachten unten Code zu kopieren:Was die bevorzugte Art und Weise ist es, einen Behälter mit Objekten zu initialisieren, die billig sind zu bewegen, aber schwer

#include <iostream> 
#include <vector> 

struct C { 
    C() {} 
    C(const C&) { std::cout << "A copy was made.\n"; } 
    C(C&&) {std::cout << "A move was made.\n";} 
}; 

std::vector<C> g() { 
    std::vector<C> ret {C(), C(), C()}; 
    return ret; 
} 

std::vector<C> h() { 
    std::vector<C> ret; 
    ret.reserve(3); 
    ret.push_back(C()); 
    ret.push_back(C()); 
    ret.push_back(C()); 
    return ret; 
} 

int main() { 
    std::cout << "Test g\n"; 
    std::vector<C> v1 = g(); 

    std::cout << "Test h\n"; 
    std::vector<C> v2 = h(); 
} 

mit g++ -std=c++11 main.cpp && ./a.out Zusammengestellt, ist das Ergebnis:

Test g 
A copy was made. 
A copy was made. 
A copy was made. 
Test h 
A move was made. 
A move was made. 
A move was made. 

Hinweis Beide Funktionen verwenden Kopie Elision, so dass die zurückgegebene nicht kopiert wird.

Ich verstehe, warum h() verwendet move-constructor, aber warum g()copy-constructor verwendet?

Von vector's doc:

(6) Initialisiererliste Konstruktor

Konstrukten einen Behälter mit einer Kopie von jedem der Elemente in il, in der gleichen Reihenfolge.

Es sieht aus wie Initialisierer-Liste immer die Elemente zu kopieren, dann wohl bedeuten sie die initializer-list constructor die Leistung beeinträchtigt werden könnte, wenn C billig zu bewegen, aber schwer zu kopieren.

Also meine Frage: Was ist die bevorzugte Möglichkeit, einen Container (z. B. vector) mit Objekten zu initialisieren, die billig zu bewegen sind, aber schwer zu kopieren?

+4

'return std :: vector (3);'. Und finde einen Weg, 'C's Konstruktor' noexcept' zu bewegen. – ildjarn

+2

Müssen Sie in Ihrem echten Code Argumente angeben oder verwenden Sie einfach den Standardkonstruktor? – TartanLlama

+0

@ildjarn @TartanLlama Ich weiß 'std :: vector (3);' vermeiden Sie jede Kopie oder Bewegung, aber wenn ich mit Objekten mit Parametern (nicht Standardkonstruktor) initialisieren möchte, kann ich 'std :: vector nicht verwenden (3);. Also, in diesem Fall, was ist der bevorzugte Weg? – Mine

Antwort

3

Sie können von einer Initialisierungsliste mit einem kleinen Standardvorrat wechseln.

template<class T> 
struct force_move{ 
    mutable T t; 

    template<class...Args> 
    force_move(Args&&...args): 
    t(std::forward<Args>(args)...) 
    {} 
    // todo: code that smartly uses {} if() does not work? 

    force_move()=default; 
    force_move(force_move const&)=delete; 

    template<class U, class...Args> 
    force_move(std::initializer_list<U> il, Args&&...args): 
    t(il, std::forward<Args>(args)...) 
    {} 

    operator T()const{ return std::move(t); } 
}; 

template<class T> 
struct make_container { 
    std::initializer_list<force_move<T>> il; 
    make_container(std::initializer_list<force_move<T>> l):il(l) {} 

    template<class C> 
    operator C()&&{ 
    return {il.begin(), il.end()}; 
    } 
}; 

Verwendung:

std::vector<C> v=make_container<C>{ {}, {} }; 

Dies ist präzise, ​​effizient und löst Ihr Problem.

(Möglicherweise sollte es operator T&& oben sein. Nicht sicher, und ich bin misstrauisch jemals eine rvalue Referenz Rückkehr ...)

Nun, das ist ein bisschen wie ein Hack scheint. Aber, die Alternativen saugen.

Die manuelle Push/Back-Liste ist hässlich und wird immer hässlicher, wenn Sie die Reserveanforderungen für maximale Effizienz hinzufügen. Und die naive il Lösung kann sich nicht bewegen.

Lösungen, die Sie nicht die Elemente genau dort, wo die Instanz erklärt werden, sind peinlich, meiner Meinung nach. Sie möchten die Liste der Inhalte neben der Deklaration einfügen.

Eine andere "lokale Liste" Alternative besteht darin, eine Variantfunktion zu erstellen, die intern einen (möglicherweise von Ref-Wrappern) initialisiert, der sich dann von diesem Array in den Container bewegt. Dies erlaubt jedoch keine { {}, {}, {} } Stillisten, daher finde ich es zu wenig.

Wir könnten dies tun:

template<class T, std::size_t N> 
std::vector<T> move_from_array(T(&arr)[N]){ 
    return {std::make_move_iterator(std::begin(arr)), std::make_move_iterator(std::end(arr))}; 
} 

Dann:

C arr[]={{}, {}, {}}; 
std::vector<C> v = move_from_array(arr); 

der einzige Nachteil zwei Aussagen am Verwendungsort ist erforderlich. Aber der Code ist weniger stumpf als meine erste Lösung.

+0

Die 'force_move' und' make_container' funktioniert, aber 'std :: vector v3 = make_container {C(), C(), C()};' Kosten 9 bewegen Konstruktor Aufrufe. Vielleicht könnte "move_from_array" einen besseren Job machen? – Mine

+0

@Mine Nicht 'C()', nur '{}'. Das spart 3 Züge: Du hast ohne Grund Provisorien erstellt. Ersetzen Sie 'operator C()' durch 'operator C &&()'. Das spart 3 Züge. – Yakk

+0

Die Änderung von '{}' spart 3 Züge, aber interessanterweise gibt 'operator C() &&' beim Klängen 3 Züge, aber gcc gibt 6 Züge. – Mine

2

Leider std::initializer_listdoesn't really work with move semantics.

Wenn Sie Argumente müssen an den Konstruktor liefern, würde ich stattdessen emplace_back verwenden, die das Element anstelle konstruiert:

std::vector<C> h() { 
    std::vector<C> ret; 
    ret.reserve(3); 
    ret.emplace_back(arg1); 
    ret.emplace_back(arg2,arg3); 
    ret.emplace_back(0,12); 
    return ret; 
} 
+1

Vergessen Sie nicht Konstruktoren, die mit 'emplace_back' aufgerufen werden, müssen' noexcept' sein. Andernfalls werden stattdessen + + -Konstruktoren aufgerufen. –

+0

@AndreiR .: Kann Ihre Kopie nicht reproduzieren: [Demo] (http://coliru.stacked-crooked.com/a/3074b357819752ac) – Jarod42

+0

@ Jarod42, dies ist, weil 'Reserve' auch das Verhalten beeinflusst: [demo] (http : //coliru.stacked-crooked.com/a/32d2cc583df98abe). Im Allgemeinen müssen Bewegungskonstruktoren/Zuweisungsoperatoren nicht mit vielen Algorithmen arbeiten ('std :: move_if_noexcept' ist in stl weit verbreitet) –

3

Sie cannot move from an initializer_list (abgesehen von Gymnastik mit mutable wie in Yakk Antwort), weil die Elemente Eine Initialisierungsliste wird const (moving elements of an initialization_list considered dangerous?) deklariert.

Ich würde empfehlen, Ihre Objekte in einem Behälter mit einer aggregierten Initialisierung das heißt einer klassischen Anordnung oder std::array konstruieren, dann den Vektor von move iterators Konstruktion:

std::vector<C> h() { 
    C[] arr{C(), C(), C()}; 
    return std::vector<C>(
     std::make_move_iterator(std::begin(arr)), 
     std::make_move_iterator(std::end(arr))); 
} 
+0

Eigentlich scheint Yakks Antwort in Ordnung zu sein. Er bewegt sich nicht von der 'initializer_list', sondern wandelt nur die Elemente davon um. Unter der Haube wird der Inhalt der initializer_list geändert. Aber das ist in Ordnung, da seine Elemente nicht in den Nur-Lese-Speicher gesetzt werden können, weil mutable das verhindert. –

+0

@RalphTandetzky Entschuldigung, ich wollte nicht andeuten, dass Yakks Antwort falsch war; nur, dass es ziemlich kompliziert (obwohl wiederverwendbar) ist und dass man ohne es illegales Verhalten hätte. – ecatmur