Dalam C ++, programmer harus memutuskan bagaimana sumber daya yang digunakan akan dibebaskan, tidak ada alat otomatis seperti pengumpul sampah. Artikel ini membahas kemungkinan solusi untuk masalah ini, memeriksa secara terperinci potensi masalah, serta sejumlah masalah terkait.
Daftar isi
Pendahuluan
Manajemen sumber daya adalah sesuatu yang harus dilakukan oleh seorang programmer C ++ sepanjang waktu. Sumber daya termasuk blok memori, objek kernel OS, kunci multi-threaded, koneksi jaringan, koneksi database, dan sembarang objek yang dibuat dalam memori dinamis. Akses ke sumber daya adalah melalui deskriptor, tipe deskriptor biasanya berupa pointer atau salah satu aliasnya ( HANDLE
, dll.), Terkadang keseluruhan (deskriptor file UNIX). Setelah menggunakan sumber daya, Anda harus melepaskannya, jika tidak cepat atau lambat suatu aplikasi yang tidak melepaskan sumber daya (dan mungkin aplikasi lain) akan kehabisan sumber daya. Masalah ini sangat akut, kita dapat mengatakan bahwa salah satu fitur utama dari .NET, Java, dan beberapa platform lainnya adalah sistem manajemen sumber daya terpadu berdasarkan pengumpulan sampah.
Fitur berorientasi objek dari C ++ secara alami mengarah ke solusi berikut: kelas yang mengelola sumber daya berisi deskriptor sumber daya sebagai anggota, menginisialisasi deskriptor ketika sumber daya ditangkap, dan membebaskan sumber daya dalam destruktor. Tetapi setelah beberapa pemikiran (atau pengalaman) muncul pemahaman bahwa itu tidak begitu sederhana. Dan masalah utama adalah semantik menyalin. Jika kelas yang mengelola sumber daya menggunakan copy constructor yang dihasilkan oleh compiler default, maka setelah menyalin objek kita akan mendapatkan dua salinan pegangan dari sumber daya yang sama. Jika satu objek membebaskan sumber daya, maka setelah itu objek kedua akan dapat mencoba menggunakan atau membebaskan sumber daya yang sudah dibebaskan, yang dalam hal apa pun tidak benar dan dapat mengarah pada perilaku yang tidak terdefinisi, yaitu, apa pun bisa terjadi, misalnya, penghentian program yang tidak normal.
Untungnya, dalam C ++, seorang programmer dapat sepenuhnya mengendalikan proses penyalinan dengan mendefinisikan seorang copy constructor dan operator penugasan sendiri, yang memungkinkan kita untuk menyelesaikan masalah di atas, dan biasanya tidak dengan satu cara. Implementasi penyalinan harus terkait erat dengan mekanisme pembebasan sumber daya, dan kami secara kolektif akan menyebutnya sebagai strategi penyalinan kepemilikan. Apa yang disebut "aturan Tiga Besar" dikenal luas, yang menyatakan bahwa jika seorang programmer mendefinisikan setidaknya satu dari tiga operasi - copy constructor, copy copy operator atau destructor - maka ia harus mendefinisikan ketiga operasi. Strategi kepemilikan salinan hanya menentukan bagaimana melakukan ini. Ada empat strategi dasar kepemilikan salinan.
1. Strategi kepemilikan salinan dasar
Sebelum penangkapan sumber daya atau setelah rilis, deskriptor harus mengambil nilai khusus yang menunjukkan bahwa itu tidak terkait dengan sumber daya tersebut. Biasanya ini nol, terkadang -1, dilemparkan ke tipe deskriptor. Bagaimanapun, deskriptor seperti itu akan disebut nol. Kelas yang mengelola sumber daya harus mengenali deskriptor nol dan tidak mencoba menggunakan atau membebaskan sumber daya dalam kasus ini.
1.1. Salin Strategi Larangan
Ini adalah strategi paling sederhana. Dalam hal ini, hanya dilarang menyalin dan menetapkan instance kelas. Destuctor membebaskan sumber daya yang ditangkap. Dalam C ++, untuk melarang penyalinan tidaklah sulit, kelas harus mendeklarasikan, tetapi tidak mendefinisikan, konstruktor penyalinan tertutup dan operator penyalinan salinan.
class X { private: X(const X&); X& operator=(const X&);
Upaya penyalinan digagalkan oleh kompiler dan tautan.
Standar C ++ 11 menawarkan sintaks khusus untuk kasus ini:
class X { public: X(const X&) = delete; X& operator=(const X&) = delete;
Sintaks ini lebih visual dan memberikan pesan yang lebih mudah dipahami kepada kompiler ketika mencoba menyalin.
Dalam versi sebelumnya dari perpustakaan standar (C ++ 98), kelas input / output stream ( std::fstream
, dll.) CFile
strategi copy-ban, dan pada Windows, banyak kelas dari MFC ( CFile
, CEvent
, CMutex
, dll.). Di pustaka standar C ++ 11, beberapa kelas menggunakan strategi ini untuk mendukung sinkronisasi multi-utas.
1.2. Strategi Kepemilikan Eksklusif
Dalam hal ini, ketika menerapkan penyalinan dan penugasan, deskriptor sumber daya bergerak dari objek sumber ke objek target, yaitu, tetap dalam satu salinan. Setelah menyalin atau menugaskan, objek sumber memiliki deskriptor nol dan tidak dapat menggunakan sumber daya. Destuctor membebaskan sumber daya yang ditangkap. Istilah kepemilikan eksklusif atau ketat [Josuttis] juga digunakan untuk strategi ini, Andrei Alexandrescu menggunakan istilah menyalin destruktif. Dalam C ++ 11, hal ini dilakukan sebagai berikut: penyalinan dan penyalinan biasa dilarang dengan cara yang dijelaskan di atas, dan semantik gerakan diimplementasikan, yaitu, konstruktor bergerak dan operator penugasan langkah ditentukan. (Lebih lanjut tentang semantik gerakan nanti.)
class X { public: X(const X&) = delete; X& operator=(const X&) = delete; X(X&& src) noexcept; X& operator=(X&& src) noexcept;
Dengan demikian, strategi kepemilikan eksklusif dapat dianggap sebagai perpanjangan dari strategi copy-ban.
Di pustaka standar C ++ 11, strategi ini menggunakan smart pointer std::unique_ptr<>
dan beberapa kelas lainnya, misalnya: std::thread
, std::unique_lock<>
, serta kelas yang sebelumnya menggunakan strategi larangan penyalinan ( std::fstream
, dll.). Di Windows, kelas MFC yang sebelumnya menggunakan strategi larangan-salin juga mulai menggunakan strategi kepemilikan eksklusif ( CFile
, CEvent
, CMutex
, dll.).
1.3. Strategi salin mendalam
Dalam hal ini, Anda dapat menyalin dan menetapkan instance kelas. Kita perlu mendefinisikan konstruktor salinan dan operator penugasan salinan, sehingga objek target menyalin sumber daya itu sendiri dari objek sumber. Setelah itu, setiap objek memiliki salinan sumber dayanya, dapat menggunakan, memodifikasi, dan melepaskan sumber daya secara mandiri. Destuctor membebaskan sumber daya yang ditangkap. Terkadang untuk objek yang menggunakan strategi salin dalam, istilah nilai objek digunakan.
Strategi ini tidak berlaku untuk semua sumber daya. Ini dapat diterapkan ke sumber daya yang terkait dengan buffer memori, seperti string, tetapi tidak begitu jelas bagaimana menerapkannya pada objek kernel OS seperti file, mutex, dll.
Strategi salin mendalam digunakan dalam semua jenis string objek, std::vector<>
dan wadah lain dari perpustakaan standar.
1.4. Strategi kepemilikan bersama
Dalam hal ini, Anda dapat menyalin dan menetapkan instance kelas. Anda harus mendefinisikan pembuat salinan dan operator penugasan di mana deskriptor sumber daya (serta data lainnya) disalin, tetapi bukan sumber daya itu sendiri. Setelah itu, setiap objek memiliki salinan deskriptor sendiri, dapat menggunakan, memodifikasi, tetapi tidak dapat melepaskan sumber daya, selama ada setidaknya satu objek lagi yang memiliki salinan deskriptor. Sumber daya dibebaskan setelah objek terakhir yang memiliki salinan pegangan keluar dari ruang lingkup. Bagaimana ini dapat diimplementasikan dijelaskan di bawah ini.
Strategi kepemilikan bersama sering digunakan oleh smart pointer, dan juga wajar untuk menggunakannya untuk sumber daya yang tidak dapat diubah. std::shared_ptr<>
smart pointer mengimplementasikan strategi ini di pustaka standar C ++ 11.
2. Deep Copy Strategy - Masalah dan Solusi
Pertimbangkan templat untuk fungsi pertukaran status objek bertipe T
di pustaka standar C ++ 98.
template<typename T> void swap(T& a, T& b) { T tmp(a); a = b; b = tmp; }
Jika tipe T
memiliki sumber daya dan menggunakan strategi penyalinan yang dalam, maka kami memiliki tiga operasi untuk mengalokasikan sumber daya baru, tiga operasi penyalinan, dan tiga operasi untuk membebaskan sumber daya. Sementara dalam kebanyakan kasus operasi ini dapat dilakukan tanpa mengalokasikan sumber daya baru dan menyalin sama sekali, itu sudah cukup bagi objek untuk bertukar data internal, termasuk deskriptor sumber daya. Ada banyak contoh serupa ketika Anda harus membuat salinan sementara sumber daya dan segera merilisnya. Implementasi operasi sehari-hari yang tidak efektif seperti itu merangsang pencarian solusi untuk optimasi mereka. Mari pertimbangkan opsi utama.
2.1. Salin dalam rekaman
Copy on write (COW), juga disebut salinan ditangguhkan, dapat dilihat sebagai upaya untuk menggabungkan strategi copy dalam dan strategi kepemilikan bersama. Awalnya, ketika menyalin objek, deskriptor sumber daya disalin, tanpa sumber daya itu sendiri, dan untuk pemilik sumber daya menjadi dibagikan dan hanya-baca, tetapi segera setelah beberapa pemilik perlu mengubah sumber daya bersama, sumber daya disalin dan kemudian pemilik ini bekerja dengan nya salinan. Menerapkan KK memecahkan masalah pertukaran negara: alokasi sumber daya tambahan dan penyalinan tidak terjadi. Menggunakan COW cukup populer ketika menerapkan string, misalnya, CString
(MFC, ATL). Diskusi tentang cara-cara yang mungkin untuk menerapkan KK dan masalah yang muncul dapat ditemukan di [Meyers1], [Sutter]. [Guntheroth] mengusulkan implementasi SAP menggunakan std::shared_ptr<>
. Ada masalah ketika menerapkan SAP dalam lingkungan multi-utas, itulah sebabnya mengapa dilarang menggunakan SAP untuk string dalam pustaka C ++ 11 standar, lihat [Josuttis], [Guntheroth].
Pengembangan gagasan KK mengarah ke skema manajemen sumber daya berikut: sumber daya tidak dapat diubah dan dikelola oleh objek menggunakan strategi kepemilikan bersama, jika perlu, untuk mengubah sumber daya, dibuat sumber daya baru yang dimodifikasi secara tepat, dan objek pemilik baru dikembalikan. Skema ini digunakan untuk string dan objek tidak berubah lainnya pada platform .NET dan Java. Dalam pemrograman fungsional, ini digunakan untuk struktur data yang lebih kompleks.
2.2. Mendefinisikan fungsi pertukaran negara untuk suatu kelas
Diperlihatkan di atas betapa tidak efisiennya fungsi pertukaran negara dapat, dilaksanakan secara langsung, melalui penyalinan dan penugasan. Dan itu digunakan cukup luas, misalnya, digunakan oleh banyak algoritma perpustakaan standar. Agar algoritma tidak menggunakan std::swap()
, tetapi fungsi lain yang secara khusus ditentukan untuk kelas, dua langkah harus dilakukan.
1. Tentukan di kelas fungsi anggota Swap()
(nama tidak penting) yang mengimplementasikan pertukaran status.
class X { public: void Swap(X& other) noexcept;
Anda harus memastikan bahwa fungsi ini tidak membuang pengecualian, dalam C ++ 11, fungsi tersebut harus dinyatakan sebagai noexcept
.
2. Dalam namespace yang sama dengan kelas X
(biasanya dalam file header yang sama), tentukan fungsi swap()
non-anggota) gratis swap()
sebagai berikut (nama dan tanda tangan sangat mendasar):
inline void swap(X& a, X& b) noexcept { a.Swap(b); }
Setelah itu, algoritma perpustakaan standar akan menggunakannya, bukan std::swap()
. Ini menyediakan mekanisme yang disebut lookup ketergantungan argumen (ADL). Untuk lebih lanjut tentang ADL, lihat [Dewhurst1].
Di pustaka standar C ++, semua kontainer, pointer pintar, serta kelas lainnya menerapkan fungsi pertukaran negara seperti dijelaskan di atas.
Fungsi anggota Swap()
biasanya mudah didefinisikan: perlu untuk secara berurutan menerapkan operasi pertukaran negara ke database dan anggota, jika mereka mendukungnya, dan std::swap()
jika tidak.
Deskripsi di atas agak disederhanakan, yang lebih rinci dapat ditemukan di [Meyers2]. Diskusi tentang masalah yang berkaitan dengan fungsi pertukaran negara juga dapat ditemukan di [Sutter / Alexandrescu].
Fungsi pertukaran negara dapat dikaitkan dengan salah satu operasi dasar kelas. Dengan menggunakannya, Anda dapat mendefinisikan operasi lain dengan anggun. Sebagai contoh, operator penugasan salinan didefinisikan melalui salin dan Swap()
sebagai berikut:
X& X::operator=(const X& src) { X tmp(src); Swap(tmp); return *this; }
Template ini disebut salinan dan pertukaran idiom atau idiom Herb Sutter, untuk lebih jelasnya lihat [Sutter], [Sutter / Alexandrescu], [Meyers2]. Modifikasinya dapat diterapkan untuk mengimplementasikan semantik perpindahan, lihat bagian 2.4, 2.6.1.
2.3. Menghapus salinan perantara oleh kompiler
Pertimbangkan kelasnya
class X { public: X();
Dan fungsinya
X Foo() {
Dengan pendekatan langsung, pengembalian dari fungsi Foo()
direalisasikan dengan menyalin contoh X
Tetapi kompiler dapat menghapus operasi penyalinan dari kode, objek dibuat langsung pada titik panggilan. Ini disebut pengembalian nilai optimasi (RVO). RVO telah digunakan oleh pengembang kompiler untuk beberapa waktu dan saat ini diperbaiki dalam standar C ++ 11. Meskipun keputusan RVO dibuat oleh kompiler, programmer dapat menulis kode berdasarkan penggunaannya. Untuk melakukan ini, diharapkan bahwa fungsi memiliki satu titik kembali dan jenis ekspresi yang dikembalikan cocok dengan jenis nilai balik fungsi. Dalam beberapa kasus, disarankan untuk mendefinisikan konstruktor tertutup khusus yang disebut "konstruktor komputasional", untuk detail lebih lanjut lihat [Dewhurst2]. RVO juga dibahas dalam [Meyers3] dan [Guntheroth].
Kompiler dapat menghapus salinan perantara dalam situasi lain.
2.4. Implementasi semantik perpindahan
Implementasi semantik bergerak terdiri dari mendefinisikan konstruktor bergerak yang memiliki parameter tipe rvalue-referensi ke sumber dan operator penugasan langkah dengan parameter yang sama.
Di Perpustakaan Standar C ++ 11, templat fungsi pertukaran negara didefinisikan sebagai berikut:
template<typename T> void swap(T& a, T& b) { T tmp(std::move(a)); a = std::move(b); b = std::move(tmp); }
Sesuai dengan aturan untuk menyelesaikan kelebihan fungsi yang memiliki parameter dari tipe referensi nilai (lihat Lampiran A), dalam kasus ketika tipe T
memiliki konstruktor bergerak dan operator penugasan bergerak, mereka akan digunakan, dan tidak akan ada alokasi sumber daya sementara dan penyalinan. Jika tidak, operator penyalin dan penyalin salinan akan digunakan.
Menggunakan semantik relokasi menghindari membuat salinan sementara dalam konteks yang jauh lebih luas daripada fungsi pertukaran negara yang dijelaskan di atas. Semantik gerakan berlaku untuk nilai nilai apa pun, yaitu nilai sementara dan tidak bernama, serta nilai pengembalian fungsi jika dibuat secara lokal (termasuk nilai), dan RVO tidak diterapkan. Dalam semua kasus ini, dijamin bahwa objek sumber tidak dapat digunakan dengan cara apa pun setelah pindah. Semantik bergerak juga berlaku untuk nilai lvalue yang diterapkan transformasi std::move()
. Tetapi dalam kasus ini, programmer bertanggung jawab atas bagaimana objek sumber akan digunakan setelah pindah (contoh std::swap()
).
Pustaka C ++ 11 standar telah dirancang ulang dengan mempertimbangkan semantik gerakan. Banyak kelas telah menambahkan konstruktor bergerak dan operator penugasan langkah, serta fungsi anggota lainnya, dengan parameter referensi tipe nilai. Misalnya, std::vector<T>
memiliki versi void push_back(T&& src)
yang kelebihan beban. Semua ini memungkinkan dalam banyak kasus untuk menghindari membuat salinan sementara.
Menerapkan semantik langkah tidak membatalkan definisi fungsi pertukaran negara untuk kelas. Fungsi pertukaran negara yang didefinisikan secara khusus dapat lebih efisien daripada std::swap()
. Selain itu, konstruktor pemindahan dan operator penugasan pemindahan sangat mudah ditentukan dengan menggunakan fungsi anggota dari pertukaran status sebagai berikut (variasi salinan dan idiom pertukaran):
class X { public: X() noexcept {} void Swap(X& other) noexcept {} X(X&& src) noexcept : X() { Swap(src); } X& operator=(X&& src) noexcept { X tmp(std::move(src));
Konstruktor bergerak dan operator penugasan langkah adalah fungsi-fungsi anggota yang sangat diinginkan untuk memastikan bahwa mereka tidak membuang pengecualian dan karenanya dinyatakan sebagai tidak noexcept
. Hal ini memungkinkan Anda untuk mengoptimalkan beberapa operasi dari wadah perpustakaan standar tanpa melanggar jaminan ketat tentang keselamatan pengecualian, untuk detail lebih lanjut lihat [Meyers3] dan [Guntheroth]. Template yang diusulkan memberikan jaminan seperti itu, asalkan konstruktor default dan fungsi anggota dari pertukaran negara tidak memberikan pengecualian.
C ++ 11 standar menyediakan untuk kompiler secara otomatis menghasilkan konstruktor bergerak dan operator penugasan bergerak. Untuk melakukan ini, mereka harus dideklarasikan menggunakan konstruk "=default"
.
class X { public: X(X&&) = default; X& operator=(X&&) = default;
Operasi dilaksanakan dengan secara berurutan menerapkan operasi pindah ke pangkalan dan anggota kelas, jika mereka mendukung gerakan, dan menyalin operasi sebaliknya. Jelas bahwa opsi ini jauh dari selalu dapat diterima. Deskriptor mentah tidak bergerak, tetapi Anda biasanya tidak dapat menyalinnya. Dalam kondisi tertentu, kompiler dapat secara independen menghasilkan konstruktor bergerak yang sama dan operator penugasan bergerak, tetapi lebih baik tidak menggunakan kesempatan ini, kondisi ini agak membingungkan dan dapat dengan mudah berubah ketika kelas disempurnakan. Lihat [Meyers3] untuk detailnya.
Secara umum, implementasi dan penggunaan semantik perpindahan cukup "tipis". Kompiler dapat menerapkan penyalinan di mana programmer mengharapkan suatu langkah. Berikut adalah beberapa aturan untuk menghilangkan atau setidaknya mengurangi kemungkinan situasi seperti itu.
- Jika memungkinkan, gunakan larangan menyalin.
- Nyatakan konstruktor pemindahan dan pindahkan operator penugasan sebagai bukan
noexcept
. - Menerapkan semantik gerakan untuk kelas dasar dan anggota.
- Menerapkan transformasi
std::move()
ke parameter fungsi tipe rvalue referensi.
Aturan 2 dibahas di atas. 4 , rvalue- lvalue (. ). .
class B {
, . 6.2.1.
2.5. vs.
, RVO (. 2.3), , . ( ), , . , . C++11 - emplace()
, emplace_front()
, emplace_back()
, . , - — (variadic templates), . , C++11 — .
:
- , , .
- , , .
, .
std::vector<std::string> vs; vs.push_back(std::string(3, 'X'));
std::string
, . . , , . , [Meyers3].
2.6. Ringkasan
, , . - . . — : , . , , , . : , , «» .
: , , .NET Java. , Clone()
Duplicate()
.
- - , :
- .
- .
- - rvalue-.
.NET Java - , , .NET IClonable
. , .
3.
, . - , . , . Windows: , HANDLE
, COM-. DuplicateHandle()
, CloseHandle()
. COM- - IUnknown::AddRef()
IUnknown::Release()
. ATL ComPtr<>
, COM- . UNIX, C, _dup()
, .
C++11 std::shared_ptr<>
. , , , , , . , . std::shared_ptr<>
[Josuttis], [Meyers3].
: - , ( ). ( ) , . std::shared_ptr<>
std::weak_ptr<>
. . [Josuttis], [Meyers3].
- [Alexandrescu]. ( ) , [Schildt]. , .
( ) [Alger].
-. [Josuttis] [Alexandrescu].
- .NET Java. , , , .
4.
, C++ rvalue- . C++98 std::auto_ptr<>
, , , . , , ( ). C++11 rvalue- , , . C++11 std::auto_ptr><>
std::unique_ptr<>
. , [Josuttis], [Meyers3].
: - ( std::fstream
, etc.), ( std::thread
, std::unique_lock<>
, etc.). MFC , ( CFile
, CEvent
, CMutex
, etc.).
5. —
. , . , , , . , , , ( ) . , , , . ( ) , . , . — . 6.
, - -, « », - . - . , , , , - . «».
6. -
, - . , -. .
6.1.
- . , , :
- . , .
- .
- .
, , , . C++11 .
« » (resource acquisition is initialization, RAII). RAII ( ), ., [Dewhurst1]. «» RAII. , , , (immutable) RAII.
6.2.
, RAII, , , . - , , - . , , , . .
6.2.1.
, , , , :
- , .
- .
- .
- .
C++11 , , , . , - clear()
, , , . . , shrink_to_fit()
, , (. ).
, RAII, , , . , .
class X { public:
.
X x;
std::thread
.
2.4, - . , - - . .
class X {
:
X::X(X&& src) noexcept : X() { Swap(src); } X& X::operator=(X&& src) noexcept { X tmp(std::move(src));
- :
void X::Create() { X tmp();
, , , - . , , , . , .
- « », , . : , , ( ). : , . , : , , . , . [Sutter], [Sutter/Alexandrescu], [Meyers2].
, RAII .
6.2.2.
RAII . , , , , :
- , .
- .
- . , .
- .
- .
«» RAII, — . , , . 3. . «», .
6.2.3.
— . RAII , . , . , , ( -). - ( -). 6.2.1, .
6.3.
, - RAII, : . , , .
7.
, , , , . - -.
4 -:
- .
- .
- .
- .
. , - : , , - .
, . , , -, , .
- . . , (. 6.2.3). , (. 6.2.1). , . , , . , std::shared_ptr<>
.
Aplikasi
. Rvalue-
Rvalue- C++ , , rvalue-. rvalue- T
T&&
.
:
class Int { int m_Value; public: Int(int val) : m_Value(val) {} int Get() const { return m_Value; } void Set(int val) { m_Value = val; } };
, rvalue- .
Int&& r0;
rvalue- ++ , lvalue. Contoh:
Int i(7); Int&& r1 = i;
rvalue:
Int&& r2 = Int(42);
lvalue rvalue-:
Int&& r4 = static_cast<Int&&>(i);
rvalue- ( ) std::move()
, ( <utility>
).
Rvalue rvalue , .
int&& r5 = 2 * 2;
rvalue- .
Int&& r = 7; std::cout << r.Get() << '\n';
Rvalue- .
Int&& r = 5; Int& x = r;
Rvalue- , . , rvalue-, rvalue .
void Foo(Int&&); Int i(7); Foo(i);
, rvalue rvalue- , . rvalue-.
, , , rvalue-, (ambiguous) rvalue .
void Foo(Int&&); void Foo(const Int&);
Int i(7); Foo(i);
: rvalue- lvalue.
Int&& r = 7; Foo(r);
, rvalue-, lvalue std::move()
. . 2.4.
++11, rvalue- — -. (lvalue/rvalue) this
.
class X { public: X(); void DoIt() &;
.
, ( std::string
, std::vector<>
, etc.) . — . , rvalue- . , , - , - , . , , , rvalue, lvalue. , rvalue. . , ( lvalue), RVO.
Referensi
[Alexandrescu]
, . C++.: . dari bahasa inggris - M.: LLC “Saya. », 2002.
[Guntheroth]
, . C++. .: . dari bahasa inggris — .: «-», 2017.
[Josuttis]
, . C++: , 2- .: . dari bahasa inggris - M.: LLC “Saya. », 2014.
[Dewhurst1]
, . C++. , 2- .: . dari bahasa inggris — .: -, 2013.
[Dewhurst2]
, . C++. .: . dari bahasa inggris — .: , 2012.
[Meyers1]
, . C++. 35 .: . dari bahasa inggris — .: , 2000.
[Meyers2]
, . C++. 55 .: . dari bahasa inggris — .: , 2014.
[Meyers3]
, . C++: 42 C++11 C ++14.: . dari bahasa inggris - M.: LLC “Saya. », 2016.
[Sutter]
, . C++.: . dari bahasa inggris — : «.. », 2015.
[Sutter/Alexandrescu]
, . , . ++.: . dari bahasa inggris - M.: LLC “Saya. », 2015.
[Schildt]
, . C++.: . dari bahasa inggris — .: -, 2005.
[Alger]
, . C++: .: . dari bahasa inggris — .: « «», 1999.