Teknik untuk menghindari perilaku tidak terdefinisi saat mengakses singleton

Artikel ini membahas penyebab dan metode menghindari perilaku yang tidak terdefinisi ketika mengakses singleton di c ++ modern. Contoh kode single-threaded disediakan. Tidak ada yang khusus-compiler, semua sesuai dengan standar.

Pendahuluan


Untuk memulai, saya sarankan Anda membaca artikel lain tentang singleton di Habré:

Tiga usia pola Singleton
Singleton dan Common Instances
3 Cara untuk Melanggar Prinsip Tanggung Jawab Tunggal
Singleton - pola atau antipattern?
Menggunakan pola singleton

Dan, akhirnya, sebuah artikel yang menyentuh topik yang sama, tetapi lolos (jika hanya karena kerugian dan keterbatasan tidak dipertimbangkan):
benda-benda tialized (yaitu, objek
Singleton dan objek seumur hidup

Selanjutnya:

  • ini bukan artikel tentang properti arsitektur singleton;
  • ini bukan artikel "bagaimana membuat singleton putih dan halus dari singleton yang mengerikan dan mengerikan";
  • ini bukan kampanye tunggal;
  • ini bukan perang melawan singleton;
  • ini bukan artikel akhir yang menyenangkan.

Artikel ini adalah tentang satu yang sangat penting, tetapi masih aspek teknis menggunakan singleton di C ++ modern. Perhatian utama dalam artikel ini diberikan pada saat kehancuran singleton, sebagai di sebagian besar sumber, masalah kehancuran tidak diungkapkan dengan baik. Biasanya, penekanannya adalah pada saat singleton diciptakan, dan tentang kehancuran, paling-paling, ia mengatakan sesuatu seperti "dihancurkan dalam urutan terbalik."

Saya akan meminta Anda untuk mengikuti ruang lingkup artikel dalam komentar, terutama untuk tidak mengatur holivar "pola tunggal vs tunggal-antipattern".

Jadi ayo pergi.

Apa yang dikatakan standar


Kutipan berasal dari C ++ 14 draft final N3936, as tersedia C ++ 17 konsep tidak ditandai sebagai "final".
Saya memberikan bagian yang paling penting secara keseluruhan. Tempat-tempat penting disorot oleh saya.

3.6.3 Pengakhiran [basic.start.term]

1. Destructors (12.4) untuk objek yang diinisialisasi (yaitu, objek yang masa pakainya (3.8) telah dimulai) dengan durasi penyimpanan statis disebut sebagai hasil dari kembali dari utama dan sebagai akibat dari panggilan std :: exit (18.5). Destruktor untuk objek yang diinisialisasi dengan durasi penyimpanan utas dalam utas yang diberikan disebut sebagai hasil dari fungsi awal utas tersebut dan sebagai akibat dari utas yang memanggil std :: exit. Penyelesaian destruktor untuk semua objek yang diinisialisasi dengan durasi penyimpanan utas dalam utas tersebut diurutkan sebelum inisiasi destruktor objek apa pun dengan durasi penyimpanan statis. Jika penyelesaian konstruktor atau inisialisasi dinamis suatu objek dengan durasi penyimpanan ulir diurutkan sebelum yang lain, penyelesaian destruktor yang kedua diurutkan sebelum inisiasi destruktor yang pertama. Jika penyelesaian konstruktor atau inisialisasi dinamis suatu objek dengan durasi penyimpanan statis diurutkan sebelum yang lain, penyelesaian destruktor yang kedua diurutkan sebelum inisiasi destruktor yang pertama. [Catatan: Definisi ini memungkinkan penghancuran bersamaan. –End note] Jika objek diinisialisasi secara statis, objek dihancurkan dalam urutan yang sama seperti jika objek diinisialisasi secara dinamis. Untuk objek array atau tipe kelas, semua sub objek dari objek tersebut dihancurkan sebelum objek ruang lingkup blok apa pun dengan durasi penyimpanan statis yang diinisialisasi selama konstruksi subobjek dihancurkan. Jika penghancuran objek dengan durasi penyimpanan statis atau keluar melalui pengecualian, std :: terminate disebut (15.5.1).

2. Jika suatu fungsi berisi objek blok-lingkup durasi penyimpanan statis atau thread yang telah dihancurkan dan fungsi dipanggil selama penghancuran objek dengan durasi penyimpanan statis atau thread, program memiliki perilaku yang tidak ditentukan jika aliran kontrol melewati melalui definisi objek blockscope yang sebelumnya hancur. Demikian juga, perilaku tidak terdefinisi jika objek lingkup blok digunakan secara tidak langsung (yaitu, melalui pointer) setelah kehancurannya.

3. Jika penyelesaian inisialisasi objek dengan durasi penyimpanan statis diurutkan sebelum panggilan ke std :: atexit (lihat "cstdlib", 18.5), panggilan ke fungsi yang diteruskan ke std :: atexit diurutkan sebelum panggilan ke destruktor untuk objek. Jika panggilan ke std :: atexit diurutkan sebelum penyelesaian inisialisasi objek dengan durasi penyimpanan statis, panggilan ke destruktor untuk objek diurutkan sebelum panggilan ke fungsi yang dilewatkan ke std :: atexit. Jika panggilan ke std :: atexit diurutkan sebelum panggilan lain ke std :: atexit, panggilan ke fungsi yang diteruskan ke std kedua :: panggilan atexit diurutkan sebelum panggilan ke fungsi dialihkan ke std :: atexit call pertama .

4. Jika ada penggunaan objek atau fungsi perpustakaan standar yang tidak diizinkan dalam penangan sinyal (18.10) yang tidak terjadi sebelum (1.10) penyelesaian penghancuran objek dengan durasi penyimpanan statis dan pelaksanaan fungsi terdaftar std :: atexit terdaftar (18.5) ), program memiliki perilaku yang tidak ditentukan. [Catatan: Jika ada penggunaan objek dengan durasi penyimpanan statis yang tidak terjadi sebelum kerusakan objek, program memiliki perilaku yang tidak ditentukan. Mengakhiri setiap utas sebelum panggilan ke std :: keluar atau keluar dari utama sudah cukup, tetapi tidak perlu, untuk memenuhi persyaratan ini. Persyaratan ini mengizinkan manajer utas sebagai objek durasi penyimpanan statis. —End note]

5. Memanggil fungsi std :: abort () yang dideklarasikan dalam “cstdlib” mengakhiri program tanpa mengeksekusi destruktor dan tanpa memanggil fungsi yang dilewatkan ke std :: atexit () atau std :: at_quick_exit ().
Interpretasi:

  • penghancuran objek dengan durasi penyimpanan thread dilakukan dalam urutan terbalik dari penciptaannya;
  • setelah itu, objek dengan durasi penyimpanan statis dihancurkan dan panggilan dilakukan ke fungsi yang terdaftar dengan std :: atexit dalam urutan terbalik untuk membuat objek tersebut dan mendaftarkan fungsi tersebut;
  • Upaya untuk mengakses objek yang hancur dengan durasi penyimpanan utas atau durasi penyimpanan statis berisi perilaku yang tidak terdefinisi. Inisialisasi ulang objek tersebut tidak disediakan.

Catatan: variabel global dalam standar disebut sebagai "variabel non-lokal dengan durasi penyimpanan statis". Akibatnya, ternyata semua variabel global, semua singletone (statika lokal) dan semua panggilan ke std :: atexit jatuh ke dalam antrian LIFO tunggal saat mereka dibuat / terdaftar.

Informasi yang berguna untuk artikel ini juga terdapat di bagian 3.6.2 Inisialisasi variabel non-lokal [basic.start.init] . Saya hanya membawa yang paling penting:
Inisialisasi dinamis dari variabel non-lokal dengan durasi penyimpanan statis dapat dipesan atau tidak. [...] Variabel dengan inisialisasi terurut yang didefinisikan dalam unit terjemahan tunggal harus diinisialisasi dalam urutan definisi mereka dalam unit terjemahan.
Interpretasi (dengan mempertimbangkan teks lengkap bagian ini): variabel global dalam satu unit terjemahan diinisialisasi dalam urutan deklarasi.

Apa yang akan ada dalam kode


Semua contoh kode yang disediakan dalam artikel dipublikasikan di github .

Kode terdiri dari tiga lapisan, seolah-olah ditulis oleh orang yang berbeda:

  • singleton;
  • utilitas (kelas menggunakan singleton);
  • pengguna (variabel global dan utama).

Singleton dan utilitasnya seperti perpustakaan pihak ketiga, dan pengguna adalah pengguna.
Lapisan utilitas dirancang untuk mengisolasi lapisan pengguna dari lapisan tunggal. Dalam contoh, pengguna memiliki kesempatan untuk mengakses singleton, tetapi kami akan bertindak seolah-olah itu tidak mungkin.

Pengguna pertama melakukan segalanya dengan benar, dan kemudian dengan jentikan pergelangan tangan segalanya pecah. Pertama kita mencoba untuk memperbaikinya di lapisan utilitas, dan jika tidak berhasil, maka di lapisan tunggal.

Dalam kode, kita akan terus berjalan di sepanjang tepi - sekarang di sisi terang, lalu di gelap. Untuk membuatnya lebih mudah untuk beralih ke sisi gelap, case yang paling sulit dipilih - mengakses singleton dari utilitas destruktor.

Mengapa kasus pemanggilan dari destruktor yang paling sulit? Karena utilitas perusak dapat dipanggil dalam proses meminimalkan aplikasi, ketika pertanyaan “apakah singleton telah dihancurkan atau belum” menjadi relevan.

Kasingnya semacam sintetis. Dalam praktiknya, panggilan ke singleton dari destructor tidak diperlukan. Bahkan sesuai kebutuhan. Misalnya, untuk mencatat kerusakan objek.

Tiga kelas singleton digunakan:

  • SingletonClassic - tidak ada petunjuk pintar. Sebenarnya, itu tidak langsung klasik, tetapi jelas yang paling klasik di antara ketiganya dipertimbangkan;
  • SingletonShared - dengan std :: shared_ptr;
  • SingletonWeak - dengan std :: lemah_ptr.

Semua singletones adalah templat. Parameter template digunakan untuk mewarisi darinya. Dalam sebagian besar contoh, mereka diparameterisasi oleh kelas Payload, yang menyediakan satu fungsi publik untuk menambahkan data ke std :: set.

Penghancur utilitas dalam kebanyakan contoh mencoba mengisi seratus nilai di sana. Keluaran diagnostik ke konsol juga digunakan dari konstruktor singleton, destruktor singleton, dan instance ().

Kenapa begitu sulit? Untuk membuatnya lebih mudah untuk memahami bahwa kita berada di sisi gelap. Seruan kepada singleton yang hancur adalah perilaku yang tidak terdefinisi, tetapi mungkin tidak dimanifestasikan dengan cara apa pun secara eksternal. Memasukkan nilai ke std :: set yang hancur juga tentu saja tidak menjamin manifestasi eksternal, tetapi tidak ada cara yang lebih dapat diandalkan (pada kenyataannya, di GCC di Linux dalam contoh yang salah dengan singleton klasik, std :: set yang dihancurkan berhasil diisi, dan dalam MSVS di bawah Windows - hang). Dengan perilaku tidak terdefinisi, output ke konsol mungkin tidak terjadi. Jadi dalam contoh yang benar, kami mengharapkan tidak adanya akses ke instance () setelah destruktor, serta tidak adanya crash dan tidak ada hang, dan pada yang tidak benar, baik dengan adanya banding, atau crash, atau hang, atau sekaligus dalam kombinasi apa pun, atau apa pun.

Singleton klasik


Payload.h
#pragma once #include <set> class Payload { public: Payload() = default; ~Payload() = default; Payload(const Payload &) = delete; Payload(Payload &&) = delete; Payload& operator=(const Payload &) = delete; Payload& operator=(Payload &&) = delete; void add(int value) { m_data.emplace(value); } private: std::set<int> m_data; }; 


SingletonClassic.h
 #pragma once #include <iostream> template<typename T> class SingletonClassic : public T { public: ~SingletonClassic() { std::cout << "~SingletonClassic()" << std::endl; } SingletonClassic(const SingletonClassic &) = delete; SingletonClassic(SingletonClassic &&) = delete; SingletonClassic& operator=(const SingletonClassic &) = delete; SingletonClassic& operator=(SingletonClassic &&) = delete; static SingletonClassic& instance() { std::cout << "instance()" << std::endl; static SingletonClassic inst; return inst; } private: SingletonClassic() { std::cout << "SingletonClassic()" << std::endl; } }; 


SingletonClassic example 1


Classic_Example1_correct.cpp
 #include "SingletonClassic.h" #include "Payload.h" #include <memory> class ClassicSingleThreadedUtility { public: ClassicSingleThreadedUtility() { // To ensure that singleton will be constucted before utility SingletonClassic<Payload>::instance(); } ~ClassicSingleThreadedUtility() { auto &instance = SingletonClassic<Payload>::instance(); for ( int i = 0; i < 100; ++i ) instance.add(i); } }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified ClassicSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<ClassicSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<ClassicSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order is correct int main() { return 0; } 


Output Konsol
contoh ()
SingletonClassic ()
contoh ()
~ SingletonClassic ()

Utilitas memanggil singleton dalam konstruktor untuk memastikan bahwa singleton dibuat sebelum utilitas dibuat.

Pengguna membuat dua std :: unique_ptr: satu kosong, yang kedua berisi utilitas.

Urutan penciptaan:

- empty std :: unique_ptr.
- singleton;
- utilitas.

Dan karenanya, urutan kehancuran:

- utilitas;
- singleton;
- empty std :: unique_ptr.

Panggilan dari perusak utilitas ke singleton sudah benar.

SingletonClassic example 2


Semuanya sama, tetapi pengguna mengambilnya dan merusak semuanya dengan satu baris.

Classic_Example2_incorrect.cpp
 #include "SingletonClassic.h" #include "Payload.h" #include <memory> class ClassicSingleThreadedUtility { public: ClassicSingleThreadedUtility() { // To ensure that singleton will be constucted before utility SingletonClassic<Payload>::instance(); } ~ClassicSingleThreadedUtility() { auto &instance = SingletonClassic<Payload>::instance(); for ( int i = 0; i < 100; ++i ) instance.add(i); } }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified ClassicSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<ClassicSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<ClassicSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order seems to be correct ... int main() { // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); // Guaranteed destruction order is still the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect return 0; } 


Output Konsol
contoh ()
SingletonClassic ()
~ SingletonClassic ()
contoh ()

Urutan penciptaan dan kehancuran dipertahankan. Tampaknya semuanya masih ada. Tapi tidak. Dengan memanggil emptyUnique.swap (utilityUnique), pengguna melakukan perilaku yang tidak terdefinisi.

Mengapa pengguna melakukan hal bodoh seperti itu? Karena dia tidak tahu apa-apa tentang struktur internal perpustakaan, yang memberinya singleton dan utilitas.

Dan apakah Anda tahu struktur internal perpustakaan? ... kalau begitu, dalam kode nyata sangat mudah untuk terlibat. Dan Anda harus keluar dengan debag yang menyakitkan, karena untuk memahami apa yang sebenarnya terjadi tidak akan mudah.

Mengapa tidak mengharuskan perpustakaan digunakan dengan benar? Nah, ada semua jenis dermaga untuk ditulis, contoh ... Dan mengapa tidak membuat perpustakaan yang tidak begitu mudah rusak?

SingletonClassic example 3


Dalam mempersiapkan artikel selama beberapa hari, saya percaya bahwa tidak mungkin untuk menghilangkan perilaku tidak terbatas dari contoh sebelumnya di lapisan utilitas, dan solusinya hanya tersedia di lapisan tunggal. Namun seiring berjalannya waktu, sebuah solusi masih muncul.

Sebelum membuka spoiler dengan kode dan penjelasan, saya sarankan pembaca untuk mencoba mencari jalan keluar dari situasi mereka sendiri (hanya di lapisan utilitas!). Saya tidak mengesampingkan bahwa ada solusi yang lebih baik.

Classic_Example3_correct.cpp
 #include "SingletonClassic.h" #include "Payload.h" #include <memory> #include <iostream> class ClassicSingleThreadedUtility { public: ClassicSingleThreadedUtility() { thread_local auto flag_strong = std::make_shared<char>(0); m_flag_weak = flag_strong; SingletonClassic<Payload>::instance(); } ~ClassicSingleThreadedUtility() { if ( !m_flag_weak.expired() ) { auto &instance = SingletonClassic<Payload>::instance(); for ( int i = 0; i < 100; ++i ) instance.add(i); } } private: std::weak_ptr<char> m_flag_weak; }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified ClassicSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<ClassicSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<ClassicSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order seems to be correct ... int main() { // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); { // To demonstrate normal processing before application ends auto utility = ClassicSingleThreadedUtility(); } // Guaranteed destruction order is still the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect ... // ... but utility uses a variable with thread storage duration to detect thread termination. return 0; } 


Output Konsol
contoh ()
SingletonClassic ()
contoh ()
contoh ()
~ SingletonClassic ()

Penjelasan
Masalahnya hanya terjadi ketika meminimalkan aplikasi. Perilaku tidak terdefinisi dapat dihilangkan dengan mengajarkan utilitas untuk mengenali kapan aplikasi diminimalkan. Untuk melakukan ini, kami menggunakan variabel flag_strong dari std :: shared_ptr type, yang memiliki kualifikasi durasi penyimpanan thread (lihat kutipan dari standar dalam artikel di atas) - ini seperti statis, tetapi hanya dihancurkan ketika utas saat ini berakhir sebelum statika dihancurkan , termasuk sebelum kehancuran singleton. Variabel flag_strong adalah satu untuk seluruh aliran, dan setiap instance dari utilitas menyimpan salinannya yang lemah.

Dalam arti sempit, solusinya bisa disebut hack, karena itu tidak langsung dan tidak jelas. Selain itu, ia memperingatkan terlalu dini, dan kadang-kadang (dalam aplikasi multi-utas) umumnya memperingatkan salah. Namun dalam arti luas, ini bukan peretasan, tetapi solusi dengan sepenuhnya ditentukan oleh properti standar - baik kerugian dan keuntungan.

Singletonshared


Mari kita beralih ke singleton yang dimodifikasi berdasarkan std :: shared_ptr.

SingletonShared.h
 #pragma once #include <memory> #include <iostream> template<typename T> class SingletonShared : public T { public: ~SingletonShared() { std::cout << "~SingletonShared()" << std::endl; } SingletonShared(const SingletonShared &) = delete; SingletonShared(SingletonShared &&) = delete; SingletonShared& operator=(const SingletonShared &) = delete; SingletonShared& operator=(SingletonShared &&) = delete; static std::shared_ptr<SingletonShared> instance() { std::cout << "instance()" << std::endl; // "new" and no std::make_shared because of private c-tor static auto inst = std::shared_ptr<SingletonShared>(new SingletonShared); return inst; } private: SingletonShared() { std::cout << "SingletonShared()" << std::endl; } }; 


Ay-ah-ah, operator baru tidak boleh digunakan dalam kode modern, tetapi std :: make_share diperlukan! Dan ini dicegah oleh konstruktor pribadi singleton.

Ha! Saya punya masalah juga! Deklarasikan std :: make_share freind singleton! ... dan dapatkan variasi PublicMorozov yang antipattern: menggunakan std :: make_share yang sama, dimungkinkan untuk membuat instance tambahan dari singleton yang tidak disediakan oleh arsitektur.

SingletonShared Contoh 1 dan 2


Sepenuhnya sesuai dengan contoh No. 1 dan 2 untuk versi klasik. Perubahan signifikan dibuat hanya pada lapisan tunggal, utilitas dasarnya tetap sama. Sama seperti dalam contoh dengan singleton klasik, contoh-1 benar, dan contoh-2 menunjukkan perilaku yang tidak terdefinisi.

Shared_Example1_correct.cpp
 #include "SingletonShared.h" #include <Payload.h> #include <memory> class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() { // To ensure that singleton will be constucted before utility SingletonShared<Payload>::instance(); } ~SharedSingleThreadedUtility() { if ( auto instance = SingletonShared<Payload>::instance() ) for ( int i = 0; i < 100; ++i ) instance->add(i); } }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified SharedSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<SharedSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<SharedSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order is correct int main() { return 0; } 


Output Konsol
contoh ()
SingletonShared ()
contoh ()
~ SingletonShared ()

Shared_Example2_incorrect.cpp
 #include "SingletonShared.h" #include "Payload.h" #include <memory> class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() { // To ensure that singleton will be constucted before utility SingletonShared<Payload>::instance(); } ~SharedSingleThreadedUtility() { // Sometimes this check may result as "false" even for destroyed singleton // preventing from visual effects of undefined behaviour ... //if ( auto instance = SingletonShared::instance() ) // for ( int i = 0; i < 100; ++i ) // instance->add(i); // ... so this code will demonstrate UB in colour auto instance = SingletonShared<Payload>::instance(); for ( int i = 0; i < 100; ++i ) instance->add(i); } }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified SharedSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<SharedSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<SharedSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order seems to be correct ... int main() { // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); // Guaranteed destruction order is the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect return 0; } 


Output Konsol
contoh ()
SingletonShared ()
~ SingletonShared ()
contoh ()

Contoh SingletonShared 3


Dan sekarang kami akan mencoba untuk memperbaiki masalah ini lebih baik daripada dalam contoh nomor 3 dari klasik.
Solusinya jelas: Anda hanya perlu memperpanjang umur singleton dengan menyimpan salinan std :: shared_ptr yang dikembalikan oleh singleton dalam utilitas. Dan solusi ini, lengkap dengan SingletonShared, telah direplikasi secara luas dalam sumber terbuka.

Shared_Example3_correct.cpp
 #include "SingletonShared.h" #include "Payload.h" #include <memory> class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_singleton(SingletonShared<Payload>::instance()) { } ~SharedSingleThreadedUtility() { // Sometimes this check may result as "false" even for destroyed singleton // preventing from visual effects of undefined behaviour ... //if ( m_singleton ) // for ( int i = 0; i < 100; ++i ) // m_singleton->add(i); // ... so this code will allow to demonstrate UB in colour for ( int i = 0; i < 100; ++i ) m_singleton->add(i); } private: // A copy of smart pointer, not a reference std::shared_ptr<SingletonShared<Payload>> m_singleton; }; // 1. Create an empty unique_ptr // 2. Create singleton (because of SharedSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<SharedSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<SharedSingleThreadedUtility>(); int main() { // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order is correct ... // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); // Guaranteed destruction order is the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect... // ... but utility have made a copy of shared_ptr when it was available, // so it's correct again. return 0; } 


Output Konsol
contoh ()
SingletonShared ()
~ SingletonShared ()

Dan sekarang, perhatian, pertanyaannya adalah: apakah Anda benar-benar ingin memperpanjang hidup seorang lajang?
Atau apakah Anda ingin menyingkirkan perilaku tak terbatas, dan memilih perpanjangan hidup sebagai cara berbaring di permukaan?

Ketidaktepatan teoretis dalam bentuk penggantian tujuan dengan cara mengarah pada risiko kebuntuan (atau referensi siklik - sebut saja apa yang Anda inginkan).

Ya nuuuuuuu, ini yang harus kamu coba! Anda harus datang dengan waktu yang begitu lama, dan Anda pasti tidak akan melakukannya secara tidak sengaja!

CallbackPayload.h
 #pragma once #include <functional> class CallbackPayload { public: CallbackPayload() = default; ~CallbackPayload() = default; CallbackPayload(const CallbackPayload &) = delete; CallbackPayload(CallbackPayload &&) = delete; CallbackPayload& operator=(const CallbackPayload &) = delete; CallbackPayload& operator=(CallbackPayload &&) = delete; void setCallback(std::function<void()> &&fn) { m_callbackFn = std::move(fn); } private: std::function<void()> m_callbackFn; }; 


SomethingWithVeryImportantDestructor.h
 #pragma once #include <iostream> class SomethingWithVeryImportantDestructor { public: SomethingWithVeryImportantDestructor() { std::cout << "SomethingWithVeryImportantDestructor()" << std::endl; } ~SomethingWithVeryImportantDestructor() { std::cout << "~SomethingWithVeryImportantDestructor()" << std::endl; } SomethingWithVeryImportantDestructor(const SomethingWithVeryImportantDestructor &) = delete; SomethingWithVeryImportantDestructor(SomethingWithVeryImportantDestructor &&) = delete; SomethingWithVeryImportantDestructor& operator=(const SomethingWithVeryImportantDestructor &) = delete; SomethingWithVeryImportantDestructor& operator=(SomethingWithVeryImportantDestructor &&) = delete; }; 


Shared_Example4_incorrect.cpp
 #include "SingletonShared.h" #include "CallbackPayload.h" #include "SomethingWithVeryImportantDestructor.h" class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_singleton(SingletonShared<CallbackPayload>::instance()) { std::cout << "SharedSingleThreadedUtility()" << std::endl; } ~SharedSingleThreadedUtility() { std::cout << "~SharedSingleThreadedUtility()" << std::endl; } void setCallback(std::function<void()> &&fn) { if ( m_singleton ) m_singleton->setCallback(std::move(fn)); } private: // A copy of smart pointer, not a reference std::shared_ptr<SingletonShared<CallbackPayload>> m_singleton; }; int main() { auto utility = std::make_shared<SharedSingleThreadedUtility>(); auto something = std::make_shared<SomethingWithVeryImportantDestructor>(); // lambda with "utility" and "something" captured utility->setCallback( [utility, something](){} ); return 0; } 


Output Konsol
contoh ()
SingletonShared ()
SharedSingleThreadedUtility ()
SomethingWithVeryImportantDestructor ()

Singleton diciptakan.

Utilitas telah dibuat.

Sesuatu S-Sangat-Penting-Destructor telah dibuat (saya menambahkan ini untuk intimidasi, karena di Internet ada tulisan seperti "well, singleton destructor tidak akan dipanggil, jadi bagaimana dengan ini, itu harus ada sepanjang waktu program ”).

Tapi tidak ada destructor yang dipanggil untuk benda-benda ini!

Karena apa? Karena penggantian tujuan dengan cara.

Singletonweak


SingletonWeak.h
 #pragma once #include <memory> #include <iostream> template<typename T> class SingletonWeak : public T { public: ~SingletonWeak() { std::cout << "~SingletonWeak()" << std::endl; } SingletonWeak(const SingletonWeak &) = delete; SingletonWeak(SingletonWeak &&) = delete; SingletonWeak& operator=(const SingletonWeak &) = delete; SingletonWeak& operator=(SingletonWeak &&) = delete; static std::weak_ptr<SingletonWeak> instance() { std::cout << "instance()" << std::endl; // "new" and no std::make_shared because of private c-tor static auto inst = std::shared_ptr<SingletonWeak>(new SingletonWeak); return inst; } private: SingletonWeak() { std::cout << "SingletonWeak()" << std::endl; } }; 


Modifikasi singleton dalam sumber terbuka, jika diberikan, tentu tidak sering. Saya bertemu dengan beberapa varian aneh yang terbalik dengan std :: lemah_ptr, yang tampaknya digunakan, yang, tampaknya, menawarkan utilitas tidak lebih dari untuk memperpanjang umur seorang singleton:


Opsi yang saya usulkan, bila diterapkan dengan benar di lapisan tunggal dan utilitas:

  • melindungi terhadap tindakan di lapisan pengguna yang dijelaskan dalam contoh di atas, termasuk mencegah kebuntuan;
  • menentukan saat pelipatan aplikasi lebih akurat daripada aplikasi thread_local di Classic_Example3_correct, mis. memungkinkan Anda untuk mendekat ke tepi;
  • Saya tidak menderita dari masalah teoretis dengan mengganti tujuan dengan cara (saya tidak tahu apakah sesuatu yang nyata selain kebuntuan dapat muncul dari masalah teoritis ini).

Namun, ada kekurangannya: memperpanjang umur seorang lajang masih bisa membuatnya semakin dekat ke tepi.

Contoh SingletonWeak 1


Mirip dengan Shared_Example3_correct.cpp.

Weak_Example1_correct.cpp
 #include "SingletonWeak.h" #include "Payload.h" #include <memory> class WeakSingleThreadedUtility { public: WeakSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_weak(SingletonWeak<Payload>::instance()) { } ~WeakSingleThreadedUtility() { // Sometimes this check may result as "false" even in case of incorrect usage, // and there's no way to guarantee a demonstration of undefined behaviour in colour if ( auto strong = m_weak.lock() ) for ( int i = 0; i < 100; ++i ) strong->add(i); } private: // A weak copy of smart pointer, not a reference std::weak_ptr<SingletonWeak<Payload>> m_weak; }; // 1. Create an empty unique_ptr // 2. Create singleton (because of WeakSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<WeakSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<WeakSingleThreadedUtility>(); int main() { // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order is correct ... // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); // Guaranteed destruction order is the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect... // ... but utility have made a weak copy of shared_ptr when it was available, // so it's correct again. return 0; } 


Output Konsol
contoh ()
SingletonWeak ()
~ SingletonWeak ()

Mengapa kita perlu SingletonWeak, karena tidak ada yang mengganggu utilitas untuk menggunakan SingletonShared sebagai SingletonWeak? Ya, tidak ada yang mengganggu. Dan bahkan tidak ada yang mengganggu utilitas untuk menggunakan SingletonWeak sebagai SingletonShared. Tetapi menggunakannya untuk tujuan yang dimaksudkan sedikit lebih mudah daripada menggunakannya untuk tujuan lain.

Contoh SingletonWeak 2


Mirip dengan Shared_Example4_incorrect, tetapi hanya kebuntuan yang tidak terjadi dalam kasus ini.

Weak_Example2_correct.cpp
 #include "SingletonWeak.h" #include "CallbackPayload.h" #include "SomethingWithVeryImportantDestructor.h" class WeakSingleThreadedUtility { public: WeakSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_weak(SingletonWeak<CallbackPayload>::instance()) { std::cout << "WeakSingleThreadedUtility()" << std::endl; } ~WeakSingleThreadedUtility() { std::cout << "~WeakSingleThreadedUtility()" << std::endl; } void setCallback(std::function<void()> &&fn) { if ( auto strong = m_weak.lock() ) strong->setCallback(std::move(fn)); } private: // A weak copy of smart pointer, not a reference std::weak_ptr<SingletonWeak<CallbackPayload>> m_weak; }; int main() { auto utility = std::make_shared<WeakSingleThreadedUtility>(); auto something = std::make_shared<SomethingWithVeryImportantDestructor>(); // lambda with "utility" and "something" captured utility->setCallback( [utility, something](){} ); return 0; } 


Output Konsol
contoh ()
SingletonWeak ()
LemahSingleThreadedUtility ()
SomethingWithVeryImportantDestructor ()
~ SingletonWeak ()
~ SomethingWithVeryImportantDestructor ()
~ LemahSingleThreadedUtility ()

Alih-alih sebuah kesimpulan


Dan apa, modifikasi singleton seperti itu akan menghilangkan perilaku tidak terdefinisi? Saya berjanji tidak akan ada akhir yang bahagia. Contoh-contoh berikut menunjukkan bahwa tindakan sabotase yang terampil di lapisan pengguna dapat menghancurkan bahkan perpustakaan pemikiran yang benar dengan satu singleton (tetapi kita harus mengakui bahwa ini hampir tidak dapat dilakukan secara kebetulan).

Shared_Example5_incorrect.cpp
 #include "SingletonShared.h" #include "Payload.h" #include <memory> #include <cstdlib> class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_singleton(SingletonShared<Payload>::instance()) { } ~SharedSingleThreadedUtility() { // Sometimes this check may result as "false" even for destroyed singleton // preventing from visual effects of undefined behaviour ... //if ( m_singleton ) // for ( int i = 0; i < 100; ++i ) // m_singleton->add(i); // ... so this code will allow to demonstrate UB in colour for ( int i = 0; i < 100; ++i ) m_singleton->add(i); } private: // A copy of smart pointer, not a reference std::shared_ptr<SingletonShared<Payload>> m_singleton; }; void cracker() { SharedSingleThreadedUtility(); } // 1. Register cracker() using std::atexit // 2. Create singleton // 3. Create utility auto reg = [](){ std::atexit(&cracker); return 0; }(); auto utility = SharedSingleThreadedUtility(); // This guarantee destruction in order: // - utility; // - singleton. // This order is correct. // Additionally, there's a copy of shared_ptr in the class instance... // ... but there was std::atexit registered before singleton, // so cracker() will be invoked after destruction of utility and singleton. // There's second try to create a singleton - and it's incorrect. int main() { return 0; } 


Output Konsol
contoh ()
SingletonShared ()
~ SingletonShared ()
contoh ()

Weak_Example3_incorrect.cpp
 #include "SingletonWeak.h" #include "Payload.h" #include <memory> #include <cstdlib> class WeakSingleThreadedUtility { public: WeakSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_weak(SingletonWeak<Payload>::instance()) { } ~WeakSingleThreadedUtility() { // Sometimes this check may result as "false" even in case of incorrect usage, // and there's no way to guarantee a demonstration of undefined behaviour in colour if ( auto strong = m_weak.lock() ) for ( int i = 0; i < 100; ++i ) strong->add(i); } private: // A weak copy of smart pointer, not a reference std::weak_ptr<SingletonWeak<Payload>> m_weak; }; void cracker() { WeakSingleThreadedUtility(); } // 1. Register cracker() using std::atexit // 2. Create singleton // 3. Create utility auto reg = [](){ std::atexit(&cracker); return 0; }(); auto utility = WeakSingleThreadedUtility(); // This guarantee destruction in order: // - utility; // - singleton. // This order is correct. // Additionally, there's a copy of shared_ptr in the class instance... // ... but there was std::atexit registered before singleton, // so cracker() will be invoked after destruction of utility and singleton. // There's second try to create a singleton - and it's incorrect. int main() { return 0; } 


Output Konsol
contoh ()
SingletonWeak ()
~ SingletonWeak ()
contoh ()

Source: https://habr.com/ru/post/id455848/


All Articles