c++ - أين ولماذا يجب علي وضع الكلمات الرئيسية "template" و "typename"؟




templates c++-faq (4)

في القوالب ، أين ولماذا يجب علي وضع typename template على أسماء تابعة؟ ما هي بالضبط أسماء تابعة على أي حال؟ لدي الكود التالي:

template <typename T, typename Tail> // Tail will be a UnionNode too.
struct UnionNode : public Tail {
    // ...
    template<typename U> struct inUnion {
        // Q: where to add typename/template here?
        typedef Tail::inUnion<U> dummy; 
    };
    template< > struct inUnion<T> {
    };
};
template <typename T> // For the last node Tn.
struct UnionNode<T, void> {
    // ...
    template<typename U> struct inUnion {
        char fail[ -2 + (sizeof(U)%2) ]; // Cannot be instantiated for any U
    };
    template< > struct inUnion<T> {
    };
};

المشكلة لدي هو في typedef Tail::inUnion<U> dummy خط typedef Tail::inUnion<U> dummy . أنا متأكد إلى حد ما أن inUnion هو اسم تابع ، و VC ++ على حق في الاختناق عليه. أعلم أيضًا أنني يجب أن أكون قادرًا على إضافة template مكان ما لإخبار المترجم بأن inUnion هو قالب - معرف. لكن أين بالضبط؟ وهل يجب أن نفترض أن inUnion هو قالب فئة ، أي inUnion<U> يُسمي نوعًا وليس وظيفة؟


مقدمة

هذا المقال مخصص ليكون بديلاً سهل القراءة لمقالة ليتب .

الغرض الأساسي هو نفسه. شرح ل "متى"؟ و لماذا؟" يجب تطبيق typename template .

ما هو الغرض من اسم typename template ؟

typename و template قابلة للاستخدام في ظروف أخرى غير عند التصريح عن قالب.

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

نشير إلى هذه الأسماء ، حيث يمكن أن يكون هناك غموض في التفسير ، مثل: " أسماء تابعة ".

تقدم هذه المشاركة شرحًا للعلاقة بين الأسماء التابعة والكلمتين الرئيسيتين.

يقول SNIPPET أكثر من 1000 كلمة

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

template<class T> void f_tmpl () { T::foo * x; /* <-- (A) */ }


قد لا يكون الأمر سهلاً كما يظن المرء ، وعلى وجه التحديد نتيجة التقييم ( أ ) يعتمد بشكل كبير على تعريف النوع الذي تم تمريره كمعلمة القالب T

يمكن اختلاف T بشكل جذري عن دلالات المعنى.

struct X { typedef int       foo;       }; /* (C) --> */ f_tmpl<X> ();
struct Y { static  int const foo = 123; }; /* (D) --> */ f_tmpl<Y> ();


سيناريوهين مختلفين :

  • إذا قمنا بتكوين قالب الوظيفة مع النوع X ، كما في ( C ) ، سيكون لدينا تصريح لمؤشر Int المسمى x ، ولكن ؛

  • إذا قمنا بتفعيل القالب بالنوع Y ، كما في ( D ) ، فإن ( A ) سوف تتكون بدلاً من ذلك من تعبير يحسب منتج 123 مضروبًا مع بعض المتغير المعلن بالفعل x .


الأساس المنطقي

يهتم معيار C ++ بأماننا ورفاهيتنا ، على الأقل في هذه الحالة.

لمنع التنفيذ من المعاناة المحتملة من المفاجآت السيئة ، تُلزم المعايير القياسية بفرز الغموض الذي يحيط بالاسم المعتمد من خلال النص صراحة على النية في أي مكان نرغب في معاملة الاسم كاسم نوع ، أو قالب معرف .

إذا لم يتم ذكر أي شيء ، فسيتم اعتبار الاسم التابع إما متغيرًا أو دالة.


كيفية التعامل مع الأسماء المستقلة ؟

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

الاسم التابع هو أي اسم يعتمد بشكل مباشر أو غير مباشر على معلمة قالب .

template<class T> void g_tmpl () {
   SomeTrait<T>::type                   foo; // (E), ill-formed
   SomeTrait<T>::NestedTrait<int>::type bar; // (F), ill-formed
   foo.data<int> ();                         // (G), ill-formed    
}

لدينا أربعة أسماء تابعة في المقتطف أعلاه:

  • ه )
    • يعتمد "type" على إنشاء مثيل لـ SomeTrait<T> ، والتي تتضمن T ، و؛
  • F )
    • تعتمد "NestedTrait" ، وهو معرف قالب ، على SomeTrait<T> و؛
    • يعتمد "type" في نهاية ( F ) على NestedTrait ، والتي تعتمد على SomeTrait<T> ، و؛
  • ز )
    • "البيانات" ، التي تبدو وكأنها قالب عضو ، هو بشكل غير مباشر اسم تابع لأن نوع foo يعتمد على إنشاء مثيل لـ SomeTrait<T> .

لا يعتبر أي من العبارات ( E ) أو ( F ) أو ( G ) صحيحًا إذا كان المترجم يفسر الأسماء التابعة كمتغيرات / وظائف (وهو ما تم ذكره سابقًا هو ما يحدث إذا لم نذكر بشكل صريح خلاف ذلك).

الحل

لجعل g_tmpl لها تعريف صالح يجب أن نخبر المترجم بوضوح أننا نتوقع نوعًا في ( E ) ، ومعرف قالب ونوعًا في ( F ) ، ومعرف قالب في ( G ).

template<class T> void g_tmpl () {
   typename SomeTrait<T>::type foo;                            // (G), legal
   typename SomeTrait<T>::template NestedTrait<int>::type bar; // (H), legal
   foo.template data<int> ();                                  // (I), legal
}

في كل مرة يشير فيها اسم ما إلى نوع ما ، يجب أن تكون جميع الأسماء المعنية إما أسماء-أنواع أو مساحات أسماء ، مع وضع ذلك في الاعتبار من السهل جدًا معرفة أننا نطبق اسم typename في بداية الاسم المؤهل بالكامل.

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


هل يمكنني فقط لصق كلمات مفتاحية أمام أي اسم؟

" هل يمكنني فقط typename template أمام أي اسم؟ لا أريد أن تقلق بشأن السياق الذي تظهر فيه ... " - Some C++ Developer

تنص القواعد في Standard على أنه يمكنك تطبيق الكلمات الأساسية طالما أنك تتعامل مع اسم مؤهل ( K ) ، ولكن إذا لم يكن الاسم مؤهلاً ، فسيكون التطبيق غير سليم ( L ).

namespace N {
  template<class T>
  struct X { };
}

         N::         X<int> a; // ...  legal
typename N::template X<int> b; // (K), legal
typename template    X<int> c; // (L), ill-formed

ملاحظة : لا typename أو template في سياق لا يتطلب فيه الأمر ممارسة جيدة ؛ فقط لأنك تستطيع القيام بشيء ما ، لا يعني أنه يجب عليك القيام بذلك.


بالإضافة إلى ذلك ، هناك سياقات لا يُسمح فيها بشكلٍ واضح typename template :

  • عند تحديد الأسس التي يرثها أي فئة

    يتم التعامل مع كل اسم مكتوب في قائمة typename الأساسية لفئة مشتقة بالفعل كاسم نوع ، typename يعني بوضوح أن تحديد اسم typename غير typename ، typename .

                       // .------- the base-specifier-list
     template<class T> // v
     struct Derived      : typename SomeTrait<T>::type /* <- ill-formed */ {
       ...
     };
    


  • عندما يكون معرّف القالب هو المسمى المشار إليه في التوجيه الذي يستخدم التوجيه لفئة مشتقة

     struct Base {
       template<class T>
       struct type { };
     };
    
     struct Derived : Base {
       using Base::template type; // ill-formed
       using Base::type;          // legal
     };
    

C ++ 11

مشكلة

في حين أن القواعد في C ++ 03 حول متى تحتاج إلى typename template معقولة إلى حد كبير ، هناك عيب مزعج واحد لصياغته

template<typename T>
struct A {
  typedef int result_type;

  void f() {
    // error, "this" is dependent, "template" keyword needed
    this->g<float>();

    // OK
    g<float>();

    // error, "A<T>" is dependent, "typename" keyword needed
    A<T>::result_type n1;

    // OK
    result_type n2; 
  }

  template<typename U>
  void g();
};

كما يمكن رؤيته ، نحتاج إلى الكلمة المفتاحية A::result_type حتى لو كان المحول البرمجي يمكنه أن يكتشف نفسه تمامًا أن A::result_type يمكن أن يكون فقط int (وهو بالتالي نوع) ، وأن this->g يمكن أن يكون فقط قالب العضو g الإعلان عنه لاحقًا (حتى إذا كان A متخصصًا بشكل صريح في مكان ما ، فلن يؤثر ذلك على الشفرة داخل هذا القالب ، لذا لا يمكن أن يتأثر معناها بتخصص لاحق لـ A !).

التأخير الحالي

لتحسين الحالة ، في C ++ 11 ، تتعقب اللغة عندما يشير نوع ما إلى القالب المرفق. لمعرفة ذلك ، يجب أن يكون قد تم تكوين النوع باستخدام نوع معين من الاسم ، وهو الاسم الخاص به (في أعلاه ، A ، A<T> ، ::A<T> ). يعرف نوع المشار إليه بواسطة هذا الاسم أن يتم إنشاء مثيل الحالي . قد يكون هناك أنواع متعددة تكون كافة إنشاء المثيل الحالي إذا كان نوع منه الاسم هو فئة عضو / متداخلة (ثم ، A::NestedClass و A كلا instantiations الحالي).

وبناءً على هذا المفهوم ، تقول اللغة أن CurrentInstantiation::Foo و Foo و CurrentInstantiationTyped->Foo (مثل A *a = this; a->Foo ) كلها أعضاء في التثبيت الحالي إذا تم العثور على أعضاء في الطبقة التي هي instantiated الحالي أو واحدة من الطبقات الأساسية غير التابعة لها (عن طريق القيام فقط البحث عن اسم على الفور).

لم يعد مطلوبًا الآن typename الكلمات الرئيسية typename إذا كان المؤهل عضوًا في إنشاء المثيل الحالي. والنقطة الرئيسية هنا للتذكر هي أن A<T> لا يزال اسمًا يعتمد على النوع (بعد أن يعتمد كل T أيضًا على النوع). ولكن من المعروف أن A<T>::result_type هو نوع - سوف يبحث المجمع بشكل سحري عن هذا النوع من الأنواع التابعة لمعرفة ذلك.

struct B {
  typedef int result_type;
};

template<typename T>
struct C { }; // could be specialized!

template<typename T>
struct D : B, C<T> {
  void f() {
    // OK, member of current instantiation!
    // A::result_type is not dependent: int
    D::result_type r1;

    // error, not a member of the current instantiation
    D::questionable_type r2;

    // OK for now - relying on C<T> to provide it
    // But not a member of the current instantiation
    typename D::questionable_type r3;        
  }
};

هذا مثير للإعجاب ، لكن هل يمكننا أن نفعل ما هو أفضل؟ تذهب اللغة إلى أبعد من ذلك وتتطلب أن يبحث التنفيذ مرة أخرى عن D::result_type عند إنشاء D::f (حتى إذا وجد معناها بالفعل في وقت التعريف). عندما تكون نتيجة البحث الآن مختلفة أو تؤدي إلى الغموض ، يكون البرنامج غير سليم ويجب إعطاء التشخيص. تخيل ما يحدث إذا حددنا C مثل هذا

template<>
struct C<int> {
  typedef bool result_type;
  typedef int questionable_type;
};

مطلوب مترجم للقبض الخطأ عند instantiating D<int>::f . لذلك تحصل على أفضل ما في العالمين: البحث "المتأخر" الذي يحميك إذا كنت قد تواجه مشاكل مع الطبقات الأساسية التابعة ، وكذلك البحث "الفوري" الذي typename من typename template .

تخصصات غير معروفة

في رمز D ، اسم typename D::questionable_type ليس عضواً في إنشاء المثيل الحالي. وبدلاً من ذلك ، تضعها اللغة كعضو في تخصص غير معروف . على وجه الخصوص ، يكون هذا هو الحال دائمًا عندما تقوم بتنفيذ DependentTypeName::Foo أو DependentTypedName->Foo أما النوع المعتمد فهو لا يتم إنشاء المثيل الحالي (في هذه الحالة ، يمكن أن يتخلى المحول البرمجي ويقول "سوف ننظر فيما بعد إلى ما هو Foo ) أو هي عملية إنشاء الحساب الحالية ولم يتم العثور على الاسم فيها أو في فصولها الأساسية غير التابعة ، وهناك أيضًا فصول أساسية تابعة.

تخيل ما يحدث إذا كان لدينا وظيفة عضو h داخل قالب الفئة A المحدد أعلاه

void h() {
  typename A<T>::questionable_type x;
}

في C ++ 03 ، اللغة المسموح بها للقبض على هذا الخطأ لأنه لا يمكن أبداً أن تكون طريقة صالحة لإنشاء مثيل A<T>::h (أيا كانت الوسيطة التي تعطيها إلى T ). في C ++ 11 ، لدى اللغة الآن فحصًا إضافيًا لإعطاء سبب آخر للمترجمين لتنفيذ هذه القاعدة. بما أن A لا تحتوي على فئات أساسية تابعة ، وتعلن A لا يوجد عضو questionable_type ، فإن الاسم A<T>::questionable_type ليس عضوًا في التأشير الحالي ولا عضوًا في التخصص غير المعروف. في هذه الحالة ، يجب أن يكون هناك أي طريقة أن يمكن ترجمة هذا التعليمة البرمجية بشكل صحيح في وقت إنشاء الحساب ، لذلك تمنع اللغة اسمًا يكون فيه المؤهل هو التأشير الحالي بحيث لا يكون عضوًا في تخصص غير معروف ولا عضوًا في عملية الإنشاء الحالية (على الرغم من ذلك ، لا يزال هذا الانتهاك غير مطلوب لتشخيصه).

أمثلة والتوافه

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

تجعل قواعد C ++ 11 التعليمة البرمجية C ++ 03 الصالحة التالية غير صحيحة (التي لم تكن مخصصة من قبل لجنة C ++ ، ولكن من المحتمل ألا تكون ثابتة)

struct B { void f(); };
struct A : virtual B { void f(); };

template<typename T>
struct C : virtual B, T {
  void g() { this->f(); }
};

int main() { 
  C<A> c; c.g(); 
}

هذا رمز C ++ 03 صالح ربط this->f إلى A::f في وقت إنشاء مثيل وكل شيء على ما يرام. ومع ذلك ، فإن C ++ 11 يربطها على الفور B::f ويتطلب إجراء تحقق مزدوج عند إجراء عملية instantiating ، للتحقق مما إذا كان البحث لا يزال مطابقًا أم لا. ومع ذلك ، عند instantiating C<A>::g ، يطبق قاعدة الهيمنة وسيبحث عن البحث A::f بدلاً من ذلك.


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

القاعدة العامة لوضع الكلمة المفتاحية typename تكون غالبًا عندما تستخدم معلمة قالب وتريد الوصول إلى typedef متداخلة أو typedef -using ، على سبيل المثال:

template<typename T>
struct test {
    using type = T; // no typename required
    using underlying_type = typename T::type // typename required
};

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

template<typename T>
struct test {
    // typename required
    using type = typename std::conditional<true, const T&, T&&>::type;
    // no typename required
    using integer = std::conditional<true, int, float>::type;
};

تتشابه القواعد العامة لإضافة مؤهِّل template الغالب إلا أنها عادةً ما تتضمن وظائف أعضاء تم إعدادها في مجموعات (ثابتة أو غير ذلك) لبنية / فئة تم إعدادها بنفسها ، على سبيل المثال:

نظرا لهذا الهيكل والوظيفة:

template<typename T>
struct test {
    template<typename U>
    void get() const {
        std::cout << "get\n";
    }
};

template<typename T>
void func(const test<T>& t) {
    t.get<int>(); // error
}

t.get<int>() محاولة الوصول إلى t.get<int>() من داخل الدالة إلى حدوث خطأ:

main.cpp:13:11: error: expected primary-expression before 'int'
     t.get<int>();
           ^
main.cpp:13:11: error: expected ';' before 'int'

وبالتالي ، في هذا السياق ، ستحتاج إلى الكلمة الرئيسية template مسبقًا وتسمىها كما يلي:

t.template get<int>()

وبهذه الطريقة ، سيقوم المجمع t.get < int هذا بشكل صحيح بدلاً من t.get < int .


typedef typename Tail::inUnion<U> dummy;

ومع ذلك ، لست متأكدًا من أن تنفيذ inUnion صحيح. إذا فهمت بشكل صحيح ، من غير المفترض أن يتم إنشاء هذه الفئة ، لذلك لن تفشل علامة التبويب "فشل" على الإطلاق. ربما يكون من الأفضل الإشارة إلى ما إذا كان النوع في الاتحاد أم لا مع قيمة منطقية بسيطة.

template <typename T, typename TypeList> struct Contains;

template <typename T, typename Head, typename Tail>
struct Contains<T, UnionNode<Head, Tail> >
{
    enum { result = Contains<T, Tail>::result };
};

template <typename T, typename Tail>
struct Contains<T, UnionNode<T, Tail> >
{
    enum { result = true };
};

template <typename T>
struct Contains<T, void>
{
    enum { result = false };
};

ملاحظة: إلقاء نظرة على Boost::Variant

PS2: إلقاء نظرة على typelists ، ولا سيما في كتاب Andrei Alexandrescu: Modern C ++ Design







dependent-name