مسائل - ملخص c++




لماذا لا يمكن تنفيذ القوالب إلا في ملف الرأس؟ (9)

إجابات صحيحة كثيرة هنا ، لكني أردت إضافة هذا (لإكماله):

إذا قمت ، في الجزء السفلي من ملف cpp الخاص بالتنفيذ ، بإجراء عملية إنشاء صريحة لجميع الأنواع التي سيتم استخدام القالب بها ، فسيتمكن الرابط من العثور عليها كالمعتاد.

تحرير: إضافة مثال على إنشاء مثيل قالب صريح. تم استخدامه بعد تعريف القالب ، وتم تعريف جميع وظائف الأعضاء.

template class vector<int>;

سيؤدي هذا إلى إنشاء (وبالتالي إتاحته للرابط) للفئة وجميع وظائف العضو (فقط). يعمل بناء الجملة المماثل لوظائف القالب ، لذا إذا كان لديك عبء زائد من المشغل غير العضو قد تحتاج إلى القيام بنفس الشيء لتلك الوظائف.

المثال أعلاه عديم الفائدة إلى حدٍ ما حيث يتم تعريف المتجه بالكامل في الرؤوس ، إلا عندما يستخدم ملف تضمين شائع (رأس precompiled؟) extern template class vector<int> لمنعه من instantiating عليه في كافة الملفات الأخرى (1000؟) التي تستخدم المتجهات.

اقتباس من مكتبة C ++ القياسية: برنامج تعليمي وكتيب :

الطريقة الوحيدة المحمولة لاستخدام القوالب في هذه اللحظة هو تنفيذها في ملفات رأس باستخدام وظائف مضمنة.

لماذا هذا؟

(توضيح: الملفات الرأسية ليست هي الحل المحمول الوحيد ، ولكنها الحل المحمول الأكثر ملاءمة.)


إذا كان القلق هو وقت التجميع الإضافي والحجم الثنائي الناتج عن طريق تجميع .h كجزء من جميع وحدات .cpp التي تستخدمه ، في كثير من الحالات ما يمكنك القيام به هو جعل فئة القالب تنحدر من فئة أساسية غير مقسمة الأجزاء التي لا تعتمد على نوع من الواجهة ، ويمكن أن يكون لهذه الفئة الأساسية تنفيذها في ملف .cpp.


تحتاج القوالب إلى أن يتم إنشاء القوالب قبل تجميعها فعليًا في رمز الكائن. يمكن إنشاء هذا التأليف فقط إذا كانت وسائط القوالب معروفة. الآن تخيل سيناريو حيث يتم الإعلان عن وظيفة القالب في ah ، المعرفة في a.cpp والمستخدمة في b.cpp . عندما يتم a.cpp ، لا يكون من الضروري بالضرورة أن يتطلب b.cpp البرمجي b.cpp القادم b.cpp من القالب ، ناهيك عن مثيل محدد سيكون ذلك. لمزيد من ملفات الرأس والمصدر ، يمكن أن يصبح الوضع أكثر تعقيدًا بسرعة.

يمكن للمرء أن يجادل بأن المجمعين يمكن أن يصبحوا أذكى "للتطلع إلى الأمام" لجميع استخدامات القالب ، لكنني متأكد من أنه لن يكون من الصعب إنشاء سيناريوهات متكررة أو معقدة أخرى. AFAIK ، المجمعين لا تفعل مثل هذه النظرة المستقبلية. وكما أشار أنطون ، فإن بعض المجمعين يدعمون تصريحات التصدير الصريحة عن عمليات تشكيل النماذج ، ولكن ليس جميع المجمعين يدعمونها (حتى الآن؟).


سيقوم المحول البرمجي بإنشاء رمز لكل إنشاء مثيل قالب عند استخدام قالب أثناء الخطوة التحويل البرمجي. يتم تحويل ملفات .cpp في عملية التحويل البرمجي والربط إلى كائن نقي أو رمز الجهاز الذي يحتوي على مراجع أو رموز غير محددة لأن الملفات .h التي تم تضمينها في main.cpp الخاص بك لم يتم تطبيق YET. هذه جاهزة للارتباط بملف كائن آخر يحدد تطبيق القالب الخاص بك وبالتالي يكون لديك ملف a.out كامل قابل للتنفيذ. ومع ذلك ، بما أن القوالب تحتاج إلى المعالجة في خطوة التحويل البرمجي لإنشاء تعليمة برمجية لكل عملية إنشاء نموذجية تقوم بها في برنامجك الرئيسي ، فإن الارتباط لن يساعد لأن تجميع main.cpp main.c ثم تجميع القالب الخاص بك .cpp إلى template.o ثم الربط لن يحقق الغرض من القوالب لأنني أقوم بربط إنشاء قالب مختلف لتطبيق القالب نفسه! ومن المفترض أن تقوم النماذج بعمل العكس ، أي أن يكون لديك تطبيق واحد ولكن تسمح بالعديد من عمليات البدء المتاحة عن طريق استخدام فئة واحدة.

بمعنى typename T get أثناء خطوة التحويل البرمجي وليس خطوة الربط ، لذلك إذا حاولت تجميع قالب دون استبدال T كنوع قيمة ملموس ، فلن ينجح ذلك لأن تعريف القوالب هو عملية وقت تجميعية ، و btw البرمجي هو كل شيء عن استخدام هذا التعريف.


على الرغم من أن C ++ القياسي لا يحتوي على مثل هذا المطلب ، فإن بعض المترجمين يتطلب أن تكون جميع قوالب الوظائف والطبقة متاحة في كل وحدة ترجمة يتم استخدامها. في الواقع ، بالنسبة لهؤلاء المبرمجين ، يجب توفير أجسام وظائف القوالب في ملف رأس. للتكرار: هذا يعني أن هؤلاء المجمعين لن يسمحوا بتعريفهم في الملفات غير الرأسية مثل ملفات .cpp

هناك كلمة رئيسية لتصدير يفترض أنها تخفف من هذه المشكلة ، ولكنها في أي مكان تقترب من أن تكون محمولة.


على الرغم من وجود الكثير من التفسيرات الجيدة أعلاه ، فأنا أفتقد طريقة عملية لفصل النماذج إلى العنوان والجسم.
اهتمامي الرئيسي هو تجنب إعادة تجميع جميع مستخدمي القوالب ، عندما أقوم بتغيير تعريفها.
إن وجود جميع عمليات تشكيل النماذج في نص القالب ليس حلاً قابلاً للتطبيق بالنسبة لي ، حيث إن مؤلف القالب قد لا يعرف كل ما إذا كان استخدامه ومستخدم القالب قد لا يحق له تعديله.
أخذت النهج التالي ، والذي يعمل أيضا بالنسبة لل compilers القديمة (مجلس التعاون الخليجي 4.3.4 ، aCC A.03.13).

لكل استخدام للقالب ، يوجد ملف typedef في ملف الرأس الخاص به (تم إنشاؤه من نموذج UML). يحتوي جسمها على إنشاء مثيل (الذي ينتهي في مكتبة مرتبطة في النهاية).
يتضمن كل مستخدم من القالب ذلك ملف رأس ويستخدم typedef.

مثال تخطيطي:

MyTemplate.h:

#ifndef MyTemplate_h
#define MyTemplate_h 1

template <class T>
class MyTemplate
{
public:
  MyTemplate(const T& rt);
  void dump();
  T t;
};

#endif

MyTemplate.cpp:

#include "MyTemplate.h"
#include <iostream>

template <class T>
MyTemplate<T>::MyTemplate(const T& rt)
: t(rt)
{
}

template <class T>
void MyTemplate<T>::dump()
{
  cerr << t << endl;
}

MyInstantiatedTemplate.h:

#ifndef MyInstantiatedTemplate_h
#define MyInstantiatedTemplate_h 1
#include "MyTemplate.h"

typedef MyTemplate< int > MyInstantiatedTemplate;

#endif

MyInstantiatedTemplate.cpp:

#include "MyTemplate.cpp"

template class MyTemplate< int >;

MAIN.CPP:

#include "MyInstantiatedTemplate.h"

int main()
{
  MyInstantiatedTemplate m(100);
  m.dump();
  return 0;
}

وبهذه الطريقة فقط سوف تحتاج إلى إعادة صياغة نماذج فورية ، وليس كل مستخدمي القوالب (والاعتماديات).


هذا صحيح تمامًا لأن المحول البرمجي يجب أن يعرف ما هو نوع التخصيص. لذلك يجب تنفيذ فئات القوالب والوظائف والتعدادات وما إلى ذلك في ملف الرأس أيضًا إذا كان سيتم نشرها أو جزء من المكتبة (ثابتة أو ديناميكية) نظرًا لعدم تجميع ملفات الرأس بخلاف ملفات c / cpp التي ل. إذا كان المحول البرمجي لا يعرف أن النوع لا يمكنه تجميعه. في .Net يمكن ذلك لأن كل الكائنات مشتقة من فئة كائن. هذا ليس صافي.


وهذا يعني أن الطريقة الأكثر استخدامًا لنقل تطبيقات الطرق لفئات القوالب هي تعريفها داخل تعريف فئة القالب.

template < typename ... >
class MyClass
{

    int myMethod()
    {
       // Not just declaration. Add method implementation here
    }
};

ليس من الضروري وضع التطبيق في ملف الرأس ، انظر الحل البديل في نهاية هذه الإجابة.

على أي حال ، السبب في فشل التعليمة البرمجية هو أنه عند إنشاء القالب ، ينشئ المحول البرمجي فئة جديدة بوسيطة قالب محدد. فمثلا:

template<typename T>
struct Foo
{
    T bar;
    void doSomething(T param) {/* do stuff using T */}
};

// somewhere in a .cpp
Foo<int> f; 

عند قراءة هذا السطر ، سيقوم المترجم بإنشاء فئة جديدة (دعنا نسميها FooInt ) ، وهو ما يعادل ما يلي:

struct FooInt
{
    int bar;
    void doSomething(int param) {/* do stuff using int */}
}

وبالتالي ، يحتاج المحول البرمجي إلى الوصول إلى تنفيذ الأساليب ، لتكوينها باستخدام وسيطة القالب (في هذه الحالة int ). إذا لم تكن هذه التطبيقات موجودة في العنوان ، فلن يمكن الوصول إليها ، وبالتالي لن يتمكن المحول البرمجي من إنشاء قالب.

الحل المشترك لهذا هو كتابة تعريف القالب في ملف رأس ، ثم تنفيذ الفئة في ملف تنفيذ (على سبيل المثال .tpp) ، وتضمين ملف التنفيذ هذا في نهاية الرأس.

// Foo.h
template <typename T>
struct Foo
{
    void doSomething(T param);
};

#include "Foo.tpp"

// Foo.tpp
template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}

وبهذه الطريقة ، لا يزال التنفيذ منفصلاً عن الإعلان ، ولكن يمكن الوصول إليه من قبل المترجم.

هناك حل آخر يتمثل في الحفاظ على الفصل منفصلاً ، وإنشاء جميع نماذج القوالب التي تحتاج إليها بشكل صريح:

// Foo.h

// no implementation
template <typename T> struct Foo { ... };

//----------------------------------------    
// Foo.cpp

// implementation of Foo's methods

// explicit instantiations
template class Foo<int>;
template class Foo<float>;
// You will only be able to use Foo with int or float

إذا لم يكن شرحي واضحًا بما فيه الكفاية ، فيمكنك إلقاء نظرة على الأسئلة الشائعة عن C ++ حول هذا الموضوع .





c++-faq