initializer_list dan pindahkan semantik

98

Apakah saya diperbolehkan untuk memindahkan elemen dari a std::initializer_list<T>?

#include <initializer_list>
#include <utility>

template<typename T>
void foo(std::initializer_list<T> list)
{
    for (auto it = list.begin(); it != list.end(); ++it)
    {
        bar(std::move(*it));   // kosher?
    }
}

Karena std::intializer_list<T>memerlukan perhatian compiler khusus dan tidak memiliki nilai semantik seperti container normal dari pustaka standar C ++, saya lebih suka aman daripada menyesal dan bertanya.

fredoverflow
sumber
Bahasa inti mendefinisikan bahwa objek yang dirujuk oleh initializer_list<T>adalah non -const. Seperti, initializer_list<int>mengacu pada intobjek. Tapi saya pikir itu adalah cacat - itu dimaksudkan agar kompiler dapat mengalokasikan daftar secara statis dalam memori hanya baca.
Johannes Schaub - litb

Jawaban:

90

Tidak, itu tidak akan berfungsi sebagaimana mestinya; Anda masih akan mendapatkan salinannya. Saya cukup terkejut dengan ini, karena saya pikir yang initializer_listada untuk menyimpan sejumlah temporer sampai mereka movemati.

begindan endsebagai initializer_listgantinya const T *, jadi hasil dari movekode Anda adalah T const &&- referensi nilai r yang tidak dapat diubah. Ekspresi seperti itu tidak dapat dipindahkan secara berarti. Ini akan mengikat ke parameter fungsi bertipe T const &karena nilai r memang mengikat ke referensi nilai konstanta, dan Anda masih akan melihat semantik salin.

Mungkin alasannya adalah agar kompiler dapat memilih untuk membuat initializer_listkonstanta yang diinisialisasi secara statis, tetapi tampaknya akan lebih bersih untuk membuat tipenya initializer_listatau const initializer_listatas kebijaksanaan kompiler, sehingga pengguna tidak tahu apakah mengharapkan constatau bisa berubah hasil dari begindan end. Tapi itu hanya firasat saya, mungkin ada alasan bagus mengapa saya salah.

Pembaruan: Saya telah menulis proposal ISO untuk initializer_listdukungan tipe bergerak-saja. Ini hanya draf pertama, dan belum diterapkan di mana pun, tetapi Anda dapat melihatnya untuk analisis masalah lebih lanjut.

Potatoswatter
sumber
11
Jika tidak jelas, tetap berarti penggunaan std::moveitu aman, jika tidak produktif. (Pembatasan T const&&memindahkan konstruktor.)
Luc Danton
Saya tidak berpikir bahwa Anda dapat membuat seluruh argumen baik const std::initializer_list<T>atau hanya std::initializer_list<T>dengan cara yang tidak terlalu sering menimbulkan kejutan. Pertimbangkan bahwa setiap argumen di initializer_listdapat constatau tidak dan yang dikenal dalam konteks pemanggil, tetapi kompilator harus menghasilkan hanya satu versi kode dalam konteks callee (yaitu di foodalamnya tidak tahu apa-apa tentang argumen bahwa penelepon sedang lewat)
David Rodríguez - dribeas
1
@David: Poin yang bagus, tetapi akan berguna jika std::initializer_list &&kelebihan beban melakukan sesuatu, bahkan jika kelebihan beban non-referensi juga diperlukan. Saya kira itu akan lebih membingungkan daripada situasi saat ini, yang sudah buruk.
Potatoswatter
1
@JBJansen Itu tidak bisa diretas. Saya tidak melihat dengan tepat kode apa yang seharusnya dicapai wrt initializer_list, tetapi sebagai pengguna, Anda tidak memiliki izin yang diperlukan untuk berpindah darinya. Kode aman tidak akan melakukannya.
Potatoswatter
1
@Potatoswatter, telat berkomentar, tapi bagaimana status lamarannya. Apakah ada kemungkinan kecil itu bisa menjadi C ++ 20?
WhiZTiM
20
bar(std::move(*it));   // kosher?

Tidak seperti yang Anda inginkan. Anda tidak dapat memindahkan constobjek. Dan std::initializer_listhanya menyediakan constakses ke elemennya. Jadi jenis ityaitu const T *.

Upaya Anda untuk menelepon std::move(*it)hanya akan menghasilkan nilai l. YAITU: salinan.

std::initializer_listreferensi memori statis . Itulah tujuan kelas. Anda tidak dapat berpindah dari memori statis, karena gerakan berarti mengubahnya. Anda hanya dapat menyalin darinya.

Nicol Bolas
sumber
Nilai x const masih merupakan nilai x, dan initializer_listmereferensikan tumpukan jika diperlukan. (Jika isinya tidak konstan, itu masih thread-safe.)
Potatoswatter
5
@Potatoswatter: Anda tidak dapat berpindah dari objek konstan. The initializer_listobyek itu sendiri mungkin merupakan Xvalue, tapi isinya (array sebenarnya nilai-nilai yang menunjuk ke) adalah const, karena mereka isinya mungkin nilai-nilai statis. Anda tidak dapat berpindah dari konten file initializer_list.
Nicol Bolas
Lihat jawaban saya dan pembahasannya. Dia telah memindahkan iterator dereferensi, menghasilkan nilai constx. movemungkin tidak ada artinya, tetapi legal dan bahkan mungkin untuk mendeklarasikan parameter yang menerima hal itu. Jika pemindahan jenis tertentu terjadi tanpa operasi, itu bahkan mungkin bekerja dengan benar.
Potatoswatter
1
@Potatoswatter: Standar C ++ 11 menggunakan banyak bahasa untuk memastikan bahwa objek non-sementara tidak benar-benar dipindahkan kecuali Anda menggunakannya std::move. Ini memastikan bahwa Anda dapat mengetahui dari pemeriksaan saat operasi pemindahan terjadi, karena ini memengaruhi sumber dan tujuan (Anda tidak ingin hal itu terjadi secara implisit untuk objek bernama). Karena itu, jika Anda menggunakan std::movedi tempat di mana operasi pemindahan tidak terjadi (dan tidak ada gerakan aktual yang akan terjadi jika Anda memiliki nilai constx), maka kodenya menyesatkan. Saya pikir itu adalah kesalahan untuk std::movedipanggil pada suatu constobjek.
Nicol Bolas
1
Mungkin, tapi saya masih akan mengambil lebih sedikit pengecualian untuk aturan atas kemungkinan kode yang menyesatkan. Bagaimanapun, itulah mengapa saya menjawab "tidak" meskipun legal, dan hasilnya adalah nilai x meskipun hanya akan mengikat sebagai nilai konstanta. Sejujurnya, saya sudah memiliki godaan singkat const &&di kelas yang mengumpulkan sampah dengan pointer terkelola, di mana semua yang relevan dapat berubah dan memindahkan memindahkan manajemen penunjuk tetapi tidak memengaruhi nilai yang terkandung. Selalu ada kasus tepi yang rumit: v).
Potatoswatter
2

Ini tidak akan berfungsi seperti yang dinyatakan, karena list.begin()memiliki tipe const T *, dan tidak mungkin Anda dapat berpindah dari objek konstan. Perancang bahasa mungkin membuatnya sedemikian rupa untuk memungkinkan daftar penginisialisasi berisi, misalnya konstanta string, yang darinya tidak pantas untuk dipindahkan.

Namun, jika Anda berada dalam situasi di mana Anda tahu bahwa daftar penginisialisasi berisi ekspresi rvalue (atau Anda ingin memaksa pengguna untuk menuliskannya) maka ada trik yang akan membuatnya bekerja (saya terinspirasi oleh jawaban Sumant untuk ini, tetapi solusinya jauh lebih sederhana dari yang itu). Anda memerlukan elemen yang disimpan dalam daftar penginisialisasi bukan Tnilai, tetapi nilai yang merangkum T&&. Kemudian, meskipun nilai itu sendiri constmemenuhi syarat, mereka masih dapat mengambil rvalue yang dapat dimodifikasi.

template<typename T>
  class rref_capture
{
  T* ptr;
public:
  rref_capture(T&& x) : ptr(&x) {}
  operator T&& () const { return std::move(*ptr); } // restitute rvalue ref
};

Sekarang, alih-alih mendeklarasikan initializer_list<T>argumen, Anda mendeklarasikan initializer_list<rref_capture<T> >argumen. Berikut adalah contoh konkret, yang melibatkan vektor std::unique_ptr<int>pointer pintar, yang hanya mendefinisikan semantik bergerak (jadi objek ini sendiri tidak akan pernah bisa disimpan dalam daftar penginisialisasi); namun daftar penginisialisasi di bawah ini dapat dikompilasi tanpa masalah.

#include <memory>
#include <initializer_list>
class uptr_vec
{
  typedef std::unique_ptr<int> uptr; // move only type
  std::vector<uptr> data;
public:
  uptr_vec(uptr_vec&& v) : data(std::move(v.data)) {}
  uptr_vec(std::initializer_list<rref_capture<uptr> > l)
    : data(l.begin(),l.end())
  {}
  uptr_vec& operator=(const uptr_vec&) = delete;
  int operator[] (size_t index) const { return *data[index]; }
};

int main()
{
  std::unique_ptr<int> a(new int(3)), b(new int(1)),c(new int(4));
  uptr_vec v { std::move(a), std::move(b), std::move(c) };
  std::cout << v[0] << "," << v[1] << "," << v[2] << std::endl;
}

Satu pertanyaan memang membutuhkan jawaban: jika elemen dari daftar penginisialisasi harus merupakan prvalues ​​yang benar (dalam contoh ini adalah xvalues), apakah bahasa memastikan bahwa masa pakai temporer yang sesuai meluas ke titik di mana mereka digunakan? Sejujurnya, menurut saya bagian 8.5 dari standar yang relevan tidak membahas masalah ini sama sekali. Namun, membaca 1.9: 10, tampaknya ekspresi lengkap yang relevan dalam semua kasus mencakup penggunaan daftar penginisialisasi, jadi saya rasa tidak ada bahaya menggantung referensi nilai r.

Marc van Leeuwen
sumber
Konstanta string? Suka "Hello world"? Jika Anda berpindah dari mereka, Anda cukup menyalin sebuah pointer (atau mengikat referensi).
dyp
1
"Satu pertanyaan memang membutuhkan jawaban" Penginisialisasi di dalamnya {..}terikat ke referensi dalam parameter fungsi rref_capture. Ini tidak memperpanjang masa hidup mereka, mereka masih hancur pada akhir ekspresi penuh saat mereka diciptakan.
dyp
Sesuai dengan komentar TC dari jawaban lain: Jika Anda memiliki beberapa kelebihan konstruktor, bungkus std::initializer_list<rref_capture<T>>dalam beberapa sifat transformasi yang Anda pilih - katakanlah, std::decay_t- untuk memblokir deduksi yang tidak diinginkan.
Unslander Monica
2

Saya pikir mungkin bermanfaat untuk menawarkan titik awal yang masuk akal untuk sebuah solusi.

Komentar sebaris.

#include <memory>
#include <vector>
#include <array>
#include <type_traits>
#include <algorithm>
#include <iterator>

template<class Array> struct maker;

// a maker which makes a std::vector
template<class T, class A>
struct maker<std::vector<T, A>>
{
  using result_type = std::vector<T, A>;

  template<class...Ts>
  auto operator()(Ts&&...ts) const -> result_type
  {
    result_type result;
    result.reserve(sizeof...(Ts));
    using expand = int[];
    void(expand {
      0,
      (result.push_back(std::forward<Ts>(ts)),0)...
    });

    return result;
  }
};

// a maker which makes std::array
template<class T, std::size_t N>
struct maker<std::array<T, N>>
{
  using result_type = std::array<T, N>;

  template<class...Ts>
  auto operator()(Ts&&...ts) const
  {
    return result_type { std::forward<Ts>(ts)... };
  }

};

//
// delegation function which selects the correct maker
//
template<class Array, class...Ts>
auto make(Ts&&...ts)
{
  auto m = maker<Array>();
  return m(std::forward<Ts>(ts)...);
}

// vectors and arrays of non-copyable types
using vt = std::vector<std::unique_ptr<int>>;
using at = std::array<std::unique_ptr<int>,2>;


int main(){
    // build an array, using make<> for consistency
    auto a = make<at>(std::make_unique<int>(10), std::make_unique<int>(20));

    // build a vector, using make<> because an initializer_list requires a copyable type  
    auto v = make<vt>(std::make_unique<int>(10), std::make_unique<int>(20));
}
Richard Hodges
sumber
Pertanyaannya adalah apakah initializer_listdapat dipindahkan dari, bukan apakah ada yang punya solusi. Selain itu, nilai jual utama dari initializer_listadalah bahwa itu hanya template pada jenis elemen, bukan jumlah elemen, dan oleh karena itu tidak mengharuskan penerima untuk juga diberi template - dan ini benar-benar kehilangan itu.
underscore_d
1
@underscore Anda memang benar. Saya berpandangan bahwa berbagi pengetahuan terkait pertanyaan itu sendiri adalah hal yang baik. Dalam kasus ini, mungkin itu membantu OP dan mungkin tidak - dia tidak menanggapi. Namun lebih sering daripada tidak, OP dan lainnya menyambut materi tambahan terkait pertanyaan tersebut.
Richard Hodges
Tentu, ini memang membantu bagi pembaca yang menginginkan sesuatu seperti initializer_listtetapi tidak tunduk pada semua kendala yang membuatnya berguna. :)
underscore_d
@underscore_d batasan mana yang telah saya abaikan?
Richard Hodges
Yang saya maksud adalah bahwa initializer_list(melalui sihir kompilator) menghindari keharusan fungsi templat pada jumlah elemen, sesuatu yang secara inheren dibutuhkan oleh alternatif berdasarkan array dan / atau fungsi variadik, sehingga membatasi rentang kasus di mana yang terakhir dapat digunakan. Menurut pemahaman saya, ini justru salah satu alasan utama untuk memiliki initializer_list, jadi sepertinya perlu disebutkan.
underscore_d
0

Sepertinya tidak diperbolehkan dalam standar saat ini karena sudah dijawab . Berikut adalah solusi lain untuk mencapai sesuatu yang serupa, dengan mendefinisikan fungsi sebagai variadic alih-alih mengambil daftar penginisialisasi.

#include <vector>
#include <utility>

// begin helper functions

template <typename T>
void add_to_vector(std::vector<T>* vec) {}

template <typename T, typename... Args>
void add_to_vector(std::vector<T>* vec, T&& car, Args&&... cdr) {
  vec->push_back(std::forward<T>(car));
  add_to_vector(vec, std::forward<Args>(cdr)...);
}

template <typename T, typename... Args>
std::vector<T> make_vector(Args&&... args) {
  std::vector<T> result;
  add_to_vector(&result, std::forward<Args>(args)...);
  return result;
}

// end helper functions

struct S {
  S(int) {}
  S(S&&) {}
};

void bar(S&& s) {}

template <typename T, typename... Args>
void foo(Args&&... args) {
  std::vector<T> args_vec = make_vector<T>(std::forward<Args>(args)...);
  for (auto& arg : args_vec) {
    bar(std::move(arg));
  }
}

int main() {
  foo<S>(S(1), S(2), S(3));
  return 0;
}

Template variadic dapat menangani referensi nilai-r dengan tepat, tidak seperti initializer_list.

Dalam kode contoh ini, saya menggunakan satu set fungsi pembantu kecil untuk mengubah argumen variadic menjadi vektor, agar mirip dengan kode aslinya. Tapi tentu saja Anda bisa menulis fungsi rekursif dengan template variadic secara langsung.

Hiroshi Ichikawa
sumber
Pertanyaannya adalah apakah initializer_listdapat dipindahkan dari, bukan apakah ada yang punya solusi. Selain itu, nilai jual utama dari initializer_listadalah bahwa itu hanya template pada jenis elemen, bukan jumlah elemen, dan oleh karena itu tidak mengharuskan penerima untuk juga diberi template - dan ini benar-benar kehilangan itu.
underscore_d
0

Saya memiliki implementasi yang lebih sederhana yang menggunakan kelas pembungkus yang bertindak sebagai tag untuk menandai maksud pemindahan elemen. Ini adalah biaya waktu kompilasi.

Kelas pembungkus dirancang untuk digunakan dengan cara std::moveyang digunakan, cukup ganti std::movedengan move_wrapper, tetapi ini membutuhkan C ++ 17. Untuk spesifikasi yang lebih lama, Anda dapat menggunakan metode pembuat tambahan.

Anda harus menulis metode / konstruktor pembangun yang menerima kelas pembungkus di dalamnya initializer_listdan memindahkan elemen yang sesuai.

Jika Anda memerlukan beberapa elemen untuk disalin alih-alih dipindahkan, buat salinan sebelum meneruskannya initializer_list.

Kode tersebut harus didokumentasikan sendiri.

#include <iostream>
#include <vector>
#include <initializer_list>

using namespace std;

template <typename T>
struct move_wrapper {
    T && t;

    move_wrapper(T && t) : t(move(t)) { // since it's just a wrapper for rvalues
    }

    explicit move_wrapper(T & t) : t(move(t)) { // acts as std::move
    }
};

struct Foo {
    int x;

    Foo(int x) : x(x) {
        cout << "Foo(" << x << ")\n";
    }

    Foo(Foo const & other) : x(other.x) {
        cout << "copy Foo(" << x << ")\n";
    }

    Foo(Foo && other) : x(other.x) {
        cout << "move Foo(" << x << ")\n";
    }
};

template <typename T>
struct Vec {
    vector<T> v;

    Vec(initializer_list<T> il) : v(il) {
    }

    Vec(initializer_list<move_wrapper<T>> il) {
        v.reserve(il.size());
        for (move_wrapper<T> const & w : il) {
            v.emplace_back(move(w.t));
        }
    }
};

int main() {
    Foo x{1}; // Foo(1)
    Foo y{2}; // Foo(2)

    Vec<Foo> v{Foo{3}, move_wrapper(x), Foo{y}}; // I want y to be copied
    // Foo(3)
    // copy Foo(2)
    // move Foo(3)
    // move Foo(1)
    // move Foo(2)
}
bumfo
sumber
0

Daripada menggunakan a std::initializer_list<T>, Anda bisa mendeklarasikan argumen Anda sebagai referensi nilai r array:

template <typename T>
void bar(T &&value);

template <typename T, size_t N>
void foo(T (&&list)[N] ) {
   std::for_each(std::make_move_iterator(std::begin(list)),
                 std::make_move_iterator(std::end(list)),
                 &bar);
}

void baz() {
   foo({std::make_unique<int>(0), std::make_unique<int>(1)});
}

Lihat contoh menggunakan std::unique_ptr<int>: https://gcc.godbolt.org/z/2uNxv6

Jorge Bellon
sumber
-1

Pertimbangkan in<T>idiom yang dijelaskan di cpptruths . Idenya adalah untuk menentukan lvalue / rvalue pada saat run-time dan kemudian memanggil move atau copy-construction. in<T>akan mendeteksi rvalue / lvalue meskipun antarmuka standar yang disediakan oleh initializer_list adalah referensi const.

Sumant
sumber
4
Mengapa Anda ingin menentukan kategori nilai pada waktu proses ketika kompilator sudah mengetahuinya?
fredoverflow
1
Silakan baca blog dan tinggalkan komentar jika Anda tidak setuju atau memiliki alternatif yang lebih baik. Meskipun kompilator mengetahui kategori nilai, initializer_list tidak menyimpannya karena ia hanya memiliki iterator const. Jadi, Anda perlu "menangkap" kategori nilai saat membuat initializer_list dan meneruskannya sehingga fungsi dapat menggunakannya sesuka hati.
Sumant
5
Jawaban ini pada dasarnya tidak berguna tanpa mengikuti tautan, dan jawaban SO akan berguna tanpa mengikuti tautan.
Yakk - Adam Nevraumont
1
@ Sumant [menyalin komentar saya dari pos yang sama di tempat lain] Apakah kekacauan yang sangat besar itu benar-benar memberikan manfaat yang terukur untuk kinerja atau penggunaan memori, dan jika demikian, sejumlah besar manfaat tersebut untuk mengimbangi secara memadai betapa mengerikan tampilannya dan fakta bahwa itu membutuhkan waktu sekitar satu jam untuk mencari tahu apa yang coba dilakukannya? Saya agak meragukannya.
underscore_d