لماذا تعيش قوالب C++ في ملفات الرأس فقط؟ نفحصها بالأمثلة

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

مقدمة: الألم المألوف لكل مطور C++

\n\n

هل سبق لك أن كتبت فئة قالب أنيقة، وفصلت الإعلان في ملف .h والتنفيذ في ملف .cpp، ثم قمت بتجميع المشروع وحصلت من الرابط على شيء مثل "undefined reference to ..."؟ إذا كان الأمر كذلك، فأنت لست وحدك. هذا السؤال على Stack Overflow حصد آلاف المشاهدات ومئات التعليقات. موضوع "why can templates only be implemented in the header file" هو حجر عثرة حقيقي للمبتدئين وحتى للمبرمجين ذوي الخبرة الذين ينتقلون إلى C++ من لغات أخرى.

\n\n

في هذه المقالة، سنفهم مرة واحدة وإلى الأبد لماذا تتطلب قوالب C++ هذا النهج، وكيف يعمل نموذج الترجمة، وماذا تفعل إذا كنت تريد حقًا إخفاء التنفيذ في ملف .cpp. هيا بنا!

\n\n

1. كيف يرى المترجم القالب؟ نموذج ترجمة C++

\n\n

لفهم المشكلة، عليك أن تتذكر كيف يقوم C++ بتجميع الكود العادي. تتكون العملية من مرحلتين:

\n\n
    \n
  • الترجمة — يتم تجميع كل ملف .cpp (وحدة ترجمة) بشكل مستقل. يرى المترجم فقط ما تم تضمينه في هذا الملف عبر #include.
  • \n
  • الربط — يتم جمع ملفات الكائن (.o أو .obj) معًا، ويتم البحث عن تعريفات الدوال والمتغيرات والفئات.
  • \n
\n\n

تخيل الآن أن لديك قالب دالة max:

\n\n
// max.h\ntemplate <typename T>\nT max(T a, T b);  // إعلان فقط!\n\n// max.cpp\n#include "max.h"\ntemplate <typename T>\nT max(T a, T b) {\n    return (a > b) ? a : b;\n}\n\n// main.cpp\n#include "max.h"\nint main() {\n    int result = max(3, 5);  // المترجم يرى الإعلان فقط!\n    return 0;\n}
\n\n

عند تجميع main.cpp، يواجه المترجم استدعاء max(3,5). إنه يعلم أن max هو قالب، لكنه لا يرى تنفيذه. لا يمكن للمترجم إنشاء مثيل للقالب بنوع int، لأن التعريف موجود في ملف آخر (max.cpp) يتم تجميعه بشكل منفصل. ونتيجة لذلك، يقوم بإنشاء استدعاء بمرجع خارجي، ولا يجد الرابط دالة مناسبة تم إنشاء مثيل لها - ومن هنا يأتي الخطأ.

\n\n

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

\n\n

2. لماذا لا يعمل هذا كما هو الحال مع الدوال العادية؟

\n\n

على عكس الدوال العادية، القالب ليس كودًا جاهزًا. إنه "تعليمات" للمترجم: "عندما ترى استخدامًا بنوع int، قم بإنشاء دالة لـ int". إذا كان التنفيذ مخفيًا في ملف .cpp، فإن المترجم في main.cpp لا يتلقى هذه التعليمات ولا يمكنه إنشاء الكود المطلوب.

\n\n

تخيل أن لديك وصفة (قالب)، لكن الوصفة نفسها موجودة في خزينة (في ملف .cpp آخر). الطاهي (المترجم) يرى فقط اسم الطبق، لكنه لا يعرف كيفية تحضيره. لن ينجح الطبق. بالنسبة للدالة العادية، الوصفة جاهزة بالفعل - الطاهي ببساطة يأخذ الطبق الجاهز من الثلاجة (من ملف الكائن).

\n\n

3. الحل الكلاسيكي: كل شيء في ملف الرأس

\n\n

أبسط وأكثر طريقة شيوعًا هي وضع التنفيذ مباشرة في ملف الرأس. هذه تقنية قياسية تُعرف باسم مكتبات "header-only". على سبيل المثال:

\n\n
// max.h\n#ifndef MAX_H\n#define MAX_H\n\ntemplate <typename T>\nT max(T a, T b) {\n    return (a > b) ? a : b;\n}\n\n#endif
\n\n

الآن أي ملف يتضمن max.h يحصل على التعريف الكامل للقالب. يمكن للمترجم إنشاء مثيل له لأي نوع مطلوب. هذه هي بالضبط طريقة عمل المكتبات الشهيرة مثل Boost ومعظم STL. العيب

المدونات

توصيات الكتب