صرّح عن المتغير العام باستخدام extern في ملف ترويسة، وعرّفه في ملف .cpp واحد فقط، واربط ملف الكائنات الخاص به مع كل ملف يستدعيه. يوضح هذا الدليل عملية البناء الكاملة على macOS، والتحقق من العنوان، ومتى يكون استخدام متغير inline في C++17 مناسبًا.

لماذا يؤدي وجود متغير عام في ملف ترويسة إلى تعريفات مكررة؟

في برنامج صغير أصلي لنظام Mac، تتمثل المهمة في إتاحة قراءة الكائن نفسه وتعديله لعدة ملفات مصدر تُترجم كلٌّ منها على حدة. ابدأ بالفصل بين التصريح والتعريف: extern int itemCount; يصرّح عن كائن، بينما int itemCount = 0; يعرّفه. كما أن إضافة قيمة ابتدائية إلى extern، مثل extern int itemCount = 0;، تجعله تعريفًا أيضًا. توثّق مسودة معيار C++ هذا الفرق في التصريحات والتعريفات.

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

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

أنشئ مثالًا كاملًا لمتغير عام مشترك على جهاز Mac

استخدم Xcode أو حزمة أدوات سطر الأوامر من Apple الخاصة بـ Xcode. توثّق Apple الحزمة المستقلة بوصفها بديلًا للتطوير خارج Xcode؛ أما تثبيت Xcode الكامل فيتضمن الأدوات بالفعل. إذا لم يكن أي منهما مثبتًا، فنفّذ الأمر التالي وأكمل خطوات التثبيت الظاهرة. راجع تثبيت أدوات سطر الأوامر.

xcode-select --install

تحقق من دليل المطور المحدد والمترجم قبل البناء:

xcode-select --print-path
xcrun clang++ --version

يوثّق الأسئلة الشائعة حول البناء من سطر الأوامر باستخدام Xcode المؤرشف من Apple كيفية فحص دليل المطور المحدد واستخدام xcrun للعثور على أدوات المطور أو تشغيلها. استخدم سلسلة الأدوات المحددة نفسها طوال هذا المثال.

أنشئ مجلدًا جديدًا فارغًا باسم shared-global-demo، وافتحه في محررك، واحفظ فيه الملفات الأربعة التالية. هذا برنامج C++ اصطناعي أحادي المسار: يملك أحد الملفات المتغير العام، ويعدّله ملف آخر، ويتحقق ملف نقطة الدخول من قيمته وهويته.

صرّح عن الكائن في ملف الترويسة المشترك

احفظ هذا باسم shared_state.hpp:

#ifndef SHARED_STATE_HPP
#define SHARED_STATE_HPP

namespace demo {
extern int itemCount;

void addItem();
int* itemCountAddress();
}

#endif

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

أنشئ تعريفًا واحدًا للكائن

احفظ هذا باسم shared_state.cpp:

#include "shared_state.hpp"

namespace demo {
int itemCount = 0;
}

هذا هو التعريف الوحيد لـ demo::itemCount. يتيح تضمين ملف الترويسة في ملف المصدر الذي يعرّف الكائن للمترجم التحقق من توافق التعريف مع التصريح. وتوضح القيمة الابتدائية الصريحة القيمة الأولية.

عدّله من وحدة ترجمة أخرى

احفظ هذا باسم worker.cpp:

#include "shared_state.hpp"

namespace demo {
void addItem() {
    ++itemCount;
}

int* itemCountAddress() {
    return &itemCount;
}
}

احفظ هذا باسم main.cpp:

#include "shared_state.hpp"
#include <iostream>

int main() {
    std::cout << "before: " << demo::itemCount << '\n';
    demo::addItem();

    const bool sameObject =
        demo::itemCountAddress() == &demo::itemCount;

    std::cout << "after: " << demo::itemCount << '\n';
    std::cout << std::boolalpha
              << "same object: " << sameObject << '\n';

    return (demo::itemCount == 1 && sameObject) ? 0 : 1;
}

توفر مقارنة العنوان فحصًا مفيدًا يتجاوز نجاح الربط: تعيد worker.cpp العنوان الذي تراه، وتقارنه main.cpp بعنوانها الخاص. كما تتحقق حالة الخروج من القيمة المعدّلة. ولا يعتمد أي من الفحصين على تفعيل التأكيدات.

ترجم كل ملف على حدة، ثم اربط ملفات الكائنات الثلاثة

في Terminal، انتقل إلى المجلد الذي يحتوي على الملفات الأربعة. نفّذ كتلة البناء الكاملة التالية:

(
    set -e
    xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
        -c shared_state.cpp -o shared_state.o
    xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
        -c worker.cpp -o worker.o
    xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
        -c main.cpp -o main.o
    xcrun clang++ shared_state.o worker.o main.o -o shared-demo
    ./shared-demo
)

يوثّق Clang أن الخيار -c ينتج ملف كائنات من دون ربط؛ أما حذف خيار تحديد المرحلة فيتيح للسائق إجراء الربط. ويحدد الخيار الصريح -std=c++17 نمط اللغة. راجع دليل أوامر المترجم من Clang. تتوقف الصدفة الفرعية عند فشل أي أمر، ما يمنع بناءً فاشلًا من المضي إلى تشغيل ملف تنفيذي قديم.

الناتج المتوقع، المستنتج من المصدر لا الملتقط من تنفيذ على جهاز Mac، هو:

before: 0
after: 1
same object: true

لم توفر بيئة البحث مترجمًا لنظام Mac، لذلك جرى فحص هذا المثال بدلًا من تنفيذه. وتُظهر أوامر الترجمة الثلاثة عمدًا الحد الفاصل بين ملفات الكائنات: يوفّر shared_state.o التعريف، بينما يستخدمه worker.o وmain.o.

متى ينبغي استخدام متغير inline في C++17؟

اختر متغيرًا inline في C++17 عندما يكون موضع تعريف المتغير في ملف ترويسة مشترك. يدرج Clang المتغيرات inline ضمن ميزات اللغة في C++17. وتنص اللغة على أنه يجوز تعريف متغير inline ذي الربط الخارجي في وحدات ترجمة متعددة مع بقائه كيانًا واحدًا له عنوان واحد. راجع محدد inline.

في المثال نفسه، استبدل المحتوى الكامل للملف shared_state.hpp بهذه النسخة:

#ifndef SHARED_STATE_HPP
#define SHARED_STATE_HPP

namespace demo {
inline int itemCount = 0;

void addItem();
int* itemCountAddress();
}

#endif

أبقِ worker.cpp وmain.cpp دون تغيير. احذف shared_state.cpp من عملية البناء لهذا الشكل؛ فلم تعد هناك حاجة إلى تعريفه المنفصل. أعد ترجمة الملفين المستخدمين واربط ملفي الكائنات الخاصين بهما فقط:

(
    set -e
    xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
        -c worker.cpp -o worker.o
    xcrun clang++ -std=c++17 -Wall -Wextra -pedantic \
        -c main.cpp -o main.o
    xcrun clang++ worker.o main.o -o shared-demo
    ./shared-demo
)

لهذا الشكل الناتج المتوقع نفسه. لا تُدرج ملف shared_state.o القديم في أمر الربط. إن استبعاد ملفات الكائنات القديمة من قائمة الربط الصريحة يجعل تغيير الملكية واضحًا.

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

المتطلب النهج المناسب الشرط الأساسي
يتولى ملف تنفيذ واحد ملكية الحالة القابلة للتعديل تصريح في ملف ترويسة باستخدام extern؛ وتعريف واحد في ملف .cpp اربط ملف الكائنات المالك مرة واحدة بالضبط
موضع التعريف المشترك في ملف ترويسة متغير inline في C++17 تستخدم كل وحدة ترجمة مستهلكة نمط لغة متوافقًا وتعريفًا متسقًا
كل ملف مصدر يملك عمدًا حالة منفصلة متغير على مستوى مساحة الأسماء من نوع static أو مساحة أسماء غير مسماة ينشئ هذان الخياران ربطًا داخليًا، ولذلك لا ينفذان مهمة الكائن المشترك

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

شخّص المشكلة قبل تغيير أعلام المترجم

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

  1. إذا فشلت الترجمة، فتحقق من التصريحات أولًا. تأكد من أن كل ملف مصدر يضمّن shared_state.hpp ويستخدم مساحة الأسماء والنوع والتهجئة نفسها. لا يمكن للربط إصلاح اسم غير مصرّح عنه أو تصريحات متعارضة.
  2. إذا أبلغ الربط عن رمز مكرر، فاحصِ التعريفات ومدخلات الربط. ابحث عن قيمة ابتدائية في ملف الترويسة، أو تعريف ثانٍ في ملف .cpp، أو إدراج ملف الكائنات المالك نفسه مرتين. وفي شكل inline، تأكد من غياب التعريف القديم غير inline.
  3. إذا أبلغ الربط عن رمز غير معرّف، فابحث عن المالك المفقود. في نسخة extern، تأكد من ترجمة shared_state.cpp بنجاح ومن ظهور shared_state.o في أمر الربط النهائي. فالتصريح وحده لا يوفر التعريف.
  4. إذا نجح الربط لكن فشل فحص الهوية، فتحقق من الربط. ابحث عن متغيرات static على مستوى مساحة الأسماء أو تعريفات داخل مساحات أسماء غير مسماة. فهي تمنح كل وحدة ترجمة كائنًا خاصًا بها.
  5. بعد تغيير ملف ترويسة أو نهج الملكية، أعد بناء جميع الكائنات المتأثرة. كرر كتلة البناء الكاملة المناسبة لكي تستخدم الملفات المستدعية والملف المعرّف التصريحات الحالية.

يتطلب اختصاران عناية خاصة. أولًا، يؤدي استبدال المتغير العام بمتغير static int itemCount = 0; على مستوى مساحة الأسماء في ملف الترويسة إلى إنشاء كائنات منفصلة لكل وحدة ترجمة. ثانيًا، يكون لمتغير عادي من نوع const على مستوى مساحة الأسماء ربط داخلي عمومًا ما لم ينطبق استثناء، مثل تصريح صريح باستخدام extern أو inline. استخدم الربط المقصود عن قصد؛ وترد الاستثناءات ذات الصلة في البرنامج والربط.

حافظ أيضًا على ربط اللغة. يحدد extern "C" ربط لغة C؛ لكنه لا يوفر التعريف المفقود ولا يسمح بتعريفات مكررة. في هذا المثال المكتوب بالكامل بـ C++، أبقِ ربط C++ الافتراضي. تشرح مسودة المعيار ربط اللغة وخصائص التسمية واتفاقية استدعاء الدوال الخاصة بالتنفيذ في تصريحات الربط.

ما الذي يتغير عندما يحتوي تطبيق Mac أيضًا على Swift؟

يظل ترتيب التخزين في C++ جزءًا من تنفيذ C++. إن استيراد ملف ترويسة إلى Swift يوفر واجهة؛ كما يجب أن يتضمن البناء تنفيذ C++ الذي يملك متغيرًا عامًا غير inline. يشرح دليل إعداد مشروع بلغات مختلطة من Swift كيفية إتاحة واجهات C++ المدعومة عبر ملفات الترويسة العامة لإطار عمل أو ملف الترويسة الوسيط لهدف التطبيق.

في هدف Xcode الذي يحتاج إلى استدعاء C++ من Swift، يكون الإعداد الموثّق هو C++ and Objective-C interoperability وتكون قيمته C++ / Objective-C++. فعّله للهدف المستهلك وأتح ملف الترويسة المناسب. وهذا متطلب مستقل عن جعل المتغير ذا تعريف واحد. راجع إعداد مشاريع Swift وC++ متعددة اللغات.

التزم بحدود التوافق الموثّقة: يتطلب التوافق بين Swift وC++ الإصدار Swift 5.9 أو أحدث، ويجب أن تستخدم الشيفرة المختلطة بين Swift وC++ مكتبة C++ القياسية نفسها، وتستخدم منصات Apple المكتبة libc++. يتطلب استيراد ملف ترويسة يحتوي على متغير inline الوارد في هذه المقالة اختيار C++17 أو أحدث للمستورد، وكذلك لمصادر C++. توثّق Swift عناصر التحكم في معيار اللغة وقيود المترجم في الميزات المدعومة وقيود التوافق بين C++ وSwift.

تغطي الأمثلة هنا ملفات C++ التي تُترجم منفصلة ثم تُربط في ملف تنفيذي أصلي واحد. التزم بهذا النطاق عند تطبيق فحص العنوان؛ إذ تتطلب الأطر والمكتبات التي تُحمّل على نحو منفصل تصميمًا خاصًا بها للتصدير والربط. ولتنفيذ المهمة المعروضة، حدّد التعريف المالك، وأدرجه مرة واحدة في الربط، وتحقق من أن كلا الملفين المستدعيين يريان العنوان نفسه والقيمة المحدّثة.

المصادر