Dapatkah saya menginisialisasi daftar vektor tipe hanya bergerak?

95

Jika saya meneruskan kode berikut melalui snapshot GCC 4.7 saya, ia mencoba menyalin unique_ptrs ke dalam vektor.

#include <vector>
#include <memory>

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

Jelas itu tidak dapat berfungsi karena std::unique_ptrtidak dapat disalin:

kesalahan: penggunaan fungsi yang dihapus 'std :: unique_ptr <_Tp, _Dp> :: unique_ptr (const std :: unique_ptr <_Tp, _Dp> &) [dengan _Tp = int; _Dp = std :: default_delete; std :: unique_ptr <_Tp, _Dp> = std :: unique_ptr] '

Apakah GCC benar dalam mencoba menyalin petunjuk dari daftar penginisialisasi?

R. Martinho Fernandes
sumber
Visual Studio dan dentang memiliki perilaku yang sama
Jean-Simon Brochu

Jawaban:

46

Sinopsis <initializer_list>dalam 18.9 memperjelas bahwa elemen dari daftar penginisialisasi selalu diteruskan melalui referensi-konst. Sayangnya, tampaknya tidak ada cara untuk menggunakan semantik bergerak dalam elemen daftar penginisialisasi dalam revisi bahasa saat ini.

Secara khusus, kami memiliki:

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
sumber
4
Pertimbangkan idiom dalam <T> yang dijelaskan di cpptruths ( cpptruths.blogspot.com/2013/09/… ). Idenya adalah untuk menentukan lvalue / rvalue pada saat run-time dan kemudian memanggil move atau copy-construction. di <T> akan mendeteksi rvalue / lvalue meskipun antarmuka standar yang disediakan oleh initializer_list adalah referensi const.
Sumant
3
@ Sumant Tampaknya tidak begitu "idiomatis bagi saya": bukankah itu, UB murni? tidak hanya sebagai iterator melainkan elemen yang mendasarinya sendiri const, yang tidak dapat dibuang dalam program yang dibentuk dengan baik.
underscore_d
63

Sunting: Karena @Johannes sepertinya tidak ingin memposting solusi terbaik sebagai jawaban, saya akan melakukannya.

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

Iterator yang dikembalikan oleh std::make_move_iteratorakan memindahkan elemen menunjuk ke saat didereferensi.


Jawaban asli: Kami akan menggunakan tipe pembantu kecil di sini:

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

Sayangnya, kode langsung di sini tidak akan berfungsi:

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

Karena standar, untuk alasan apa pun, tidak mendefinisikan konstruktor salinan konversi seperti ini:

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

Yang initializer_list<rref_wrapper<move_only>>dibuat oleh brace-init-list ( {...}) tidak akan mengonversi menjadi initializer_list<move_only>yang vector<move_only>diambil. Jadi kita membutuhkan inisialisasi dua langkah di sini:

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
sumber
1
Ah ... ini adalah analog nilai r std::ref, bukan? Mungkin harus dipanggil std::rref.
Kerrek SB
17
Sekarang, saya rasa ini tidak boleh dibiarkan tanpa disebutkan dalam komentar :) 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: Terkadang, solusi sederhana yang luput dari saya. Meskipun saya harus mengakui, saya belum peduli dengan itu move_iterator.
Xeo
2
@Johannes: Juga, mengapa itu bukan jawaban? :)
Xeo
1
@JohanLundberg: Saya akan menganggap itu sebagai masalah QoI, tetapi saya tidak mengerti mengapa hal itu tidak dapat dilakukan. Stdlib VC ++ misalnya tag-dispatch berdasarkan kategori iterator dan digunakan std::distanceuntuk meneruskan-atau-lebih baik iterator dan std::move_iteratormenyesuaikan kategori iterator yang mendasarinya. Bagaimanapun, solusi yang bagus dan ringkas. Posting itu sebagai jawaban, mungkin?
Xeo
10

Seperti yang disebutkan dalam jawaban lain, perilaku dari std::initializer_listadalah memegang benda berdasarkan nilai dan tidak membiarkan bergerak keluar, jadi ini tidak mungkin. Berikut ini satu solusi yang mungkin, menggunakan pemanggilan fungsi di mana penginisialisasi diberikan sebagai argumen variadic:

#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{});
}

Sayangnya multi_emplace(foos, {});gagal karena tidak dapat menyimpulkan tipe untuk {}, jadi untuk objek yang akan dibangun secara default Anda harus mengulangi nama kelas. (atau gunakan vector::resize)

MM
sumber
4
Perluasan paket rekursif dapat diganti dengan peretasan dummy array comma operator, untuk menghemat beberapa baris kode
MM
0

Menggunakan trik Johannes Schaub std::make_move_iterator()dengan std::experimental::make_array(), Anda dapat menggunakan fungsi helper:

#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 !!
}

Lihat langsung Coliru.

Mungkin seseorang dapat memanfaatkan std::make_array()tipu daya untuk memungkinkan make_vector()melakukan sesuatu secara langsung, tetapi saya tidak melihat bagaimana (lebih tepatnya, saya mencoba apa yang saya pikir harus berhasil, gagal, dan pindah). Dalam kasus apa pun, compiler harus dapat melakukan transformasi array ke vektor, seperti yang dilakukan Clang dengan O2 aktif GodBolt.

logam
sumber
-1

Seperti yang telah ditunjukkan, tidak mungkin untuk menginisialisasi vektor tipe hanya bergerak dengan daftar penginisialisasi. Solusi yang awalnya diusulkan oleh @Johannes berfungsi dengan baik, tetapi saya punya ide lain ... Bagaimana jika kita tidak membuat array sementara dan kemudian memindahkan elemen dari sana ke vektor, tetapi menggunakan penempatan newuntuk menginisialisasi array ini yang sudah menggantikan blok memori vektor?

Inilah fungsi saya untuk menginisialisasi vektor unique_ptrmenggunakan paket argumen:

#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
sumber
Itu ide yang buruk. Penempatan baru bukanlah palu, ini adalah alat dengan presisi yang bagus. result.data()bukan penunjuk ke beberapa memori acak. Ini adalah penunjuk ke suatu objek . Pikirkan apa yang terjadi pada objek malang itu ketika Anda meletakkan yang baru di atasnya.
R. Martinho Fernandes
Selain itu, bentuk larik penempatan baru tidak benar-benar dapat digunakan stackoverflow.com/questions/8720425/…
R. Martinho Fernandes
@R. Martinho Fernandes: terima kasih telah menunjukkan bahwa penempatan baru untuk array tidak akan berfungsi. Sekarang saya mengerti mengapa itu ide yang buruk.
Gart