Kann ich einen Vektor vom Typ "Nur Verschieben" auflisten?

95

Wenn ich den folgenden Code durch meinen GCC 4.7-Snapshot übergebe, wird versucht, das unique_ptrs in den Vektor zu kopieren .

#include <vector>
#include <memory>

int main() {
    using move_only = std::unique_ptr<int>;
    std::vector<move_only> v { move_only(), move_only(), move_only() };
}

Offensichtlich kann das nicht funktionieren, weil std::unique_ptres nicht kopierbar ist:

Fehler: Verwendung der gelöschten Funktion 'std :: unique_ptr <_Tp, _Dp> :: unique_ptr (const std :: unique_ptr <_Tp, _Dp> &) [mit _Tp = int; _Dp = std :: default_delete; std :: unique_ptr <_Tp, _Dp> = std :: unique_ptr] '

Ist GCC korrekt beim Versuch, die Zeiger aus der Initialisierungsliste zu kopieren?

R. Martinho Fernandes
quelle
Visual Studio und Clang haben das gleiche Verhalten
Jean-Simon Brochu

Antworten:

46

Die Zusammenfassung von <initializer_list>in 18.9 macht ziemlich deutlich, dass Elemente einer Initialisiererliste immer über const-reference übergeben werden. Leider scheint es in der aktuellen Version der Sprache keine Möglichkeit zu geben, die Bewegungssemantik in Initialisierungslistenelementen zu verwenden.

Insbesondere haben wir:

typedef const E& reference;
typedef const E& const_reference;

typedef const E* iterator;
typedef const E* const_iterator;

const E* begin() const noexcept; // first element
const E* end() const noexcept; // one past the last element
Kerrek SB
quelle
4
Beachten Sie das in <T> auf cpptruths ( cpptruths.blogspot.com/2013/09/… ) beschriebene Idiom . Die Idee ist, lvalue / rvalue zur Laufzeit zu bestimmen und dann move oder copy-building aufzurufen. in <T> erkennt rvalue / lvalue, obwohl die von initializer_list bereitgestellte Standardschnittstelle eine konstante Referenz ist.
Sumant
3
@Sumant Scheint mir nicht so "idiomatisch": Ist es nicht stattdessen reine UB? Dies könnte nicht nur der Iterator sein, sondern auch die zugrunde liegenden Elemente selbst const, die in einem wohlgeformten Programm nicht weggeworfen werden können.
underscore_d
62

Bearbeiten: Da @Johannes nicht die beste Lösung als Antwort veröffentlichen möchte, mache ich es einfach.

#include <iterator>
#include <vector>
#include <memory>

int main(){
  using move_only = std::unique_ptr<int>;
  move_only init[] = { move_only(), move_only(), move_only() };
  std::vector<move_only> v{std::make_move_iterator(std::begin(init)),
      std::make_move_iterator(std::end(init))};
}

Die von zurückgegebenen Iteratoren std::make_move_iteratorverschieben das Element, auf das verwiesen wird, wenn sie dereferenziert werden.


Ursprüngliche Antwort: Wir werden hier einen kleinen Hilfstyp verwenden:

#include <utility>
#include <type_traits>

template<class T>
struct rref_wrapper
{ // CAUTION - very volatile, use with care
  explicit rref_wrapper(T&& v)
    : _val(std::move(v)) {}

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

private:
  T&& _val;
};

// only usable on temporaries
template<class T>
typename std::enable_if<
  !std::is_lvalue_reference<T>::value,
  rref_wrapper<T>
>::type rref(T&& v){
  return rref_wrapper<T>(std::move(v));
}

// lvalue reference can go away
template<class T>
void rref(T&) = delete;

Leider funktioniert der einfache Code hier nicht:

std::vector<move_only> v{ rref(move_only()), rref(move_only()), rref(move_only()) };

Da der Standard aus irgendeinem Grund keinen konvertierenden Kopierkonstruktor wie diesen definiert:

// in class initializer_list
template<class U>
initializer_list(initializer_list<U> const& other);

Das initializer_list<rref_wrapper<move_only>>von der Klammer-init-Liste ( {...}) erstellte wird nicht in das konvertiert, initializer_list<move_only>was das vector<move_only>nimmt. Wir brauchen hier also eine zweistufige Initialisierung:

std::initializer_list<rref_wrapper<move_only>> il{ rref(move_only()),
                                                   rref(move_only()),
                                                   rref(move_only()) };
std::vector<move_only> v(il.begin(), il.end());
Xeo
quelle
1
Ah ... das ist das rWert-Analogon von std::ref, nicht? Vielleicht sollte es genannt werden std::rref.
Kerrek SB
17
Nun, ich denke, dies sollte nicht ohne Erwähnung in einem Kommentar bleiben :) move_only m[] = { move_only(), move_only(), move_only() }; std::vector<move_only> v(std::make_move_iterator(m), std::make_move_iterator(m + 3)); .
Johannes Schaub - Litb
1
@Johannes: Manchmal sind es die einfachen Lösungen, die mir einfach entgehen. Obwohl ich zugeben muss, habe ich mich noch nicht darum move_iteratorgekümmert.
Xeo
2
@Johannes: Warum ist das nicht eine Antwort? :)
Xeo
1
@ JohanLundberg: Ich würde das als QoI-Problem betrachten, aber ich verstehe nicht, warum es das nicht kann. Die stdlib von VC ++, zum Beispiel Tag-Dispatches, die auf der Iteratorkategorie basieren und std::distancefür Forward-or- Better -Iteratoren verwendet werden, std::move_iteratorpasst die zugrunde liegende Iteratorkategorie an. Wie auch immer, gute und prägnante Lösung. Vielleicht als Antwort posten?
Xeo
10

Wie in anderen Antworten erwähnt, besteht das Verhalten von std::initializer_listdarin, Objekte nach Wert zu halten und kein Herausziehen zuzulassen, sodass dies nicht möglich ist. Hier ist eine mögliche Problemumgehung mithilfe eines Funktionsaufrufs, bei dem die Initialisierer als verschiedene Argumente angegeben werden:

#include <vector>
#include <memory>

struct Foo
{
    std::unique_ptr<int> u;
    int x;
    Foo(int x = 0): x(x) {}
};

template<typename V>        // recursion-ender
void multi_emplace(std::vector<V> &vec) {}

template<typename V, typename T1, typename... Types>
void multi_emplace(std::vector<V> &vec, T1&& t1, Types&&... args)
{
    vec.emplace_back( std::move(t1) );
    multi_emplace(vec, args...);
}

int main()
{
    std::vector<Foo> foos;
    multi_emplace(foos, 1, 2, 3, 4, 5);
    multi_emplace(foos, Foo{}, Foo{});
}

Unglücklicherweise multi_emplace(foos, {}); schlägt dies fehl, da der Typ für nicht abgeleitet werden {}kann. Damit Objekte standardmäßig erstellt werden, müssen Sie den Klassennamen wiederholen. (oder verwenden vector::resize)

MM
quelle
4
Die rekursive Pack-Erweiterung könnte durch den Dummy-Array-Komma-Operator-Hack ersetzt werden, um ein paar Codezeilen zu speichern
MM
0

Mit Johannes Schaubs Trick von std::make_move_iterator()mit std::experimental::make_array()können Sie eine Hilfsfunktion verwenden:

#include <memory>
#include <type_traits>
#include <vector>
#include <experimental/array>

struct X {};

template<class T, std::size_t N>
auto make_vector( std::array<T,N>&& a )
    -> std::vector<T>
{
    return { std::make_move_iterator(std::begin(a)), std::make_move_iterator(std::end(a)) };
}

template<class... T>
auto make_vector( T&& ... t )
    -> std::vector<typename std::common_type<T...>::type>
{
    return make_vector( std::experimental::make_array( std::forward<T>(t)... ) );
}

int main()
{
    using UX = std::unique_ptr<X>;
    const auto a  = std::experimental::make_array( UX{}, UX{}, UX{} ); // Ok
    const auto v0 = make_vector( UX{}, UX{}, UX{} );                   // Ok
    //const auto v1 = std::vector< UX >{ UX{}, UX{}, UX{} };           // !! Error !!
}

Sehen Sie es live weiter Coliru.

Vielleicht kann jemand den std::make_array()Trick nutzen, um make_vector()seine Sache direkt erledigen zu können, aber ich habe nicht gesehen, wie (genauer gesagt, ich habe versucht, was meiner Meinung nach funktionieren sollte, bin gescheitert und bin weitergegangen). In jedem Fall sollte der Compiler in der Lage sein, das Array in eine Vektortransformation zu integrieren, wie dies Clang bei eingeschaltetem O2 tut GodBolt.

Metall
quelle
-1

Wie bereits erwähnt, ist es nicht möglich, einen Vektor vom Typ "Nur Verschieben" mit einer Initialisierungsliste zu initialisieren. Die ursprünglich von @Johannes vorgeschlagene Lösung funktioniert einwandfrei, aber ich habe eine andere Idee ... Was ist, wenn wir kein temporäres Array erstellen und dann Elemente von dort in den Vektor verschieben, sondern die Platzierung verwendennew , um dieses Array bereits anstelle des zu initialisieren Vektorspeicherblock?

Hier ist meine Funktion zum Initialisieren eines Vektors von unique_ptr's mithilfe eines Argumentpakets:

#include <iostream>
#include <vector>
#include <make_unique.h>  /// @see http://stackoverflow.com/questions/7038357/make-unique-and-perfect-forwarding

template <typename T, typename... Items>
inline std::vector<std::unique_ptr<T>> make_vector_of_unique(Items&&... items) {
    typedef std::unique_ptr<T> value_type;

    // Allocate memory for all items
    std::vector<value_type> result(sizeof...(Items));

    // Initialize the array in place of allocated memory
    new (result.data()) value_type[sizeof...(Items)] {
        make_unique<typename std::remove_reference<Items>::type>(std::forward<Items>(items))...
    };
    return result;
}

int main(int, char**)
{
    auto testVector = make_vector_of_unique<int>(1,2,3);
    for (auto const &item : testVector) {
        std::cout << *item << std::endl;
    }
}
Gart
quelle
Das ist eine schreckliche Idee. Neu platzieren ist kein Hammer, sondern ein Werkzeug von feiner Präzision. result.data()ist kein Zeiger auf einen zufälligen Speicher. Es ist ein Zeiger auf ein Objekt . Denken Sie daran, was mit diesem armen Objekt passiert, wenn Sie es neu platzieren.
R. Martinho Fernandes
Darüber hinaus ist die Array-Form der Platzierung neu nicht wirklich verwendbar stackoverflow.com/questions/8720425/…
R. Martinho Fernandes
@R. Martinho Fernandes: Vielen Dank, dass Sie darauf hingewiesen haben, dass eine neue Platzierung für Arrays nicht funktionieren würde. Jetzt verstehe ich, warum das eine schlechte Idee war.
Gart