سؤال حول المؤشرات الذكية وعدم حتميةها
-
03-07-2019 - |
سؤال
لقد كنت أستخدم المؤشرات الذكية على نطاق واسع (boost::shared_ptr على وجه الدقة) في مشاريعي على مدار العامين الماضيين.أنا أفهم وأقدر فوائدها وأحبها كثيرًا بشكل عام.لكن كلما استخدمتها أكثر، كلما افتقدت السلوك الحتمي لـ C++ فيما يتعلق بإدارة الذاكرة وRAII الذي يبدو أنني أحبه في لغة البرمجة.تعمل المؤشرات الذكية على تبسيط عملية إدارة الذاكرة وتوفير جمع البيانات المهملة تلقائيًا من بين أشياء أخرى، ولكن المشكلة تكمن في أن استخدام جمع البيانات المهملة تلقائيًا بشكل عام والمؤشر الذكي على وجه التحديد يقدم درجة معينة من عدم التحديد في ترتيب (إلغاء) التهيئة.إن عدم الحتمية هذه تأخذ السيطرة بعيدًا عن المبرمجين، وكما أدركت مؤخرًا، فإنها تجعل مهمة تصميم وتطوير واجهات برمجة التطبيقات، التي لا يكون استخدامها معروفًا بشكل كامل مسبقًا في وقت التطوير، تستغرق وقتًا طويلاً بشكل مزعج لأن يجب التفكير جيدًا في جميع أنماط الاستخدام وحالات الزاوية.
لمزيد من التفاصيل، أقوم حاليًا بتطوير واجهة برمجة التطبيقات (API).تتطلب أجزاء من واجهة برمجة التطبيقات (API) هذه تهيئة كائنات معينة قبل الكائنات الأخرى أو تدميرها بعد ذلك.وبعبارة أخرى، فإن ترتيب (إلغاء) التهيئة مهم في بعض الأحيان.لإعطائك مثالا بسيطا، لنفترض أن لدينا فئة تسمى System.out.يوفر النظام بعض الوظائف الأساسية (تسجيل الدخول في مثالنا) ويحمل عددًا من الأنظمة الفرعية عبر مؤشرات ذكية.
class System {
public:
boost::shared_ptr< Subsystem > GetSubsystem( unsigned int index ) {
assert( index < mSubsystems.size() );
return mSubsystems[ index ];
}
void LogMessage( const std::string& message ) {
std::cout << message << std::endl;
}
private:
typedef std::vector< boost::shared_ptr< Subsystem > > SubsystemList;
SubsystemList mSubsystems;
};
class Subsystem {
public:
Subsystem( System* pParentSystem )
: mpParentSystem( pParentSystem ) {
}
~Subsystem() {
pParentSubsystem->LogMessage( "Destroying..." );
// Destroy this subsystem: deallocate memory, release resource, etc.
}
/*
Other stuff here
*/
private:
System * pParentSystem; // raw pointer to avoid cycles - can also use weak_ptrs
};
كما يمكنك أن تقول بالفعل، النظام الفرعي له معنى فقط في سياق النظام.لكن النظام الفرعي في مثل هذا التصميم يمكن أن يتجاوز عمر النظام الأصلي بسهولة.
int main() {
{
boost::shared_ptr< Subsystem > pSomeSubsystem;
{
boost::shared_ptr< System > pSystem( new System );
pSomeSubsystem = pSystem->GetSubsystem( /* some index */ );
} // Our System would go out of scope and be destroyed here, but the Subsystem that pSomeSubsystem points to will not be destroyed.
} // pSomeSubsystem would go out of scope here but wait a second, how are we going to log messages in Subsystem's destructor?! Its parent System is destroyed after all. BOOM!
return 0;
}
إذا كنا قد استخدمنا مؤشرات أولية للاحتفاظ بالأنظمة الفرعية، فسنقوم بتدمير الأنظمة الفرعية عندما يتعطل نظامنا، بالطبع، سيكون pSomeSubsystem مؤشرًا متدليًا.
على الرغم من أنه ليس من مهمة مصمم واجهة برمجة التطبيقات (API) حماية مبرمجي العملاء من أنفسهم، إلا أنها فكرة جيدة أن تجعل واجهة برمجة التطبيقات (API) سهلة الاستخدام بشكل صحيح ومن الصعب استخدامها بشكل غير صحيح.لذلك أنا أطلب منكم يا رفاق.ماذا تعتقد؟كيف يجب أن أخفف من هذه المشكلة؟كيف يمكنك تصميم مثل هذا النظام؟
شكرا مقدما ، جوش
المحلول
ملخص المشكلة
هناك نوعان من المخاوف المتنافسة في هذا السؤال.
- إدارة دورة حياة
Subsystem
s، مما يسمح بإزالتها في الوقت المناسب. - عملاء
Subsystem
بحاجة إلى معرفة أنSubsystem
يستخدمونه صالحًا.
التعامل مع رقم 1
System
يمتلك Subsystem
ويجب أن يديروا دورة حياتهم بنطاقها الخاص.استخدام shared_ptr
يعد هذا مفيدًا بشكل خاص لأنه يبسط عملية التدمير، ولكن لا ينبغي عليك توزيعها لأنك ستفقد الحتمية التي تبحث عنها فيما يتعلق بإلغاء تخصيصها.
التعامل مع رقم 2
هذا هو الاهتمام الأكثر إثارة للاهتمام الذي يجب معالجته.عند وصف المشكلة بمزيد من التفصيل، تحتاج إلى أن يحصل العملاء على كائن يتصرف مثل ملف Subsystem
في حين أن Subsystem
(و هو الوالد System
) موجود، ولكنه يتصرف بشكل مناسب بعد a Subsystem
دمرت.
يتم حل هذا بسهولة عن طريق مزيج من نمط الوكيل, ، ال نمط الدولة و ال نمط كائن فارغ.في حين أن هذا قد يبدو معقدًا بعض الشيء للحل، 'هناك بساطة لا يمكن الحصول عليها إلا على الجانب الآخر من التعقيد' كمطورين للمكتبة/API ، يجب أن نذهب إلى ميل إضافي لجعل أنظمتنا قوية.علاوة على ذلك، نريد أن تتصرف أنظمتنا بشكل حدسي كما يتوقع المستخدم، وأن تتحلل بأمان عندما يحاولون إساءة استخدامها.هناك العديد من الحلول لهذه المشكلة، ومع ذلك، يجب أن يوصلك هذا الحل إلى تلك النقطة المهمة حيث أنت و سكوت مايرز أقول إنه "سهل الاستخدام بشكل صحيح ويصعب استخدامه بشكل غير صحيح.'
الآن، أفترض أنه في الواقع، System
الصفقات في بعض الفئات الأساسية من Subsystem
الصورة، والتي تستمد منها مختلف مختلفة Subsystem
س.لقد قدمته أدناه باسم SubsystemBase
.تحتاج إلى تقديم أ الوكيل هدف، SubsystemProxy
أدناه، الذي ينفذ واجهة SubsystemBase
عن طريق إعادة توجيه الطلبات إلى الكائن الذي يقوم بتوكيله.(وبهذا المعنى، فهو يشبه إلى حد كبير تطبيقًا لأغراض خاصة لـ نمط الديكور.) كل Subsystem
ينشئ أحد هذه الكائنات، والذي يحمله عبر ملف shared_ptr
, ، ويعود عند الطلب عبر GetProxy()
, ، وهو ما يطلق عليه الوالد System
كائن عندما GetSubsystem()
يسمى.
عندما System
يخرج عن النطاق، كل واحد من ذلك Subsystem
يتم تدمير الكائنات.في مدمرهم، يسمون mProxy->Nullify()
, ، مما يسبب لهم الوكيل كائنات لتغييرها ولاية.يفعلون ذلك عن طريق التغيير للإشارة إلى أ كائن فارغ, ، والذي ينفذ SubsystemBase
واجهة، ولكن يفعل ذلك من خلال عدم القيام بأي شيء.
باستخدام نمط الدولة هنا سمح لتطبيق العميل بأن يكون غافلاً تمامًا عما إذا كان هناك تطبيق معين أم لا Subsystem
موجود.علاوة على ذلك، فهو لا يحتاج إلى التحقق من المؤشرات أو الاحتفاظ بالمثيلات التي كان ينبغي تدميرها.
ال نمط الوكيل يسمح للعميل بالاعتماد على كائن خفيف الوزن يغطي تمامًا تفاصيل الأعمال الداخلية لواجهة برمجة التطبيقات (API)، ويحافظ على واجهة ثابتة وموحدة.
ال نمط كائن فارغ يسمح ل الوكيل للعمل بعد الأصل Subsystem
تم إزالته.
عينة من الرموز
لقد قمت بوضع مثال تقريبي لجودة الكود الزائف هنا، لكنني لم أكن راضيًا عنه.لقد قمت بإعادة كتابته ليكون مثالًا دقيقًا ومجمعًا (استخدمت g++) لما وصفته أعلاه.لكي ينجح الأمر، كان علي أن أقدم بعض الفئات الأخرى، لكن استخداماتها يجب أن تكون واضحة من أسمائها.لقد اشتغلت نمط سينجلتون ل NullSubsystem
الطبقة، فمن المنطقي أنك لن تحتاج إلى أكثر من واحد. ProxyableSubsystemBase
يلخص تمامًا سلوك الوكيل بعيدًا عن Subsystem
, مما يسمح له بالجهل بهذا السلوك.فيما يلي مخطط UML للفئات:
رمز المثال:
#include <iostream>
#include <string>
#include <vector>
#include <boost/shared_ptr.hpp>
// Forward Declarations to allow friending
class System;
class ProxyableSubsystemBase;
// Base defining the interface for Subsystems
class SubsystemBase
{
public:
// pure virtual functions
virtual void DoSomething(void) = 0;
virtual int GetSize(void) = 0;
virtual ~SubsystemBase() {} // virtual destructor for base class
};
// Null Object Pattern: an object which implements the interface to do nothing.
class NullSubsystem : public SubsystemBase
{
public:
// implements pure virtual functions from SubsystemBase to do nothing.
void DoSomething(void) { }
int GetSize(void) { return -1; }
// Singleton Pattern: We only ever need one NullSubsystem, so we'll enforce that
static NullSubsystem *instance()
{
static NullSubsystem singletonInstance;
return &singletonInstance;
}
private:
NullSubsystem() {} // private constructor to inforce Singleton Pattern
};
// Proxy Pattern: An object that takes the place of another to provide better
// control over the uses of that object
class SubsystemProxy : public SubsystemBase
{
friend class ProxyableSubsystemBase;
public:
SubsystemProxy(SubsystemBase *ProxiedSubsystem)
: mProxied(ProxiedSubsystem)
{
}
// implements pure virtual functions from SubsystemBase to forward to mProxied
void DoSomething(void) { mProxied->DoSomething(); }
int GetSize(void) { return mProxied->GetSize(); }
protected:
// State Pattern: the initial state of the SubsystemProxy is to point to a
// valid SubsytemBase, which is passed into the constructor. Calling Nullify()
// causes a change in the internal state to point to a NullSubsystem, which allows
// the proxy to still perform correctly, despite the Subsystem going out of scope.
void Nullify()
{
mProxied=NullSubsystem::instance();
}
private:
SubsystemBase *mProxied;
};
// A Base for real Subsystems to add the Proxying behavior
class ProxyableSubsystemBase : public SubsystemBase
{
friend class System; // Allow system to call our GetProxy() method.
public:
ProxyableSubsystemBase()
: mProxy(new SubsystemProxy(this)) // create our proxy object
{
}
~ProxyableSubsystemBase()
{
mProxy->Nullify(); // inform our proxy object we are going away
}
protected:
boost::shared_ptr<SubsystemProxy> GetProxy() { return mProxy; }
private:
boost::shared_ptr<SubsystemProxy> mProxy;
};
// the managing system
class System
{
public:
typedef boost::shared_ptr< SubsystemProxy > SubsystemHandle;
typedef boost::shared_ptr< ProxyableSubsystemBase > SubsystemPtr;
SubsystemHandle GetSubsystem( unsigned int index )
{
assert( index < mSubsystems.size() );
return mSubsystems[ index ]->GetProxy();
}
void LogMessage( const std::string& message )
{
std::cout << " <System>: " << message << std::endl;
}
int AddSubsystem( ProxyableSubsystemBase *pSubsystem )
{
LogMessage("Adding Subsystem:");
mSubsystems.push_back(SubsystemPtr(pSubsystem));
return mSubsystems.size()-1;
}
System()
{
LogMessage("System is constructing.");
}
~System()
{
LogMessage("System is going out of scope.");
}
private:
// have to hold base pointers
typedef std::vector< boost::shared_ptr<ProxyableSubsystemBase> > SubsystemList;
SubsystemList mSubsystems;
};
// the actual Subsystem
class Subsystem : public ProxyableSubsystemBase
{
public:
Subsystem( System* pParentSystem, const std::string ID )
: mParentSystem( pParentSystem )
, mID(ID)
{
mParentSystem->LogMessage( "Creating... "+mID );
}
~Subsystem()
{
mParentSystem->LogMessage( "Destroying... "+mID );
}
// implements pure virtual functions from SubsystemBase
void DoSomething(void) { mParentSystem->LogMessage( mID + " is DoingSomething (tm)."); }
int GetSize(void) { return sizeof(Subsystem); }
private:
System * mParentSystem; // raw pointer to avoid cycles - can also use weak_ptrs
std::string mID;
};
//////////////////////////////////////////////////////////////////
// Actual Use Example
int main(int argc, char* argv[])
{
std::cout << "main(): Creating Handles H1 and H2 for Subsystems. " << std::endl;
System::SubsystemHandle H1;
System::SubsystemHandle H2;
std::cout << "-------------------------------------------" << std::endl;
{
std::cout << " main(): Begin scope for System." << std::endl;
System mySystem;
int FrankIndex = mySystem.AddSubsystem(new Subsystem(&mySystem, "Frank"));
int ErnestIndex = mySystem.AddSubsystem(new Subsystem(&mySystem, "Ernest"));
std::cout << " main(): Assigning Subsystems to H1 and H2." << std::endl;
H1=mySystem.GetSubsystem(FrankIndex);
H2=mySystem.GetSubsystem(ErnestIndex);
std::cout << " main(): Doing something on H1 and H2." << std::endl;
H1->DoSomething();
H2->DoSomething();
std::cout << " main(): Leaving scope for System." << std::endl;
}
std::cout << "-------------------------------------------" << std::endl;
std::cout << "main(): Doing something on H1 and H2. (outside System Scope.) " << std::endl;
H1->DoSomething();
H2->DoSomething();
std::cout << "main(): No errors from using handles to out of scope Subsystems because of Proxy to Null Object." << std::endl;
return 0;
}
الإخراج من الكود:
main(): Creating Handles H1 and H2 for Subsystems.
-------------------------------------------
main(): Begin scope for System.
<System>: System is constructing.
<System>: Creating... Frank
<System>: Adding Subsystem:
<System>: Creating... Ernest
<System>: Adding Subsystem:
main(): Assigning Subsystems to H1 and H2.
main(): Doing something on H1 and H2.
<System>: Frank is DoingSomething (tm).
<System>: Ernest is DoingSomething (tm).
main(): Leaving scope for System.
<System>: System is going out of scope.
<System>: Destroying... Frank
<System>: Destroying... Ernest
-------------------------------------------
main(): Doing something on H1 and H2. (outside System Scope.)
main(): No errors from using handles to out of scope Subsystems because of Proxy to Null Object.
أفكار أخرى:
مقال مثير للاهتمام قرأته في أحد كتب Game Programming Gems يتحدث عن استخدام Null Objects لتصحيح الأخطاء والتطوير.كانوا يتحدثون على وجه التحديد عن استخدام نماذج وأنسجة الرسومات الخالية، مثل نسيج رقعة الشطرنج لجعل النماذج المفقودة بارزة حقًا.ويمكن تطبيق الشيء نفسه هنا عن طريق تغيير
NullSubsystem
لReportingSubsystem
والتي من شأنها تسجيل المكالمة وربما مكدس الاستدعاءات عند الوصول إليها.سيسمح هذا لك أو لعملاء مكتبتك بتعقب المكان الذي يعتمدون فيه على شيء خارج النطاق، ولكن دون الحاجة إلى التسبب في حدوث عطل.ذكرت في تعليق @Arkadiy أن التبعية الدائرية التي طرحها بينهما
System
وSubsystem
غير سارة بعض الشيء.ويمكن علاجه بسهولة عن طريق وجودSystem
مشتقة من الواجهة التيSubsystem
يعتمد ذلك على تطبيق روبرت سي مارتن مبدأ انعكاس التبعية.والأفضل من ذلك هو عزل الوظيفة التيSubsystem
يحتاجها والديهم، اكتب واجهة لذلك، ثم احتفظ بمنفذ تلك الواجهة فيSystem
وتمريرها إلىSubsystem
s، والتي من شأنها أن تحملها عبر أshared_ptr
.على سبيل المثال، قد يكون لديكLoggerInterface
, ، الذي الخاص بكSubsystem
يستخدم للكتابة في السجل، ثم يمكنك استخلاصهCoutLogger
أوFileLogger
منه، والاحتفاظ بمثال من هذا القبيل فيSystem
.
نصائح أخرى
وهذا هو القيام به، وقادر مع الاستخدام السليم للطبقة weak_ptr
. في الواقع، كنت بالفعل قريبة جدا من وجود حل جيد. أنت على حق أنه لا يمكن توقع أن "من التفكير" المبرمجين العميل الخاص بك، ولا يجب أن تتوقع أنها سوف تتبع دائما "قواعد" API الخاص بك (وأنا متأكد من أنك على علم بالفعل). لذا، فإن أفضل ما يمكن القيام به حقا هو السيطرة على الضرر.
وأنا أوصى بعد مكالمتك إلى GetSubsystem
إرجاع weak_ptr
بدلا من shared_ptr
ببساطة بحيث المطور العميل يمكن اختبار صحة المؤشر دون يدعي دائما إشارة إلى ذلك.
وبالمثل، فإن pParentSystem
يكون boost::weak_ptr<System>
بحيث يمكن الكشف عن داخليا سواء System
الأم لا يزال موجودا عبر الدعوة إلى lock
على pParentSystem
جنبا إلى جنب مع الاختيار NULL
(لن مؤشر الخام لن اقول لكم هذا).
وعلى افتراض انك تغيير الطبقة Subsystem
للتحقق مما دائما أم لا وجوه System
المناظر له وجود، يمكنك التأكد من أن إذا حاول مبرمج العميل لاستخدام الكائن Subsystem
خارج نطاق المقصود أن خطأ سيؤدي (التي يمكنك التحكم) بدلا من استثناء لا يمكن تفسيره (التي يجب أن تثق مبرمج العميل للقبض / المعالجة الملائمة).
وهكذا، في المثال الخاص بك مع main()
، فإن الأمور لن تذهب BOOM! الطريقة الأكثر رشيقة للتعامل مع هذا في dtor وSubsystem
وسيكون لأنها قد تبدو شيئا من هذا القبيل:
class Subsystem
{
...
~Subsystem() {
boost::shared_ptr<System> my_system(pParentSystem.lock());
if (NULL != my_system.get()) { // only works if pParentSystem refers to a valid System object
// now you are guaranteed this will work, since a reference is held to the System object
my_system->LogMessage( "Destroying..." );
}
// Destroy this subsystem: deallocate memory, release resource, etc.
// when my_system goes out of scope, this may cause the associated System object to be destroyed as well (if it holds the last reference)
}
...
};
وآمل أن يساعد هذا!
وهنا نظام تملك بوضوح النظم الفرعية وأنا لا أرى أي جدوى من الملكية المشتركة. وأود أن يعود ببساطة مؤشر الخام. إذا كان النظام الفرعي outlives النظام لها، وهذا خطأ من تلقاء نفسها.
وأنت على حق في البداية في الفقرة الأولى. التصاميم الخاصة بك على أساس RAII (مثل الألغام وقانون مكتوب الأكثر جيدا C ++) تتطلب أن الأشياء الخاصة بك والتي تحتفظ بها مؤشرات الملكية الحصرية. في دفعة من شأنها أن scoped_ptr.
وهكذا لماذا لم تستخدم scoped_ptr. فمن المؤكد أنها ستكون لأنك أردت فوائد weak_ptr للحماية من التفضيلات ولكن هل يمكن أن نشير فقط weak_ptr في shared_ptr. لذلك كنت قد اعتمدت الممارسة الشائعة المتمثلة في إعلان عاجلا shared_ptr عند ما أردت حقا ملكية واحدة. هذا هو إعلان كاذب وكما تقول، أنه يعرض للخطر تالفة يطلق عليها في التسلسل الصحيح. بالطبع لو لم تشارك أي وقت مضى ملكية سوف تفلت من العقاب - ولكن سيكون لديك للتحقق باستمرار كل من التعليمات البرمجية الخاصة بك للتأكد من مشاركته أبدا
لجعل الأمور أسوأ دفعة :: weak_ptr غير مريح للاستخدام (أنه لا يوجد لديه -> مشغل) حتى المبرمجين تجنب هذا الإزعاج من خلال الإعلان زورا المراجع مراقبة سلبية كما shared_ptr. هذه ملكية أسهم بطبيعة الحال، وإذا كنت قد نسيت لاغيه أن shared_ptr ثم جوه الخاص بك لن تحصل على تدمير أو دعا المدمر لها عندما كنت تنوي أن.
وباختصار، لقد تم مخدوع من قبل مكتبة دفعة - فشلت في تبني C الممارسات البرمجة الجيدة ++ والمبرمجين قوات لجعل الإعلانات الكاذبة من أجل محاولة استخلاص بعض الاستفادة منه. ومن المفيد فقط لرمز الغراء البرمجة التي يريد حقا الملكية المشتركة وغير مهتمة رقابة مشددة على الذاكرة أو تالفة يطلق عليها في التسلسل الصحيح.
ولقد كنت أسفل نفس الطريق كما كنت. هناك حاجة إلى الحماية ضد المؤشرات متدلية بشكل سيء في C ++ ولكن المكتبة دفعة لا يوفر حلا مقبولا. كان علي أن حل هذه المشكلة - قسم البرمجيات بلدي يريد ضمانات التي يمكن تقديمها C ++ آمن. حتى أنا تدحرجت بلدي - كان الكثير من العمل ويمكن الاطلاع على:
http://www.codeproject.com/KB/cpp/XONOR.aspx
ومن المناسب تماما للعمل مترابطة واحد وأنا على وشك تحديثه لتبني مؤشرات يجري تقاسمها عبر المواضيع. وتتمثل الميزة الرئيسية هي أنه يدعم الذكية (التصفير النفس) المراقبين السلبي من الأشياء التي تملكها على وجه الحصر.
ومما يؤسف له المبرمجين أصبحت تقربها جمع القمامة و"حجم واحد يناسب الجميع" الحلول مؤشر الذكية وإلى حد كبير لا حتى التفكير في ملكية والمراقبين السلبي - نتيجة لذلك لا أعرف حتى أن ما يقومون به هو خطأ، لا يشكو. بدعة ضد دفعة يكاد يسمع!
والحلول التي اقترحت الى أنت سخيف معقدة ولا يساعد على الإطلاق. وهي أمثلة على سخافة الذي ينجم عن إحجام الثقافي الاعتراف بأن مؤشرات الكائن بأدوار متميزة يجب أن يعلن بشكل صحيح والإيمان الأعمى أن تعزيز يجب أن يكون الحل.
وأنا لا أرى مشكلة مع وجود نظام :: GetSubsystem إرجاع مرجع الخام (بدلا من مؤشر الذكية) إلى النظام الفرعي. منذ العميل ليست مسؤولة عن بناء الأجسام، ثم لا يوجد أي عقد ضمني للعميل ليكون مسؤولا عن تنظيف. ونظرا لأنه هو المرجعية الداخلية، فإنه ينبغي أن يكون من المعقول أن نفترض أن عمر الكائن النظام الفرعي يعتمد على عمر من وجوه النظام. يجب عليك ثم تعزز هذا العقد الضمني مع وثائق تفيد قدر.
وهذه النقطة هي أن كنت لا إعادة تعيين أو تقاسم ملكية - فلماذا استخدام مؤشر الذكية
المشكلة الحقيقية هنا هي التصميم الخاص بك.لا يوجد حل جميل، لأن النموذج لا يعكس مبادئ التصميم الجيدة.إليك قاعدة مفيدة أستخدمها:
- إذا كان الكائن يحمل مجموعة من الكائنات الأخرى، ويمكنه إرجاع أي كائن عشوائي من تلك المجموعة، إذن قم بإزالة هذا الكائن من التصميم الخاص بك.
أدرك أن مثالك مفتعل، لكنه نمط مخالف أرى الكثير في العمل.اسأل نفسك، ما هي القيمة System
إضافة لذالك std::vector< shared_ptr<SubSystem> >
لا؟يحتاج مستخدمو واجهة برمجة التطبيقات (API) الخاصة بك إلى معرفة الواجهة SubSystem
(بما أنك تعيدها)، لذا فإن كتابة حامل لها لا يؤدي إلا إلى زيادة التعقيد.على الأقل يعرف الناس الواجهة std::vector
, ، وإجبارهم على التذكر GetSubsystem()
فوق at()
أو operator[]
انه ببساطة يقصد.
سؤالك يتعلق بإدارة مدة حياة الكائنات، ولكن بمجرد أن تبدأ في توزيع الكائنات، فإنك إما تفقد السيطرة على مدة بقائها من خلال السماح للآخرين بإبقائها على قيد الحياة (shared_ptr
) أو المخاطرة بحدوث أعطال إذا تم استخدامها بعد اختفائها (المؤشرات الأولية).في التطبيقات متعددة الخيوط يكون الأمر أسوأ - من يقوم بتأمين الكائنات التي تقوم بتوزيعها على سلاسل رسائل مختلفة؟تعد التعزيزات المشتركة والمؤشرات الضعيفة فخًا يسبب التعقيد عند استخدامها بهذه الطريقة، خاصة أنها مجرد خيط آمن بما يكفي لتعثر المطورين عديمي الخبرة.
إذا كنت ستقوم بإنشاء حامل، فيجب عليك إخفاء التعقيد عن المستخدمين وإعفائهم من الأعباء التي يمكنك إدارتها بنفسك.على سبيل المثال، واجهة تتكون من أ) إرسال أمر إلى النظام الفرعي (على سبيل المثال:URI - /system/subsystem/command?param=value) وb) تكرار الأنظمة الفرعية وأوامر النظام الفرعي (عبر مكرر يشبه stl) وربما ج) سيسمح لك تسجيل النظام الفرعي بإخفاء جميع تفاصيل التنفيذ تقريبًا من المستخدمين لديك، وفرض متطلبات العمر/الطلب/القفل داخليًا.
تُفضل واجهة برمجة التطبيقات القابلة للتكرار/التعداد إلى حد كبير على كشف الكائنات في أي حالة - يمكن إجراء تسلسل للأوامر/التسجيلات بسهولة لإنشاء حالات اختبار أو ملفات تكوين، ويمكن عرضها بشكل تفاعلي (على سبيل المثال، في عنصر تحكم شجرة، مع مربعات حوار مؤلفة عن طريق الاستعلام الإجراءات/المعلمات المتاحة).ستحمي أيضًا مستخدمي واجهة برمجة التطبيقات لديك من التغييرات الداخلية التي قد تحتاج إلى إجرائها على فئات النظام الفرعي.
أود أن أحذرك من اتباع النصيحة الواردة في إجابة آرون.إن تصميم حل لمشكلة بهذه البساطة والتي تتطلب 5 أنماط تصميم مختلفة لتنفيذها لا يعني إلا أنه تم حل المشكلة الخاطئة.لقد سئمت أيضًا من أي شخص يقتبس من السيد مايرز فيما يتعلق بالتصميم، لأنه باعترافه الخاص:
"لم أكتب برامج إنتاج منذ أكثر من 20 عامًا، ولم أكتب مطلقًا برامج إنتاج بلغة C++.لا، ليس أبداً.علاوة على ذلك، لم أحاول مطلقًا كتابة برنامج إنتاج بلغة C++، لذا فأنا لست مطورًا حقيقيًا لـ C++ فحسب، بل لست حتى متمنيًا.موازنة هذا قليلاً هي حقيقة أنني كتبت برنامجًا بحثيًا بلغة C ++ خلال سنوات دراستي العليا (1985-1993) ، ولكن حتى ذلك كان صغيرًا (بضعة آلاف من الأسطر) لمطور واحد سيتم التخلص منه بسرعة.ومنذ أن بدأت العمل كمستشار منذ أكثر من اثني عشر عامًا، اقتصرت برمجتي في لغة C++ على برامج "دعونا نرى كيف يعمل هذا" (أو، في بعض الأحيان، "دعونا نرى عدد المترجمين الذين يكسرون هذا")، وعادةً ما تكون البرامج التي تناسب ملف واحد".
لا يعني ذلك أن كتبه لا تستحق القراءة، لكنه لا يملك السلطة للحديث عن التصميم أو التعقيد.
في المثال الخاص بك، فإنه سيكون من الأفضل لو عقد النظام وvector<Subsystem>
بدلا من vector<shared_ptr<Subsystem> >
. على حد سواء أكثر بساطة، ويزيل القلق لديك. GetSubsystem سيعود إشارة بدلا من ذلك.
وسيتم الافراج عنهم أشياء المكدس بالترتيب العكسي التي تلقيهم مثيل، وذلك ما لم المطور باستخدام API تحاول إدارة مؤشر الذكية، انها عادة لن يكون مشكلة. هناك فقط بعض الأشياء التي لن تكون قادرة على منع وأفضل ما يمكن القيام به هو تقديم التحذيرات في وقت التشغيل، ويفضل أن يكون التصحيح فقط.
والمثال لديك يبدو الى حد كبير مثل COM لي، لديك حساب مرجع على النظم الفرعية إعادتهم باستخدام shared_ptr، ولكن كنت في عداد المفقودين على وجوه النظام نفسه.
وإذا كان كل من الكائنات الفرعي فعل على addref على وجوه النظام على الخلق، والإفراج عن تدمير هل يمكن عرض ما لا يقل عن استثناء إذا كان عدد مرجع غير صحيح عند إتلاف كائن النظام في وقت مبكر.
واستخدام weak_ptr سوف تسمح لك أيضا لتقديم رسالة بدلا من ذلك / كما aswell تفجير عندما يتم اطلاق سراح الأشياء في الترتيب غير صحيح.
وجوهر المشكلة هو مرجع معاد: يشير النظام إلى النظام الفرعي، والنظام الفرعي، بدوره، يشير إلى النظام. هذا النوع من بنية البيانات لا يمكن التعامل معها بسهولة عن طريق حساب مرجع - فهي تتطلب جمع القمامة السليم. كنت في محاولة لكسر حلقة باستخدام مؤشر الخام لأحد حواف - وهذا سوف ينتج إلا المزيد من التعقيدات
وقد اقترحتواثنين على الأقل من حلول جيدة، ولذا فإنني لن تحاول التفوق على الملصقات السابقة. أنا يمكن أن نلاحظ فقط أن في @ حل هارون يمكن أن يكون بديلا عن النظام بدلا من الأنظمة الفرعية - dependingo ن ما هو أكثر تعقيدا وما يجعل الشعور