توفر هذه المقالة مضادًا خطيرًا "Zombie" ، والذي ينشأ بشكل طبيعي في بعض الحالات عند استخدام std :: enable_shared_from_this. المواد في مكان ما عند تقاطع C ++ التكنولوجيا الحديثة والهندسة المعمارية.
مقدمة
زودت C ++ 11 المطور بأدوات رائعة للعمل مع الذاكرة - مؤشرات ذكية std :: unique_ptr ومجموعة من std :: shared_ptr + std ::ضعف_ptr. استخدام المؤشرات الذكية للراحة والسلامة يفوق بكثير استخدام المؤشرات الخام. تستخدم المؤشرات الذكية على نطاق واسع في الممارسة ، كما السماح للمطور بالتركيز على المشكلات ذات المستوى الأعلى من تتبع صحة إنشاء / حذف الكيانات المنشأة ديناميكيًا.
يعد قالب الفئة std :: enable_shared_from_this أيضًا جزءًا من المعيار ، ويبدو غريباً عند مواجهته لأول مرة.
سوف يناقش المقال كيف يمكنك أن تتعثر في استخدامه.
برنامج تعليمي
RAII والمؤشرات الذكيةالغرض المباشر من المؤشرات الذكية هو رعاية قطعة ذاكرة الوصول العشوائي المخصصة على الكومة. تقوم المؤشرات الذكية بتطبيق تعريف RAII (الحصول على الموارد هو التهيئة) ويمكن بسهولة تكييفها للعناية بأنواع أخرى من الموارد التي تتطلب التهيئة وإلغاء التهيئة غير التافهة ، مثل:
- الملفات
- المجلدات المؤقتة على القرص ؛
- اتصالات الشبكة (http ، websockets) ؛
- خيوط التنفيذ (خيوط) ؛
- مزادات
- أخرى (وهو ما يكفي للخيال).
لمثل هذا التعميم ، يكفي أن تكتب فصلاً (في الواقع ، في بعض الأحيان لا يمكنك حتى كتابة فصل ، ولكن فقط تستخدم deleter - لكن اليوم لا تكمن الحكاية في ذلك) ، والتي تنفذ:
- التهيئة في المنشئ أو بطريقة منفصلة ؛
- التخفيف في المدمر ،
ثم "لفه" في المؤشر الذكي المقابل ، اعتمادًا على نموذج الملكية المطلوب - المفصل (std :: shared_ptr) أو المفرد (std :: unique_ptr). ينتج عن ذلك "RAII من طبقتين": يتيح لك المؤشر الذكي نقل / مشاركة ملكية المورد ، وتهيئة فئة المستخدم / إلغاء تهيئة مورد غير قياسي.
يستخدم std :: shared_ptr آلية حساب الارتباط. يعرّف المعيار عداد الارتباطات القوية (بحساب عدد النسخ الموجودة من std :: shared_ptr) وعداد الارتباطات الضعيفة (يحسب عدد النسخ الموجودة من std ::ضعف_ptr التي تم إنشاؤها لهذا المثيل من std :: shared_ptr). إن وجود رابط قوي واحد على الأقل يضمن عدم حدوث التدمير بعد. يتم استخدام هذه الخاصية std :: shared_ptr على نطاق واسع لضمان صلاحية كائن حتى يتم الانتهاء من العمل معه في جميع أجزاء البرنامج. لا يمنع وجود رابط ضعيف إتلاف الكائن ويسمح لك بالحصول على رابط قوي حتى يتم إتلافه.
تضمن RAII أن يكون إصدار المورد أكثر موثوقية من الدعوة الصريحة لحذف / حذف [] / free / close / reset / unlock ، للأسباب التالية:
- يمكنك ببساطة نسيان المكالمة الصريحة ؛
- يمكن إجراء مكالمة صريحة عن طريق الخطأ أكثر من مرة ؛
- التحدي الصعب صعب عند تنفيذ الملكية المشتركة للمورد ؛
- تضمن آلية تعزيز المكدس في c ++ استدعاء المدمرات لجميع الكائنات التي تخرج عن نطاقها في حالة وجود استثناء.
ضمان إلغاء التهيئة في المصطلح مهم جدًا لدرجة أنه يستحق مكانًا جيدًا باسم المصطلح مع التهيئة.
المؤشرات الذكية لها أيضًا عيوب:
- وجود النفقات العامة من حيث الأداء والذاكرة (بالنسبة لمعظم التطبيقات ليست كبيرة) ؛
- إمكانية وجود روابط دورية تمنع إطلاق المصدر وتؤدي إلى تسربه.
من المؤكد أن كل مطور قرأ عن الروابط الدائرية وشاهد أمثلة اصطناعية من الكود الإشكالي.
قد يبدو الخطر ضئيلًا للأسباب التالية:
- إذا تسربت الذاكرة بشكل متكرر وكثير - وهذا ملحوظ في استهلاكها ، وإذا كان نادرًا وصغيرًا - فمن غير المحتمل أن تظهر المشكلة على مستوى المستخدم النهائي ؛
- يستخدم تحليل الشفرة الديناميكي للتسريبات (Valgrind ، Clang LeakSanitizer ، وما إلى ذلك) ؛
- "أنا لا أكتب هكذا" ؛
- "معماري صحيح"
"تتم مراجعة الكود الخاص بنا."
الأمراض المنقولة جنسيا :: enable_shared_from_thisفي C ++ 11 ، يتم تقديم فئة المساعد std :: enable_shared_from_this. بالنسبة إلى مطور قام بإنشاء كود بنجاح بدون std :: enable_shared_from_this ، فقد لا تكون الاستخدامات المحتملة لهذه الفئة واضحة.
ماذا تفعل الأمراض المنقولة جنسيا :: enable_shared_from_this؟
يسمح وظائف الأعضاء للفئة التي تم إنشاء مثيل لها في std :: shared_ptr لتلقي نسخ قوية إضافية (shared_from_this ()) أو ضعيفة (ضعف_from_this () ، بدءًا من C ++ 17) بنسخ من std :: shared_ptr التي تم إنشاؤها فيها . لا يمكنك استدعاء shared_from_this () وضعف_from_this () من المُنشئ والمدمّر.
لماذا من الصعب جدا؟ يمكنك ببساطة بناء الأمراض المنقولة جنسيا :: shared_ptr <T> (هذا)
لا ، لا يمكنك ذلك. يجب على جميع الأمراض المنقولة جنسياً :: shared_ptrs التي تهتم بنفس مثيل الفئة استخدام وحدة حساب ارتباط واحد. لا توجد طريقة للاستغناء عن السحر الخاص.
من المتطلبات الأساسية لاستخدام std :: enable_shared_from_this في البداية إنشاء كائن فئة في std :: shared_ptr. إنشاء على المكدس ، تخصيص حيوي على الكومة ، إنشاء على std :: unique_ptr - كل هذا غير مناسب. فقط بدقة في الأمراض المنقولة جنسيا :: shared_ptr.
ولكن هل من الممكن تقييد المستخدم في طريقة إنشاء مثيلات الفصل؟
نعم يمكنك ذلك. للقيام بذلك ، فقط:
- توفير طريقة ثابتة لإنشاء مثيلات وضعت أصلاً في الأمراض المنقولة جنسياً :: shared_ptr ؛
- وضع المنشئ في الخصوصية أو المحمية ؛
- يحظر نسخ ونقل الدلالات.
ذهب الفصل إلى القفص ، وأغلقه وابتلع المفتاح - من الآن فصاعدًا ، ستعيش جميع مثيلاته فقط في الأمراض المنقولة جنسياً :: shared_ptr ، ولا توجد طرق قانونية لإخراجهم من هناك.
لا يمكن أن يسمى هذا التقييد حلاً معماريًا جيدًا ، لكن هذه الطريقة تتوافق تمامًا مع المعيار.
بالإضافة إلى ذلك ، يمكنك استخدام تعبير PIMPL: المستخدم الوحيد للفئة متقلبة - الواجهة - سيخلق التنفيذ بدقة في std :: shared_ptr ، وستُحرم الواجهة نفسها بالفعل من قيود من هذا النوع.
الأمراض المنقولة جنسياً :: enable_shared_from_this لها الفروق الدقيقة في الميراث ، ولكن مناقشتها خارج نطاق هذه المقالة.
وصول الى هذه النقطة
يتم نشر جميع أمثلة التعليمات البرمجية الواردة في المقالة على
جيثب .
يوضح الكود التقنيات السيئة المتنكرة في الاستخدام الآمن المعتاد لـ C ++ الحديثة
SimpleCyclic
يبدو أن لا شيء ينذر المشاكل. إعلان فئة تبدو بسيطة ومباشرة. باستثناء واحد من التفاصيل "الصغيرة" - يتم تطبيق الميراث من الأمراض المنقولة جنسيا :: enable_shared_from_this لسبب ما.
SimpleCyclic.h#pragma once #include <memory> #include <functional> namespace SimpleCyclic { class Cyclic final : public std::enable_shared_from_this<Cyclic> { public: static std::shared_ptr<Cyclic> create(); Cyclic(const Cyclic&) = delete; Cyclic(Cyclic&&) = delete; Cyclic& operator=(const Cyclic&) = delete; Cyclic& operator=(Cyclic&&) = delete; ~Cyclic(); void doSomething(); private: Cyclic(); std::function<void(void)> _fn; }; } // namespace SimpleCyclic
وفي التنفيذ:
SimpleCyclic.cpp #include <iostream> #include "SimpleCyclic.h" namespace SimpleCyclic { Cyclic::Cyclic() = default; Cyclic::~Cyclic() { std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } std::shared_ptr<Cyclic> Cyclic::create() { return std::shared_ptr<Cyclic>(new Cyclic); } void Cyclic::doSomething() { _fn = [shis = shared_from_this()](){}; std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } } // namespace SimpleCyclic
MAIN.CPP #include "SimpleCyclic/SimpleCyclic.h" int main() { auto simpleCyclic = SimpleCyclic::Cyclic::create(); simpleCyclic->doSomething(); return 0; }
إخراج وحدة التحكمN12SimpleCyclic6CyclicE :: doSomething
في نص الدالة doSomething () ، سينشئ مثيل الفئة
نفسه نسخة قوية إضافية من std :: shared_ptr الذي تم وضعه فيه. ثم ، باستخدام التقاط معمم ، يتم وضع هذه النسخة في دالة lambda المخصصة لحقل بيانات الفئة تحت ستار دالة std :: function غير ضارة. ينتج عن استدعاء doSomething () مرجع دائري ، ولن يتم إتلاف مثيل الفئة حتى بعد تدمير جميع الروابط الخارجية القوية.
هناك تسرب للذاكرة. لا يسمى destructor SimpleCyclic :: Cyclic :: ~ دوري.
المثيل الطبقي "يحفظ" نفسه.
رمز مرتبطة عقدة.
(الصورة مأخوذة
من هنا )
وماذا ، هذا هو antipattern "غيبوبة"؟لا ، هذا مجرد تمرين. الأكثر إثارة للاهتمام لم يأت بعد.
لماذا المطور يكتب هذا؟مثال اصطناعي. لست على دراية بأي حالات يتم فيها الحصول على مثل هذا الرمز بشكل متناغم.
فهل ظل تحليل الشفرة الديناميكي صامتاً؟لا ، أبلغ Valgrind عن حدوث تسرب للذاكرة بأمانة:
مشاركة فالجرندبالتأكيد يتم فقد 96 (64 مباشر ، 32 غير مباشر) في كتل واحدة في سجل الخسارة 29 من 46
في SimpleCyclic :: Cyclic :: create () في / المستخدمين / المستخدم / المشاريع / Zomby_antipattern_concept/SimpleCyclic/SimpleCyclic.cpp:15
1: malloc in /usr/local/Cellar/valgrind/HEAD-60ab74a/lib/valgrind/vgpreload_memcheck-amd64-darwin.so
2: عامل التشغيل الجديد (طويل غير موقّع) في /usr/lib/libc++abi.dylib
3: SimpleCyclic :: Cyclic :: create () في / المستخدمين / المستخدم / المشروعات / Zomby_antipattern_concept/SimpleCyclic/SimpleCyclic.cpp:15
4: رئيسي في / المستخدمين / المستخدم / المشاريع / Zomby_antipattern_concept/SimpleCyclic/main.cpphaps
PimplCyclic
في هذه الحالة ، يبدو ملف الرأس صحيحًا وموجزًا تمامًا. أعلنت الواجهة التي تخزن تطبيق معين في الأمراض المنقولة جنسيا :: shared_ptr. الوراثة - بما في ذلك من الأمراض المنقولة جنسيا :: enable_shared_from_this - مفقودة ، على عكس المثال السابق.
PimplCyclic.h #pragma once #include <memory> namespace PimplCyclic { class Cyclic { public: Cyclic(); ~Cyclic(); private: class Impl; std::shared_ptr<Impl> _impl; }; } // namespace PimplCyclic
وفي التنفيذ:
PimplCyclic.cpp #include <iostream> #include <functional> #include "PimplCyclic.h" namespace PimplCyclic { class Cyclic::Impl : public std::enable_shared_from_this<Cyclic::Impl> { public: ~Impl() { std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } void doSomething() { _fn = [shis = shared_from_this()](){}; std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } private: std::function<void(void)> _fn; }; Cyclic::Cyclic() : _impl(std::make_shared<Impl>()) { if (_impl) { _impl->doSomething(); } } Cyclic::~Cyclic() { std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } } // namespace PimplCyclic
MAIN.CPP #include "PimplCyclic/PimplCyclic.h" int main() { auto pimplCyclic = PimplCyclic::Cyclic(); return 0; }
إخراج وحدة التحكمN11PimplCyclic6Cyclic4ImplE :: doSomething
N11PimplCyclic6CyclicE :: ~ دوري
استدعاء Impl :: doSomething () بإنشاء مرجع دائري في مثيل فئة Impl. تم تدمير الواجهة بشكل صحيح ، لكن التنفيذ يتسرب. لا يتم استدعاء destructor PimplCyclic :: Cyclic :: Impl :: ~ Impl.
المثال اصطناعي مرة أخرى ، لكن هذه المرة أكثر خطورة - كل المعدات السيئة موجودة في التطبيق ولا تظهر في الإعلان.
علاوة على ذلك ، لإنشاء رابط دائري ، لا يتطلب رمز المستخدم أي إجراء آخر غير الإنشاء.
تحليل ديناميكي في مواجهة Valgrind ، وهذه المرة كشفت تسرب:
مشاركة فالجرندتم فقد 96 بايت في كتل واحدة بالتأكيد في سجل الخسارة 29 من 46
في PimplCyclic :: Cyclic :: Cyclic () في / المستخدمين / المستخدم / المشاريع / Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:28
1: malloc in /usr/local/Cellar/valgrind/HEAD-60ab74a/lib/valgrind/vgpreload_memcheck-amd64-darwin.so
2: عامل التشغيل الجديد (طويل غير موقّع) في /usr/lib/libc++abi.dylib
3: الأمراض المنقولة جنسيا :: __ 1 :: __ libcpp_allocate (طويلة غير موقعة ، طويلة غير موقعة) في /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/new:252
4: std :: __ 1 :: customator <std :: __ 1 :: __ shared_ptr_emplace <PimplCyclic :: Cyclic :: Impl، std :: __ 1 :: مخصص <PimplCyclic :: Cyclic :: Impl >>> تخصيص (طويلة غير موقعة ، باطلة const *) في /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:1813
5: std :: __ 1 :: shared_ptr <PimplCyclic :: Cyclic :: Impl> std :: __ 1 :: shared_ptr <PimplCyclic :: Cyclic :: Impl> :: make_shared <> () في /Applications/Xcode.app/Contents /Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:4326
6: _ZNSt3__1L11make_sharedIN11PimplCyclic6Cyclic4ImplEJEEENS_9enable_ifIXntsr8is_arrayIT_EE5valueENS_10shared_ptrIS5_EEE4typeEDpOT0_ في /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:4706
7: PimplCyclic :: Cyclic :: Cyclic () في / المستخدمين / المستخدم / المشاريع / Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:28
8: PimplCyclic :: Cyclic :: Cyclic () في / المستخدمين / المستخدم / المشاريع / Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:29
9: رئيسي في / المستخدمين / المستخدم / المشاريع / Zomby_antipattern_concept/PimplCyclic/main.cpphaps
من المشبوه بعض الشيء رؤية Pimpl ، حيث يتم تخزين التطبيق في الأمراض المنقولة جنسيا :: shared_ptr.Pimpl الكلاسيكي المبني على مؤشر خام قديم جدًا ، كما أن std :: unique_ptr له تأثير جانبي في نشر الحظر على نسخ الدلالات على الواجهة. ستنفذ هذه الواجهة لغة الملكية الوحيدة ، والتي قد لا تتوافق مع الفكرة المعمارية. من استخدام std :: shared_ptr لتخزين التطبيق ، نستنتج أن الفئة مصممة لتوفير ملكية مشتركة.
كيف يختلف هذا عن الذاكرة المخصصة للتسرب الكلاسيكي من خلال الاتصال الجديد صراحة دون حذف لاحق؟ بنفس الطريقة ، سيكون كل شيء جميلًا في الواجهة وفي التنفيذ - خطأ.نحن نناقش الطرق
الحديثة لاطلاق النار على نفسك في القدم.
أنتيباترن "الزومبي"
لذلك ، من المواد المذكورة أعلاه فمن الواضح:
- يمكن ربط المؤشرات الذكية بالعقد ؛
- استخدام std :: enable_shared_from_this يمكن أن يساهم في ذلك ، لأنه يسمح لمثيل من الفصل بربط عقدة بدون مساعدة خارجية تقريبًا.
والآن - انتباه - السؤال الرئيسي للمقال: هل نوع المورد ملفوف في مؤشر ذكي المسألة؟ هل هناك فرق بين رعاية ملفات RAII واتصال HTTPS غير المتزامن؟SimpleZomby
تم نقل الكود الشائع لجميع أمثلة الكسالى اللاحقة إلى المكتبة العامة.
واجهة غيبوبة مجردة مع مدير اسم متواضع:
مشترك / مدير #pragma once #include <memory> namespace Common { class Listener; class Manager { public: Manager() = default; Manager(const Manager&) = delete; Manager(Manager&&) = delete; Manager& operator=(const Manager&) = delete; Manager& operator=(Manager&&) = delete; virtual ~Manager() = default; virtual void runOnce(std::shared_ptr<Common::Listener> listener) = 0; }; } // namespace Common
واجهة المستمع المجردة ، جاهزة لقبول نص آمن في سلسلة الرسائل:
مشترك / المستمع #pragma once #include <string> #include <memory> namespace Common { class Listener { public: virtual ~Listener() = default; using Data = std::string; // thread-safe virtual void processData(const std::shared_ptr<const Data> data) = 0; }; } // namespace Common
المستمع الذي يعرض النص إلى وحدة التحكم. تنفذ مفهوم SingletonShared من مقالتي
تقنية لتجنب السلوك غير المحدد عند الاتصال بـ Singleton :
Common / Impl / WriteToConsoleListener.h #pragma once #include <mutex> #include "Common/Listener.h" namespace Common { class WriteToConsoleListener final : public Listener { public: WriteToConsoleListener(const WriteToConsoleListener&) = delete; WriteToConsoleListener(WriteToConsoleListener&&) = delete; WriteToConsoleListener& operator=(const WriteToConsoleListener&) = delete; WriteToConsoleListener& operator=(WriteToConsoleListener&&) = delete; ~WriteToConsoleListener() override; static std::shared_ptr<WriteToConsoleListener> instance(); // blocking void processData(const std::shared_ptr<const Data> data) override; private: WriteToConsoleListener(); std::mutex _mutex; }; } // namespace Common
Common / Impl / WriteToConsoleListener.cpp #include <iostream> #include "WriteToConsoleListener.h" namespace Common { WriteToConsoleListener::WriteToConsoleListener() = default; WriteToConsoleListener::~WriteToConsoleListener() { auto lock = std::lock_guard(_mutex); std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } std::shared_ptr<WriteToConsoleListener> WriteToConsoleListener::instance() { static auto inst = std::shared_ptr<WriteToConsoleListener>(new WriteToConsoleListener); return inst; } void WriteToConsoleListener::processData(const std::shared_ptr<const Data> data) { if (data) { auto lock = std::lock_guard(_mutex); std::cout << *data << std::flush; } } } // namespace Common
وأخيرا ، أول غيبوبة ، أبسط والأكثر براعة.
SimpleZomby.h #pragma once #include <memory> #include <atomic> #include <thread> #include "Common/Manager.h" namespace Common { class Listener; } // namespace Common namespace SimpleZomby { class Zomby final : public Common::Manager, public std::enable_shared_from_this<Zomby> { public: static std::shared_ptr<Zomby> create(); ~Zomby() override; void runOnce(std::shared_ptr<Common::Listener> listener) override; private: Zomby(); using Semaphore = std::atomic<bool>; std::shared_ptr<Common::Listener> _listener; Semaphore _semaphore = false; std::thread _thread; }; } // namespace SimpleZomby
SimpleZomby.cpp #include <sstream> #include "SimpleZomby.h" #include "Common/Listener.h" namespace SimpleZomby { std::shared_ptr<Zomby> Zomby::create() { return std::shared_ptr<Zomby>(new Zomby()); } Zomby::Zomby() = default; Zomby::~Zomby() { _semaphore = false; if (_thread.joinable()) { _thread.detach(); } if (_listener) { std::ostringstream buf; buf << typeid(*this).name() << "::" << __func__ << std::endl; _listener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) { if (_semaphore) { throw std::runtime_error("SimpleZomby::Zomby::runOnce() called twice"); } _listener = listener; _semaphore = true; _thread = std::thread([shis = shared_from_this()](){ while (shis && shis->_listener && shis->_semaphore) { shis->_listener->processData(std::make_shared<Common::Listener::Data>("SimpleZomby is alive!\n")); std::this_thread::sleep_for(std::chrono::seconds(1)); } }); } } // namespace SimpleZomby
يدير الزومبي وظيفة lambda في خيط منفصل ، ويرسل دوريا سلسلة إلى المستمع. تحتاج وظائف Lambda للعمل إلى إشارة وسماع ، وهي حقول من فئة zombie. لا تقوم وظيفة lambda بالتقاطها كحقول منفصلة ، ولكنها تستخدم الكائن كمجمع. سيؤدي تدمير مثيل لفئة zombie قبل اكتمال وظيفة lambda إلى حدوث سلوك غير محدد. لتجنب ذلك ، تلتقط وظيفة lambda نسخة قوية من shared_from_this ().
في المدمر الزومبي ، يتم تعيين الإشارة إلى خطأ ، وبعد ذلك يتم استدعاء فصل () للدفق. تعيين إشارة يخبر مؤشر الترابط لإيقاف.
في destructor ، كان من الضروري استدعاء عدم فصل () ، ولكن الانضمام ()!... واحصل على أداة إتلاف تمنع التنفيذ لفترة غير محددة ، وهو ما قد يكون غير مقبول.
لذلك هذا انتهاك RAII! كان من المفترض RAII للخروج من المدمر إلا بعد تحرير المورد!إذا كان بصرامة - ثم نعم ، لا يقوم المدمر الزومبي بإطلاق المصدر ، لكنه
يضمن فقط
إصداره . أنتجت في وقت ما - ربما في وقت قريب ، أو ربما ليس حقا. وحتى أنه من الممكن أن ينتهي الرئيسي من العمل في وقت مبكر - ثم سيتم مسح الخيط بالقوة من قبل نظام التشغيل. ولكن في الواقع ، يمكن أن يكون الخط الفاصل بين RAII "الصحيح" و "الخاطئ" رفيعًا جدًا: على سبيل المثال ، RAII "الصحيح" ، والذي يستدعي std :: filesystem :: remove () في أداة إتلاف لملف مؤقت ، قد يعيد التحكم إلى ذلك اللحظة التي سيظل فيها أمر الكتابة في أي من ذاكرات التخزين المؤقت المتطايرة ولن تتم كتابته بأمانة على اللوحة المغناطيسية للقرص الثابت.
MAIN.CPP #include <chrono> #include <thread> #include <sstream> #include "Common/Impl/WriteToConsoleListener.h" #include "SimpleZomby/SimpleZomby.h" int main() { auto writeToConsoleListener = Common::WriteToConsoleListener::instance(); { auto simpleZomby = SimpleZomby::Zomby::create(); simpleZomby->runOnce(writeToConsoleListener); std::this_thread::sleep_for(std::chrono::milliseconds(4500)); } // Zomby should be killed here { std::ostringstream buf; buf << "============================================================\n" << "| Zomby was killed |\n" << "============================================================\n"; if (writeToConsoleListener) { writeToConsoleListener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } std::this_thread::sleep_for(std::chrono::milliseconds(5000)); return 0; }
إخراج وحدة التحكمSimpleZomby على قيد الحياة!
SimpleZomby على قيد الحياة!
SimpleZomby على قيد الحياة!
SimpleZomby على قيد الحياة!
SimpleZomby على قيد الحياة!
================================================== ==========
| قتل زومبي |
================================================== ==========
SimpleZomby على قيد الحياة!
SimpleZomby على قيد الحياة!
SimpleZomby على قيد الحياة!
SimpleZomby على قيد الحياة!
SimpleZomby على قيد الحياة!
ما يمكن رؤيته من مخرجات البرنامج:
- استمر الزومبي في العمل حتى بعد مغادرة مجال الرؤية ؛
- لم يتم استدعاء أي مدمرات إما من الزومبي أو WriteToConsoleListener.
حدث تسرب للذاكرة.
كان هناك تسرب للموارد. والمورد في هذه الحالة هو موضوع التنفيذ.
استمرت التعليمة البرمجية التي كان من المفترض أن تتوقف عن العمل في مؤشر ترابط منفصل.
كان من الممكن منع حدوث تسرب لـ WriteToConsoleListener باستخدام تقنية SingletonWeak من مقالتي ،
تجنب السلوك غير المحدد عند الاتصال بـ Singleton ، لكنني لم أقم بذلك.

(الصورة مأخوذة
من هنا )
لماذا الكسالى؟لأنه قُتل وما زال حياً.
كيف يختلف هذا عن المراجع الدائرية في الأمثلة السابقة؟حقيقة أن المورد المفقود ليس مجرد جزء من الذاكرة ، ولكنه شيء ينفذ التعليمات البرمجية بشكل مستقل عن الخيط الذي أطلقه.
هل من الممكن تدمير "الزومبي"بعد ترك النطاق (أي بعد تدمير كل الإشارات الخارجية القوية والضعيفة إلى الزومبي) - هذا أمر مستحيل. سيتم تدمير الزومبي عندما يقرر تدمير نفسه (نعم ، إنه شيء ذو سلوك نشط) ، وربما لن يحدث أبدًا ، على سبيل المثال سوف يستمر حتى ينظف نظام التشغيل عند إنهاء التطبيق. بالطبع ، قد يكون لرمز المستخدم بعض التأثير على شرط الخروج من رمز zombie ، لكن هذا التأثير سيكون غير مباشر ويعتمد على التطبيق.
وقبل مغادرة النطاق؟يمكنك استدعاء zombie destructor بشكل صريح ، لكن من غير المحتمل أن تتجنب أي سلوك غير محدد بسبب التدمير المتكرر للكائن بواسطة destructor الذكي للمؤشر كذلك - هذه معركة ضد RAII. أو يمكنك إضافة وظيفة إلغاء التهيئة الصريحة - وهذا رفض لـ RAII.
كيف يختلف هذا عن مجرد بدء سلسلة رسائل متبوعة بـ detach ()؟في حالة الزومبي ، على عكس دعوة بسيطة للفصل () ، هناك فكرة لإيقاف التدفق. فقط لا يعمل. إن وجود الفكرة الصحيحة يساعد على إخفاء المشكلة.
هل لا يزال المثال اصطناعيًا؟جزئيا في هذا المثال البسيط ، لم يكن هناك ما يكفي من الأسباب لاستخدام Shared_from_this () - على سبيل المثال ، يمكنك القيام بالتقاط الضعف () أو التقاط جميع الحقول المطلوبة في الفصل الدراسي. ولكن مع تعقيد المهمة ، قد يتحول التوازن إلى الجانب
Shared_from_this ().
فالجرند ، فالجرند! لدينا خط دفاع إضافي ضد الزومبي!للأسف وآه - ولكن Valgrind لم يكشف عن تسرب للذاكرة. لماذا - أنا لا أعرف. في التشخيص ، لا يوجد سوى إدخالات
"ربما ضائعة" تشير إلى وظائف النظام - تقريبًا تقريبًا وبنفس المقدار كما هو الحال عند استخدام مفتاح فارغ. لا توجد مراجع كود المستخدم. قد تكون أدوات التحليل الديناميكي الأخرى أفضل ، ولكن إذا كنت لا تزال تعتمد عليها ، فاقرأ عليها.
SteppingZomby
ينتقل الرمز في هذا المثال إلى خطوات solutionDnsName ---> connectTcp ---> EstablishSsl ---> sendHttpRequest ---> readHttpReply ، محاكاة تشغيل اتصال HTTPS للعميل في التنفيذ غير المتزامن. كل خطوة تستغرق حوالي ثانية.
SteppingZomby.h #pragma once #include <memory> #include <atomic> #include <thread> #include "Common/Manager.h" namespace Common { class Listener; } // namespace Common namespace SteppingZomby { class Zomby final : public Common::Manager, public std::enable_shared_from_this<Zomby> { public: static std::shared_ptr<Zomby> create(); ~Zomby() override; void runOnce(std::shared_ptr<Common::Listener> listener) override; private: Zomby(); using Semaphore = std::atomic<bool>; std::shared_ptr<Common::Listener> _listener; Semaphore _semaphore = false; std::thread _thread; void resolveDnsName(); void connectTcp(); void establishSsl(); void sendHttpRequest(); void readHttpReply(); }; } // namespace SteppingZomby
SteppingZomby.cpp #include <sstream> #include <string> #include "SteppingZomby.h" #include "Common/Listener.h" namespace { void doSomething(Common::Listener& listener, std::string&& callingFunctionName) { listener.processData(std::make_shared<Common::Listener::Data>(callingFunctionName + " started\n")); std::this_thread::sleep_for(std::chrono::milliseconds(1000)); listener.processData(std::make_shared<Common::Listener::Data>(callingFunctionName + " finished\n")); } } // namespace namespace SteppingZomby { Zomby::Zomby() = default; std::shared_ptr<Zomby> Zomby::create() { return std::shared_ptr<Zomby>(new Zomby()); } Zomby::~Zomby() { _semaphore = false; if (_thread.joinable()) { _thread.detach(); } if (_listener) { std::ostringstream buf; buf << typeid(*this).name() << "::" << __func__ << std::endl; _listener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) { if (_semaphore) { throw std::runtime_error("SteppingZomby::Zomby::runOnce() called twice"); } _listener = listener; _semaphore = true; _thread = std::thread([shis = shared_from_this()](){ if (shis && shis->_listener && shis->_semaphore) { shis->resolveDnsName(); } if (shis && shis->_listener && shis->_semaphore) { shis->connectTcp(); } if (shis && shis->_listener && shis->_semaphore) { shis->establishSsl(); } if (shis && shis->_listener && shis->_semaphore) { shis->sendHttpRequest(); } if (shis && shis->_listener && shis->_semaphore) { shis->readHttpReply(); } }); } void Zomby::resolveDnsName() { doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__); } void Zomby::connectTcp() { doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__); } void Zomby::establishSsl() { doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__); } void Zomby::sendHttpRequest() { doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__); } void Zomby::readHttpReply() { doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__); } } // namespace SteppingZomby
MAIN.CPP #include <chrono> #include <thread> #include <sstream> #include "SteppingZomby/SteppingZomby.h" #include "Common/Impl/WriteToConsoleListener.h" int main() { auto writeToConsoleListener = Common::WriteToConsoleListener::instance(); { auto steppingZomby = SteppingZomby::Zomby::create(); steppingZomby->runOnce(writeToConsoleListener); std::this_thread::sleep_for(std::chrono::milliseconds(1500)); } // Zombies should be killed here { std::ostringstream buf; buf << "============================================================\n" << "| Zomby was killed |\n" << "============================================================\n"; if (writeToConsoleListener) { writeToConsoleListener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } std::this_thread::sleep_for(std::chrono::milliseconds(5000)); return 0; }
إخراج وحدة التحكمبدأ N13SteppingZomby5ZombyE :: solutionDnsName
الانتهاء من N13SteppingZomby5ZombyE :: solutionDnsName
بدأ N13SteppingZomby5ZombyE :: connectTcp
================================================== ==========
| قتل زومبي |
================================================== ==========
N13SteppingZomby5ZombyE :: connectTcp الانتهاء
بدأ N13SteppingZomby5ZombyE :: EstablishSsl
N13SteppingZomby5ZombyE :: EstablishSsl الانتهاء
بدأ N13SteppingZomby5ZombyE :: sendHttpRequest
N13SteppingZomby5ZombyE :: sendHttpRequest الانتهاء
بدأ N13SteppingZomby5ZombyE :: readHttpReply
N13SteppingZomby5ZombyE :: readHttpReply انتهى
N13SteppingZomby5ZombyE :: ~ Zomby
N6Common22WriteToConsoleListenerE :: ~ WriteToConsoleListener
كما في المثال السابق ، أدت استدعاء runOnce () إلى إشارة دائرية.
ولكن هذه المرة ، تم استدعاء المدمرات Zomby و WriteToConsoleListener. تم إصدار جميع الموارد بشكل صحيح حتى يتم إنهاء التطبيق. لم يحدث تسرب للذاكرة.
ما هي المشكلة إذن؟المشكلة هي أن الزومبي عاش لفترة طويلة - حوالي ثلاث ثوان ونصف بعد تدمير جميع الروابط الخارجية القوية والضعيفة بها. حوالي ثلاث ثوان أطول مما كان ينبغي أن يعيش. وطوال هذه الفترة ، شارك في الترويج لتنفيذ اتصال HTTPS - حتى وصل إلى نهايته. على الرغم من حقيقة أن النتيجة لم تعد هناك حاجة. على الرغم من حقيقة أن منطق الأعمال المتفوق حاول إيقاف الزومبي.
حسنًا ، فكر في الأمر ، لقد حصلت على الإجابة التي لا تحتاجها ....في حالة اتصال HTTPS للعميل ، قد تكون العواقب
على جانبنا على النحو التالي:
- استهلاك الذاكرة ؛
- استهلاك وحدة المعالجة المركزية ؛
- استهلاك منفذ TCP ؛
- عرض النطاق الترددي لقناة الاتصال (يمكن أن يكون كل من الطلب والاستجابة وحدة تخزين بالميغابايت) ؛
- قد تتسبب البيانات غير المتوقعة في تعطيل تشغيل منطق الأعمال ذي المستوى الأعلى - حتى الانتقال إلى فرع التنفيذ الخاطئ أو إلى سلوك غير محدد ، لأن قد يتم بالفعل تدمير آليات معالجة الاستجابة.
وعلى
الجانب البعيد (لا تنسى - طلب HTTPS مخصص لشخص ما) - بالضبط نفس مضيعة الموارد ، بالإضافة إلى أنه ممكن:
- نشر صور القطط على موقع الشركة.
- تعطيل التدفئة تحت البلاط في المطبخ الخاص بك ؛
- تنفيذ أمر تجاري في البورصة ؛
- تحويل الأموال من حسابك ؛
- إطلاق صاروخ باليستي عابر للقارات.
حاول منطق العمل إيقاف الزومبي عن طريق إزالة جميع الروابط القوية والضعيفة.
كان من المفترض أن يتم إيقاف تقدم طلب HTTPS - فلم يفت الأوان بعد ، ولم يتم إرسال بيانات مستوى التطبيق بعد.
لكن الكسالى قررت بطريقتها الخاصة.
يمكن منطق الأعمال إنشاء كائنات جديدة في مكان الكسالى ومحاولة مرة أخرى تدميرها ، وضرب استنزاف الموارد.
في حالة وجود عملية مستمرة (على سبيل المثال ، اتصال Websocket) ، يمكن أن تستمر هدر الموارد لساعات ، وإذا كانت هناك آلية لإعادة الاتصال التلقائي في التنفيذ عندما يتم قطع الاتصال ، يمكن إيقافها بشكل عام.
Valgrind؟لا فرصة كل شيء صدر بشكل صحيح وتنظيفها. في وقت متأخر وليس من الخيط الرئيسي ، ولكن صحيح تماما.
BoozdedZomby
يستخدم هذا المثال مكتبة boozd :: azzio ، التي تعد تقليدًا للدعم :: asio. على الرغم من حقيقة أن التقليد الخام إلى حد ما ، فإنه يسمح لنا لإظهار جوهر المشكلة.
تحتوي المكتبة على وظيفة io_context :: async_read (في الأصل ، إنها مجانية ، ولكنها لا تغير الجوهر) ، والتي تقبل:- دفق ، يمكن أن تأتي منه البيانات ؛- مخزن مؤقت يسمح لك بتجميع هذه البيانات ؛- وظيفة رد الاتصال التي سيتم استدعاؤها عند الانتهاء من قراءة البيانات.يتم تنفيذ وظيفة io_context :: async_read على الفور ولا تستدعي معاودة الاتصال ، حتى لو كانت نتيجة التنفيذ معروفة بالفعل (على سبيل المثال ، خطأ). يتم استدعاء رد الاتصال فقط من وظيفة الحظر io_context :: run () (في الأصل ، هناك وظائف أخرى مصممة لاستدعاء عمليات رد الاتصال بمجرد أن تكون البيانات جاهزة).buffer.h #pragma once #include <vector> namespace boozd::azzio { using buffer = std::vector<int>; } // namespace boozd::azzio
stream.h #pragma once #include <optional> namespace boozd::azzio { class stream { public: virtual ~stream() = default; virtual std::optional<int> read() = 0; }; } // namespace boozd::azzio
io_context.h #pragma once #include <functional> #include <optional> #include "buffer.h" namespace boozd::azzio { class stream; class io_context { public: ~io_context(); enum class error_code {no_error, good_error, bad_error, unknown_error, known_error, well_known_error}; using handler = std::function<void(error_code)>; // Start an asynchronous operation to read a certain amount of data from a stream. // This function is used to asynchronously read a certain number of bytes of data from a stream. // The function call always returns immediately. void async_read(stream& s, buffer& b, handler&& handler); // Run the io_context object's event processing loop. void run(); private: using pack = std::tuple<stream&, buffer&>; using pack_optional = std::optional<pack>; using handler_optional = std::optional<handler>; pack_optional _pack_optional; handler_optional _handler_optional; }; } // namespace boozd::azzio
io_context.cpp #include <iostream> #include <thread> #include <chrono> #include "io_context.h" #include "stream.h" namespace boozd::azzio { io_context::~io_context() { std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } void io_context::async_read(stream& s, buffer& b, io_context::handler&& handler) { _pack_optional.emplace(s, b); _handler_optional.emplace(std::move(handler)); } void io_context::run() { if (_pack_optional && _handler_optional) { auto& [s, b] = *_pack_optional; using namespace std::chrono; auto start = steady_clock::now(); while (duration_cast<milliseconds>(steady_clock::now() - start).count() < 1000) { if (auto read = s.read()) b.emplace_back(*read); std::this_thread::sleep_for(milliseconds(100)); } (*_handler_optional)(error_code::no_error); } } } // namespace boozd::azzio
تطبيق واجهة boozd :: azzio :: stream الوحيد الذي ينتج بيانات عشوائية:impl / random_stream.h #pragma once #include "boozd/azzio/stream.h" namespace boozd::azzio { class random_stream final : public stream { public: ~random_stream() override; std::optional<int> read() override; }; } // namespace boozd::azzio
impl / random_stream.cpp #include <iostream> #include "random_stream.h" namespace boozd::azzio { boozd::azzio::random_stream::~random_stream() { std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } std::optional<int> random_stream::read() { if (!(rand() & 0x1)) return rand(); return std::nullopt; } } // namespace boozd::azzio
تدير BoozdedZomby وظيفة lambda في خيط منفصل. تقوم الدالة lambda بتسجيل المعالج عن طريق استدعاء async_read () ، وبعد ذلك تمنح التحكم للآليات الداخلية لـ boozd :: azzio باستخدام run (). بعد ذلك ، يمكن للآليات الداخلية لـ boozd :: azzio إجراء مكالمات إلى المخزن المؤقت ودفق (مصدر البيانات) في أي وقت قبل استدعاء وظيفة رد الاتصال. لضمان صحة العديد من الكائنات التي تم تجميعها في مثيل للفئة ، تلتقط وظيفة lambda shared_from_this.BoozdedZomby.h #pragma once #include <memory> #include <atomic> #include <thread> #include "Common/Manager.h" #include "boozd/azzio/buffer.h" #include "boozd/azzio/io_context.h" #include "boozd/azzio/impl/random_stream.h" namespace Common { class Listener; } // namespace Common namespace BoozdedZomby { class Zomby final : public Common::Manager, public std::enable_shared_from_this<Zomby> { public: static std::shared_ptr<Zomby> create(); ~Zomby() override; void runOnce(std::shared_ptr<Common::Listener> listener) override; private: Zomby(); using Semaphore = std::atomic<bool>; Semaphore _semaphore = false; std::shared_ptr<Common::Listener> _listener; boozd::azzio::random_stream _stream; boozd::azzio::buffer _buffer; boozd::azzio::io_context _context; std::thread _thread; }; } // namespace BoozdedZomby
BoozdedZomby.cpp #include <iostream> #include <sstream> #include "boozd/azzio/impl/random_stream.h" #include "BoozdedZomby.h" #include "Common/Listener.h" namespace BoozdedZomby { Zomby::Zomby() = default; std::shared_ptr<Zomby> Zomby::create() { return std::shared_ptr<Zomby>(new Zomby()); } Zomby::~Zomby() { _semaphore = false; if (_thread.joinable()) { _thread.detach(); } if (_listener) { std::ostringstream buf; buf << typeid(*this).name() << "::" << __func__ << std::endl; _listener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) { if (_semaphore) { throw std::runtime_error("BoozdedZomby::Zomby::runOnce() called twice"); } _listener = listener; _semaphore = true; _thread = std::thread([shis = shared_from_this()]() { while (shis && shis->_semaphore && shis->_listener) { auto handler = [shis](auto errorCode) { if (shis && shis->_listener && errorCode == boozd::azzio::io_context::error_code::no_error) { std::ostringstream buf; buf << "BoozdedZomby has got a fresh data: "; for (auto const &elem : shis->_buffer) buf << elem << ' '; buf << std::endl; shis->_listener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } }; shis->_buffer.clear(); shis->_context.async_read(shis->_stream, shis->_buffer, handler); shis->_context.run(); } }); } } // namespace BoozdedZomby
MAIN.CPP #include <chrono> #include <thread> #include <sstream> #include "BoozdedZomby/BoozdedZomby.h" #include "Common/Impl/WriteToConsoleListener.h" int main() { auto writeToConsoleListener = Common::WriteToConsoleListener::instance(); { auto boozdedZomby = BoozdedZomby::Zomby::create(); boozdedZomby->runOnce(writeToConsoleListener); std::this_thread::sleep_for(std::chrono::milliseconds(4500)); } // Zombies should be killed here { std::ostringstream buf; buf << "============================================================\n" << "| Zomby was killed |\n" << "============================================================\n"; if (writeToConsoleListener) { writeToConsoleListener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } std::this_thread::sleep_for(std::chrono::milliseconds(5000)); return 0; }
إخراج وحدة التحكمBoozdedZomby has got a fresh data: 1144108930 101027544 1458777923 1115438165 74243042
BoozdedZomby has got a fresh data: 143542612 1131570933
BoozdedZomby has got a fresh data: 893351816 563613512 704877633
BoozdedZomby has got a fresh data: 1551901393 1399125485 1899894091 937186357 590357944 357571490
============================================================
| Zomby was killed |
============================================================
BoozdedZomby has got a fresh data: 1927702196 130060903 1083454666 2118797801 2035308228 824938981
BoozdedZomby has got a fresh data: 2020739063 1635339425 34075629
BoozdedZomby has got a fresh data: 2146319451 500782188 1269406752 884936716 892053144
BoozdedZomby has got a fresh data: 330111137 1723153177 1070477904
BoozdedZomby has got a fresh data: 343098142 280090412 589673557 889688008 2014119113 388471006
تسبب استدعاء run_once () في إشارة دائرية. واصل الزومبي العمل حتى بعد مغادرة مجال الرؤية. لم يتم استدعاء Destructors لكثير من الكائنات التي تم إنشاؤها أثناء البرنامج:- boozdedZomby؛- WriteToConsoleListener ؛- حقول البيانات غيبوبة.حدث تسرب للذاكرة.كان هناك تسرب للموارد.كيف يختلف هذا المثال عن السابق؟إنه أقرب بكثير إلى الكود الحقيقي. هذا لم يعد مثالا اصطناعيا. قد يحدث هذا الرمز بشكل طبيعي عند استخدام التعزيز :: asio. علاوة على ذلك ، لا يمكن إصلاحه بمجرد رفض التقاط رابط قوي لصالح رابط ضعيف - وهذا سوف يتعارض مع صحة المخزن المؤقت والدفق (مصدر البيانات).Valgrind؟من قبل. على الرغم من أنه يبدو أنه تم الكشف عن التسريبات.الكسالى في البرية
المشكلة بعيدة المنال! لذلك لا أحد يكتب!كما يكتب.مثال عميل HTTP مثال عميلWebsocketوثائق الدرس الرسمي يعلم كيفية كتابة BoozdedZomby + SteppingZomby المختلطة. من المستحيل منعه ، لكن لا أحد يحاول. على وجه التحديد ، في رمز العرض التوضيحي ، لا تظهر الخاصية الرئيسية للزومبي ، ولكن يجب عليك نقل هذا إلى الإنتاج - والآن أنت تمشي بالفعل على طول الحافة ، على الأرجح حتى على الجانب المظلم.يمكنك إيقاف الكسالى من خلال تدمير دفعة :: asio :: io_context مثيل!... على طول الطريق ، وتدمير كيانات أخرى n (ربما غير الكسالى) الذين يعيشون في هذا السياق.المزيد من الأمثلة: فيمايلي مثال مشابه على مورد تابع لجهة خارجية ،وهنا يسأل شخص سؤال حول stackoverflow ، كيف يمكنه جعل الكود الخاص به أكثر غيبوبةإليكم سؤال آخر عن سبب عدم عمل غيبوبة الحبيبة ،وهنا رجل خائف من الرسائل حول تسرب الذاكرة عند تشغيل غيبوبة.استنتاج
بالطبع ، لا تصف المقالة جميع أنواع مضادات "الزومبي".يمكن العثور عليها في شكل الهجينة من الأنواع المذكورة أعلاه ، وفي شكل أنواع مستقلة جديدة.يمكن أن يحدث antipattern ليس فقط عندما يتم تشغيل std :: thread في التعليمات البرمجية الخاصة بك - يمكن الاستيلاء على هذا الجزء من العمل من خلال مكتبة متعددة مؤشرات الترابط تابعة لجهة خارجية.قد يكون الرابط الدوري أطول من الأمثلة.يمكن أن تكون البنية إما مدفوعة بالحدث أو بناءً على الاقتراع الدوري القائم على الاقتراع.هذا ليس كل شيء مهم جدا.من المهم أن دائمايبدأ antipattern بمثيل الطبقة في الحصول على إشارة قوية إلى نفسه. يتم إنشاؤه دائمًا تقريبًا باستخدام std :: enable_shared_from_this ، على الرغم من أنه يمكن أيضًا توفيره خارجيًا (بما في ذلك الارتباط الضعيف - الفئة نفسها يمكن أن تجعلها قوية). ربما يكون هناك استثناء استثنائي واحد لهذه القاعدة: عندما يوفر الرمز الخارجي مرجعًا قويًا أو ضعيفًا لمثيل فئة إلى أحد حقول البيانات الخاصة به.قد لا يكون تحليل الشفرة الديناميكية قادراً على اكتشاف هذا المضاد ، وخاصة إصداره من SteppingZomby. يوجد أيضًا أمل ضئيل في التحليل الثابت - يوجد خط رفيع جدًا بين الاستخدام الصحيح وغير الصحيح لـ Shared_from_this (يمكن تصحيح جميع أمثلة التعليمات البرمجية الواردة في المقالة عن طريق إجراء تصحيحات صغيرة جدًا - فقط 1 إلى 6 أسطر من التعليمات البرمجية).يمكن أن تساعد الاختبارات التلقائية في تحديد ذلك والتحقق من صحة عملية الإزالة - ولكن لهذا تحتاج إلى معرفة ما الذي تبحث عنه. أعرف تماما.البحث antipattern ، هنا وهناك ، سوف تضطر إلى يدويا. ولهذا تحتاج إلى إعادة التفكير في جميع تطبيقات std :: enable_shared_from_this - فهي خطيرة للغاية .ملاحظة: وفقًا لنتائج التصويت - سأقوم بإعداد مقالة منفصلة مع مناقشة خيارات التخلص من المضاد.