Mencuri sumber daya dari std :: kunci peta diizinkan?


15

Dalam C ++, apakah boleh mencuri sumber daya dari peta yang tidak saya perlukan lagi? Lebih tepatnya, anggap saya punya kunci std::mapwith std::stringdan saya ingin membuat vektor dengan mencuri sumber daya mapkunci s yang digunakan std::move. Perhatikan bahwa akses tulis ke kunci merusak struktur data internal (pemesanan kunci) maptapi saya tidak akan menggunakannya setelah itu.

Pertanyaan : Dapatkah saya melakukan ini tanpa masalah atau akankah ini menyebabkan bug yang tak terduga misalnya di destructor mapkarena saya mengaksesnya dengan cara yang std::maptidak dimaksudkan?

Berikut ini contoh programnya:

#include<map>
#include<string>
#include<vector>
#include<iostream>
using namespace std;
int main(int argc, char *argv[])
{
    std::vector<std::pair<std::string,double>> v;
    { // new scope to make clear that m is not needed 
      // after the resources were stolen
        std::map<std::string,double> m;
        m["aLongString"]=1.0;
        m["anotherLongString"]=2.0;
        //
        // now steal resources
        for (auto &p : m) {
            // according to my IDE, p has type 
            // std::pair<const class std::__cxx11::basic_string<char>, double>&
            cout<<"key before stealing: "<<p.first<<endl;
            v.emplace_back(make_pair(std::move(const_cast<string&>(p.first)),p.second));
            cout<<"key after stealing: "<<p.first<<endl;
        }
    }
    // now use v
    return 0;
}

Ini menghasilkan output:

key before stealing: aLongString
key after stealing: 
key before stealing: anotherLongString
key after stealing: 

EDIT: Saya ingin melakukan ini untuk seluruh konten peta besar dan menyimpan alokasi dinamis dengan mencuri sumber daya ini.


3
Apa tujuan dari "mencuri" ini? Untuk menghapus elemen dari peta? Lalu mengapa tidak hanya melakukan itu (menghapus elemen dari peta)? Juga, memodifikasi constnilai selalu UB.
Beberapa programmer Bung

rupanya itu akan menyebabkan bug serius!
rezaebrh

1
Bukan jawaban langsung untuk pertanyaan Anda, tetapi: Bagaimana jika Anda tidak mengembalikan vektor tetapi rentang atau sepasang iterator? Itu akan menghindari penyalinan sepenuhnya. Bagaimanapun, Anda perlu tolok ukur untuk melacak kemajuan pengoptimalan dan profiler untuk menemukan hotspot.
Ulrich Eckhardt

1
@ ALX23z Apakah Anda memiliki sumber untuk pernyataan ini. Saya tidak bisa membayangkan bagaimana menyalin pointer lebih mahal daripada menyalin seluruh wilayah memori.
Sebastian Hoffmann

1
@ SebastianHoffmann disebutkan pada CppCon baru-baru ini tidak yakin pada pembicaraan yang mana, tho. Masalahnya adalah std::stringmemiliki optimasi string pendek. Berarti ada beberapa logika non-sepele pada penyalinan dan pemindahan, bukan hanya penunjuk penunjuk dan sebagai tambahan sebagian besar waktu pemindahan menyiratkan penyalinan - jangan sampai Anda berurusan dengan string yang agak panjang. Perbedaan statistik kecil sekali dan secara umum pasti bervariasi tergantung pada jenis pemrosesan string yang dilakukan.
ALX23z

Jawaban:


18

Anda sedang melakukan perilaku yang tidak terdefinisi, gunakan const_castuntuk memodifikasi constvariabel. Jangan lakukan itu. Alasannya constadalah karena peta diurutkan berdasarkan kunci mereka. Jadi memodifikasi kunci di tempat adalah melanggar asumsi yang mendasari peta dibangun.

Anda tidak boleh menggunakan const_castuntuk menghapus constdari variabel dan memodifikasi variabel itu.

Bahwa menjadi kata, C ++ 17 memiliki solusi untuk masalah Anda: std::map's extractfungsi:

#include <map>
#include <string>
#include <vector>
#include <utility>

int main() {
  std::vector<std::pair<std::string, double>> v;
  std::map<std::string, double> m{{"aLongString", 1.0},
                                  {"anotherLongString", 2.0}};

  auto extracted_value = m.extract("aLongString");
  v.emplace_back(std::make_pair(std::move(extracted_value.key()),
                                std::move(extracted_value.mapped())));

  extracted_value = m.extract("anotherLongString");
  v.emplace_back(std::make_pair(std::move(extracted_value.key()),
                                std::move(extracted_value.mapped())));
}

Dan jangan using namespace std;. :)


Terima kasih, saya akan coba ini! Tapi apakah Anda yakin saya tidak bisa melakukan seperti yang saya lakukan? Maksud saya maptidak akan mengeluh jika saya tidak memanggil metode (yang tidak saya lakukan) dan mungkin pemesanan internal tidak penting dalam destruktornya?
phinz

2
Kunci peta dibuat const. Memutasi suatu constobjek adalah UB instan, terlepas dari apakah sesuatu benar-benar mengaksesnya setelahnya.
HTNW

Metode ini memiliki dua masalah: (1) Karena saya ingin mengekstrak semua elemen, saya tidak ingin mengekstrak dengan kunci (pencarian tidak efisien) tetapi dengan iterator. Saya telah melihat bahwa ini juga mungkin, jadi ini baik-baik saja. (2) Perbaiki saya jika saya salah tetapi untuk mengekstraksi semua elemen akan ada overhead yang sangat besar (untuk menyeimbangkan struktur pohon internal di setiap ekstraksi)?
phinz

2
@phinz Seperti yang dapat Anda lihat di cppreference extract saat menggunakan iterators karena argumen telah mengamortisasi kompleksitas konstan. Beberapa overhead tidak dapat dihindari, tetapi mungkin tidak akan cukup berarti. Jika Anda memiliki persyaratan khusus yang tidak tercakup oleh ini, maka Anda harus menerapkan sendiri mappersyaratan yang memenuhi ini. stdWadah dimaksudkan untuk aplikasi keperluan umum umum . Wadah tidak dioptimalkan untuk kasus penggunaan khusus.
kenari

@ HTNW Anda yakin kunci dibuat const? Dalam hal ini dapatkah Anda menunjukkan di mana argumentasi saya salah.
phinz

4

Kode Anda mencoba untuk memodifikasi constobjek, sehingga memiliki perilaku yang tidak terdefinisi, seperti yang ditunjukkan oleh jawaban druckermanly .

Beberapa jawaban lain ( phinz's dan Deuchie ) berpendapat bahwa kunci tersebut tidak boleh disimpan sebagai constobjek karena gagang simpul yang dihasilkan dari penggalian simpul keluar dari peta memungkinkan non- constakses ke kunci. Kesimpulan ini mungkin tampak masuk akal pada awalnya, tetapi P0083R3 , makalah yang memperkenalkan extractfungsi), memiliki bagian khusus pada topik ini yang membatalkan argumen ini:

Kekhawatiran

Beberapa keprihatinan telah dikemukakan tentang desain ini. Kami akan mengatasinya di sini.

Perilaku tidak terdefinisi

Bagian yang paling sulit dari proposal ini dari perspektif teoretis adalah fakta bahwa elemen yang diekstraksi mempertahankan tipe kunci utamanya. Ini mencegah keluar atau mengubahnya. Untuk mengatasi ini, kami telah menyediakan kunci fungsi accessor, yang menyediakan akses non-const untuk kunci dalam elemen yang diselenggarakan oleh pegangan simpul. Fungsi ini membutuhkan implementasi "sihir" untuk memastikannya berfungsi dengan benar di hadapan optimisasi kompiler. Salah satu cara untuk melakukan ini adalah dengan penyatuan pair<const key_type, mapped_type> dan pair<key_type, mapped_type>. Konversi antara ini dapat dilakukan secara aman menggunakan teknik yang mirip dengan yang digunakan std::launderpada ekstraksi dan pemasukan kembali.

Kami tidak merasa bahwa ini menimbulkan masalah teknis atau filosofis. Salah satu alasan Perpustakaan Standard ada adalah untuk menulis kode non-portabel dan magis bahwa klien tidak bisa menulis di portable C ++ (misalnya <atomic>, <typeinfo>, <type_traits>, dll). Ini hanyalah contoh lainnya. Semua yang diperlukan oleh vendor kompiler untuk menerapkan sihir ini adalah bahwa mereka tidak mengeksploitasi perilaku yang tidak terdefinisi dalam serikat untuk tujuan optimasi — dan saat ini kompiler sudah menjanjikan hal ini (sejauh itu sedang dimanfaatkan di sini).

Ini memberlakukan batasan pada klien bahwa, jika fungsi-fungsi ini digunakan, std::pairtidak dapat dikhususkan sedemikian sehingga pair<const key_type, mapped_type>memiliki tata letak yang berbeda dari pair<key_type, mapped_type>. Kami merasa kemungkinan siapa pun yang benar-benar ingin melakukan ini adalah nol, dan dalam kata-kata resmi kami membatasi spesialisasi apa pun dari pasangan ini.

Perhatikan bahwa fungsi anggota kunci adalah satu-satunya tempat di mana trik seperti itu diperlukan, dan bahwa tidak ada perubahan pada wadah atau pasangan diperlukan.

(penekanan milikku)


Ini sebenarnya adalah bagian dari jawaban untuk pertanyaan asli tetapi saya hanya bisa menerimanya.
phinz

0

Saya tidak berpikir itu const_castdan modifikasi mengarah pada perilaku yang tidak terdefinisi dalam kasus ini, tetapi tolong komentari apakah argumentasi ini benar.

Jawaban ini mengklaim itu

Dengan kata lain, Anda mendapatkan UB jika Anda memodifikasi objek const awalnya, dan sebaliknya tidak.

Jadi garis v.emplace_back(make_pair(std::move(const_cast<string&>(p.first)),p.second));dalam pertanyaan tidak mengarah ke UB jika dan hanya jika stringobjek p.firsttidak dibuat sebagai objek const. Sekarang perhatikan bahwa referensi tentangextract status

Mengekstrak sebuah simpul akan membatalkan iterator ke elemen yang diekstraksi. Pointer dan referensi ke elemen yang diekstraksi tetap valid, tetapi tidak dapat digunakan saat elemen dimiliki oleh node handle: mereka dapat digunakan jika elemen tersebut dimasukkan ke dalam wadah.

Jadi jika saya extractyang node_handlesesuai p, pterus tinggal di lokasi penyimpanannya. Tetapi setelah ekstraksi, saya diizinkan untuk movepergi sumber daya pseperti dalam kode jawaban druckermanly . Ini berarti bahwa pdan karenanya juga stringobjek p.firstitu tidak dibuat sebagai objek const awalnya.

Oleh karena itu, saya berpikir bahwa modifikasi mapkunci tidak mengarah ke UB dan dari jawaban Deuchie , tampaknya juga struktur pohon yang sekarang rusak (sekarang beberapa kunci string kosong yang sama) maptidak menyebabkan masalah pada destruktor. Jadi kode dalam pertanyaan harus bekerja dengan baik setidaknya di C ++ 17 di mana extractmetode ini ada (dan pernyataan tentang pointer sisa berlaku berlaku).

Memperbarui

Saya sekarang berpandangan bahwa jawaban ini salah. Saya tidak menghapusnya karena direferensikan oleh jawaban lain.


1
Maaf, phinz, jawaban Anda salah. Saya akan menulis jawaban untuk menjelaskan ini - ini ada hubungannya dengan serikat pekerja dan std::launder.
LF

-1

EDIT: Jawaban ini salah. Komentar yang baik menunjukkan kesalahan, tetapi saya tidak menghapusnya karena telah dirujuk dalam jawaban lain.

@druckermanly menjawab pertanyaan pertama Anda, yang mengatakan bahwa mengubah kunci mapsecara paksa merusak ketertiban di mana mapstruktur data internal dibuat (pohon Merah-Hitam). Tetapi aman untuk menggunakan extractmetode ini karena melakukan dua hal: memindahkan kunci dari peta dan kemudian menghapusnya, sehingga tidak mempengaruhi keteraturan peta sama sekali.

Pertanyaan lain yang Anda ajukan, apakah akan menimbulkan masalah ketika mendekonstruksi, bukan masalah. Ketika peta mendekonstruksi, ia akan memanggil dekonstruktor dari masing-masing elemennya (mapped_types dll.), Dan movemetode ini memastikan bahwa aman untuk mendekonstruksi kelas setelah dipindahkan. Jadi jangan khawatir. Singkatnya, itu adalah operasi moveyang memastikan bahwa aman untuk menghapus atau menugaskan kembali beberapa nilai baru ke kelas "pindah". Khusus untuk string, movemetode ini dapat menetapkan pointer char-nya nullptr, sehingga tidak menghapus data aktual yang dipindahkan ketika dekonstruktor dari kelas asli dipanggil.


Sebuah komentar mengingatkan saya pada poin yang saya abaikan, pada dasarnya dia benar tetapi ada satu hal yang saya tidak sepenuhnya setuju: const_castmungkin bukan UB. consthanyalah janji antara kompiler dan kami. objek yang dicatat sebagai constmasih merupakan objek, sama dengan yang tidak dengan const, dalam hal jenis dan representasi dalam bentuk biner. Ketika constdibuang, itu harus berperilaku seolah-olah itu adalah kelas yang bisa berubah normal. Sehubungan dengan move, Jika Anda ingin menggunakannya, Anda harus lulus &bukan const &, jadi karena saya melihat itu bukan UB, itu hanya melanggar janji constdan memindahkan data.

Saya juga melakukan dua percobaan, menggunakan MSVC 14.24.28314 dan Dentang 9.0.0 masing-masing, dan mereka menghasilkan hasil yang sama.

map<string, int> m;
m.insert({ "test", 2 });
m.insert({ "this should be behind the 'test' string.", 3 });
m.insert({ "and this should be in front of the 'test' string.", 1 });
string let_me_USE_IT = std::move(const_cast<string&>(m.find("test")->first));
cout << let_me_USE_IT << '\n';
for (auto const& i : m) {
    cout << i.first << ' ' << i.second << '\n';
}

keluaran:

test
and this should be in front of the 'test' string. 1
 2
this should be behind the 'test' string. 3

Kita dapat melihat bahwa string '2' kosong sekarang, tetapi jelas kami telah melanggar ketertiban peta karena string kosong harus ditempatkan kembali ke depan. Jika kami mencoba menyisipkan, menemukan atau menghapus beberapa titik peta tertentu, itu dapat menyebabkan bencana.

Bagaimanapun, kami mungkin sepakat bahwa itu tidak pernah merupakan ide yang baik untuk memanipulasi data dalam dari setiap kelas yang melewati antarmuka publik mereka. The find, insert, removefungsi dan sebagainya mengandalkan kebenaran mereka pada keteraturan struktur data batin, dan itulah alasan mengapa kita harus tinggal jauh dari pikiran mengintip ke dalam.


2
"Seperti apakah itu akan menyebabkan masalah ketika mendekonstruksi, bukan masalah." Secara teknis benar, karena perilaku tidak terdefinisi (perubahan constnilai) terjadi sebelumnya. Namun, argumen " move[fungsi] memastikan bahwa aman untuk mendekonstruksi [objek dari] kelas setelah dipindahkan" tidak berlaku: Anda tidak dapat dengan aman berpindah dari constobjek / referensi, karena itu membutuhkan modifikasi, yang constmencegah. Anda dapat mencoba untuk mengatasi keterbatasan itu dengan menggunakan const_cast, tetapi pada titik itu Anda yang terbaik akan masuk ke perilaku implementasi yang sangat spesifik, jika tidak UB.
hoffmale

@ Hoffmale Terima kasih, saya mengabaikan dan membuat kesalahan besar. Jika bukan Anda yang menunjukkannya, jawaban saya di sini mungkin menyesatkan orang lain. Sebenarnya saya harus mengatakan bahwa movefungsi mengambil &alih - alih a const&, jadi jika seseorang bersikeras bahwa dia memindahkan kunci dari peta, dia harus menggunakannya const_cast.
Deuchie

1
"Objek yang dicatat sebagai const masih merupakan objek, sama seperti yang tidak dengan const, dalam hal jenis dan representasi dalam bentuk biner" Tidak. Objek const dapat dimasukkan ke dalam memori read only. Juga, const memungkinkan kompilator untuk alasan tentang kode dan cache nilai daripada menghasilkan kode untuk banyak pembacaan (yang dapat membuat perbedaan besar dalam hal kinerja). Jadi UB disebabkan oleh const_castakan menjengkelkan. Ini mungkin bekerja sebagian besar waktu, tetapi pecahkan kode Anda dengan cara yang halus.
LF

Tetapi di sini saya berpikir bahwa kita dapat memastikan bahwa objek tidak dimasukkan ke dalam memori read only karena setelah itu extract, kita diperbolehkan untuk bergerak dari objek yang sama, kan? (lihat jawaban saya)
phinz

1
Maaf, analisis dalam jawaban Anda salah. Saya akan menulis jawaban untuk menjelaskan ini - ini ada hubungannya dengan serikat pekerja danstd::launder
LF
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.