تخزين تعريفات وظائف قالب C++ في ملف ‎.CPP

StackOverflow https://stackoverflow.com/questions/115703

  •  02-07-2019
  •  | 
  •  

سؤال

لدي بعض التعليمات البرمجية للقالب التي أفضل تخزينها في ملف CPP بدلاً من تضمينها في الرأس.أعلم أنه يمكن القيام بذلك طالما أنك تعرف أنواع القوالب التي سيتم استخدامها.على سبيل المثال:

ملف .h

class foo
{
public:
    template <typename T>
    void do(const T& t);
};

ملف .CPP

template <typename T>
void foo::do(const T& t)
{
    // Do something with t
}

template void foo::do<int>(const int&);
template void foo::do<std::string>(const std::string&);

لاحظ السطرين الأخيرين - يتم استخدام وظيفة القالب foo::do فقط مع ints وstd::strings، لذا فإن هذه التعريفات تعني أن التطبيق سوف يرتبط.

سؤالي هو - هل هذا اختراق سيئ أم أنه سيعمل مع المترجمين/الروابط الأخرى؟أنا أستخدم هذا الرمز فقط مع VS2008 في الوقت الحالي ولكني أرغب في النقل إلى بيئات أخرى.

هل كانت مفيدة؟

المحلول

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

أوصي بقراءة النقاط التالية من الأسئلة الشائعة حول C++ Lite:

لقد تناولوا الكثير من التفاصيل حول مشكلات القوالب هذه (وغيرها).

نصائح أخرى

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

في ملف .h الخاص بك...

template<typename T>
class foo
{
public:
    void bar(const T &t);
};

وفي ملف .cpp الخاص بك

template <class T>
void foo<T>::bar(const T &t)
{ }

// Explicit template instantiation
template class foo<int>;

تم صياغة هذا الرمز بشكل جيد.ما عليك سوى الانتباه إلى أن تعريف القالب يكون مرئيًا عند إنشاء مثيل.لاقتباس المعيار، § 14.7.2.4:

تعريف قالب وظيفة غير مُصدر، أو قالب وظيفة عضو غير مُصدر، أو وظيفة عضو غير مُصدرة أو عضو بيانات ثابت لقالب فئة يجب أن يكون موجودًا في كل وحدة ترجمة يتم إنشاء مثيل لها بشكل صريح.

يجب أن يعمل هذا بشكل جيد في كل مكان يتم فيه دعم القوالب.يعد إنشاء مثيل القالب الصريح جزءًا من معيار C++.

مثالك صحيح ولكنه ليس محمولاً للغاية.هناك أيضًا صيغة أكثر وضوحًا يمكن استخدامها (كما أشار @namespace-sid).

لنفترض أن الفصل النموذجي جزء من بعض المكتبات التي سيتم مشاركتها.هل يجب تجميع إصدارات أخرى من الفصل القالب؟هل من المفترض أن يتوقع مشرف المكتبة جميع الاستخدامات النموذجية المحتملة للفئة؟

النهج البديل هو اختلاف طفيف عما لديك:قم بإضافة ملف ثالث وهو ملف تنفيذ/إنشاء القالب.

ملف foo.h

// Standard header file guards omitted

template <typename T>
class foo
{
public:
    void bar(const T& t);
};

ملف foo.cpp

// Always include your headers
#include "foo.h"

template <typename T>
void foo::bar(const T& t)
{
    // Do something with t
}

ملف foo-impl.cpp

// Yes, we include the .cpp file
#include "foo.cpp"
template class foo<int>;

التحذير الوحيد هو أنك تحتاج إلى إخبار المترجم بالتجميع foo-impl.cpp بدلاً من foo.cpp لأن تجميع الأخير لا يفعل شيئًا.

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

يتيح ذلك قدرًا أكبر من المرونة عند مشاركة الفئة النموذجية لاستخدامات أخرى.

يعمل هذا الإعداد أيضًا على تقليل أوقات الترجمة للفئات المعاد استخدامها لأنك لا تقوم بإعادة ترجمة نفس ملف الرأس في كل وحدة ترجمة.

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

توجد، في أحدث المعايير، كلمة رئيسية (export) من شأنه أن يساعد في تخفيف هذه المشكلة، ولكن لم يتم تنفيذه في أي مترجم أعرفه، بخلاف Comeau.

انظر الأسئلة الشائعة لايت حول هذا.

نعم، هذه هي الطريقة القياسية للقيام بذلك التخصص إنشاء مثيل واضح.كما ذكرت، لا يمكنك إنشاء مثيل لهذا القالب مع أنواع أخرى.

يحرر:تم التصحيح بناءً على التعليق.

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

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

  2. حدد القالب في نفس الرأس، ولكن خارج الفصل الدراسي.هذه هي طريقتي المفضلة في معظم الأوقات.فهو يحافظ على تعريف فصلك مرتبًا، ويظل القالب قالبًا حقيقيًا.ومع ذلك، فهو يتطلب تسمية كاملة للقالب والتي قد تكون صعبة.كما أن الكود الخاص بك متاح للجميع.ولكن إذا كنت تريد أن يكون الكود الخاص بك مضمّنًا، فهذه هي الطريقة الوحيدة.يمكنك أيضًا تحقيق ذلك عن طريق إنشاء ملف .INL في نهاية تعريفات الفصل الدراسي الخاص بك.

  3. قم بتضمين header.h وImplementation.CPP في main.CPP الخاص بك.أعتقد أن هذه هي الطريقة التي يتم بها الأمر.لن تضطر إلى إعداد أي عمليات إنشاء مسبقة، وسوف تعمل كقالب حقيقي.المشكلة التي أواجهها هي أنها ليست طبيعية.نحن لا نقوم عادةً بتضمين الملفات المصدر ونتوقع تضمينها.أعتقد أنه بما أنك قمت بتضمين الملف المصدر، فيمكن تضمين وظائف القالب.

  4. هذه الطريقة الأخيرة، وهي الطريقة المنشورة، هي تحديد القوالب في ملف مصدر، تمامًا مثل الطريقة رقم 3؛ولكن بدلاً من تضمين الملف المصدر، نقوم مسبقًا بإنشاء نماذج للنماذج التي سنحتاج إليها.ليس لدي مشكلة مع هذه الطريقة وهي مفيدة في بعض الأحيان.لدينا رمز واحد كبير، ولا يمكن الاستفادة من تضمينه، لذا ضعه فقط في ملف CPP.وإذا عرفنا المثيلات الشائعة ويمكننا تحديدها مسبقًا.وهذا ينقذنا من كتابة نفس الشيء 5 أو 10 مرات.تتمتع هذه الطريقة بميزة الحفاظ على ملكية الكود الخاص بنا.لكنني لا أوصي بوضع وظائف صغيرة تُستخدم بانتظام في ملفات CPP.لأن هذا سوف يقلل من أداء مكتبتك.

لاحظ أنني لست على علم بعواقب ملف obj المتضخم.

لا حرج في المثال الذي قدمته.ولكن يجب أن أقول إنني أعتقد أنه ليس من الفعال تخزين تعريفات الوظائف في ملف CPP.أنا أفهم فقط الحاجة إلى فصل إعلان الوظيفة وتعريفها.

عند استخدامها مع إنشاء مثيل صريح للفئة، يمكن أن تساعدك مكتبة Boost Concept Check Library (BCCL) في إنشاء رمز وظيفة القالب في ملفات cpp.

حان الوقت للتحديث!قم بإنشاء ملف مضمن (.inl، أو ربما أي ملف آخر) وقم ببساطة بنسخ جميع تعريفاتك فيه.تأكد من إضافة القالب فوق كل وظيفة (template <typename T, ...>).الآن بدلاً من تضمين ملف الرأس في الملف المضمن، يمكنك القيام بالعكس.قم بتضمين الملف المضمن بعد إعلان صفك (#include "file.inl").

لا أعرف حقًا لماذا لم يذكر أحد هذا.لا أرى أي عيوب فورية.

لنأخذ مثالاً واحدًا، لنفترض لسبب ما أنك تريد الحصول على فئة قالب:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

إذا قمت بتجميع هذا الرمز باستخدام Visual Studio - فهو يعمل خارج الصندوق.سوف ينتج عن gcc خطأ في الرابط (إذا تم استخدام نفس ملف الرأس من ملفات .cpp متعددة):

error : multiple definition of `DemoT<int>::test()'; your.o: .../test_template.h:16: first defined here

من الممكن نقل التنفيذ إلى ملف .cpp، ولكن بعد ذلك تحتاج إلى إعلان فئة مثل هذا -

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

template <>
void DemoT<int>::test();

template <>
void DemoT<bool>::test();

// Instantiate parametrized template classes, implementation resides on .cpp side.
template class DemoT<bool>;
template class DemoT<int>;

ومن ثم سيبدو .cpp بالشكل التالي:

//test_template.cpp:
#include "test_template.h"

template <>
void DemoT<int>::test()
{
    printf("int test (int)\n");
}


template <>
void DemoT<bool>::test()
{
    printf("int test (bool)\n");
}

بدون وجود سطرين أخيرين في ملف الرأس - سيعمل gcc بشكل جيد، لكن Visual studio سينتج خطأ:

 error LNK2019: unresolved external symbol "public: void __cdecl DemoT<int>::test(void)" (?test@?$DemoT@H@@QEAAXXZ) referenced in function

يعد بناء جملة فئة القالب اختياريًا في حالة رغبتك في الكشف عن الوظيفة عبر تصدير .dll، ولكن هذا ينطبق فقط على نظام التشغيل Windows - لذلك يمكن أن يبدو test_template.h كما يلي:

//test_template.h:
#pragma once
#include <cstdio>

template <class T>
class DemoT
{
public:
    void test()
    {
        printf("ok\n");
    }
};

#ifdef _WIN32
    #define DLL_EXPORT __declspec(dllexport) 
#else
    #define DLL_EXPORT
#endif

template <>
void DLL_EXPORT DemoT<int>::test();

template <>
void DLL_EXPORT DemoT<bool>::test();

مع ملف .cpp من المثال السابق.

ومع ذلك، فإن هذا يسبب المزيد من المتاعب للرابط، لذا يوصى باستخدام المثال السابق إذا لم تقم بتصدير وظيفة .dll.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top