كيف تتسلق شجرة

الصورة 2

بتعبير أدق ، كيفية النزول منه. لكن أول الأشياء أولاً. هذه المقالة خارجة قليلاً عن تنسيق المقالة المعتاد من PVS-Studio. غالبًا ما نكتب عن فحص المشاريع الأخرى ، ولكن تقريبًا لا تفتح أبدًا باب مطبخنا الداخلي. حان الوقت لإصلاحها والتحدث عن كيفية بناء المحلل من الداخل. بتعبير أدق ، عن أهم أجزائها - شجرة النحو. ستركز المقالة على جزء PVS-Studio الذي يتعلق بلغات C و C ++.

اهم الاشياء اولا


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

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

في محلل PVS-Studio مع شجرة مبنية ، تحدث عدة أشياء:

  • . , , , using typedef, . , . ;
  • . , ;
  • , , , ;
  • . ( ). . , nullptr , , . ;
  • . , . , .

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

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

الصورة 1

كيف تعمل


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

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

من وجهة نظر المترجمين ، كل شيء قياسي هنا ، أنصح جميع المهتمين بكلاسيكيات هذا النوع - كتاب التنين .

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

في الواقع ، مثال:

int f(int a, int b)
{
  return a + b;
}

ستبدو هذه الوظيفة البسيطة بعد المعالجة بواسطة المحلل اللغوي كما يلي (يتم تمييز العقد غير الطرفية باللون الأصفر):

صورة 6

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

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

بينما سنكون مهتمين بالجزء الأيسر من الشجرة. ها هي في صورة مقربة أكبر:

صورة 10

يتميز هذا الإعلان. العقدة الأصلية PtreeDeclarator هي كائن يمكنك من خلاله الوصول إلى العقد باسم الوظيفة ومعلماتها. كما يقوم بتخزين التوقيع المشفر لنظام النوع. يبدو لي أن هذه الصورة مرئية جدًا ، ومن السهل جدًا مقارنة عناصر الشجرة بالرمز.

تبدو سهلة ، أليس كذلك؟

للتوضيح ، لنأخذ مثالاً أبسط. تخيل أن لدينا كودًا يستدعي وظيفتنا f :

f(42, 23);

سيبدو استدعاء دالة في الشجرة كما يلي:

صورة 12

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

افترض أن لدينا فقط مؤشر إلى العقدة FUNCALL الأصل . من أي محطة غير طرفية ، يمكننا الحصول على العقد الفرعية اليسرى واليمنى. نوع كل منهم معروف. نحن نعرف بنية الشجرة ، حتى نتمكن من الوصول على الفور إلى العقدة التي تكمن بموجبها قائمة الحجج - هذه هي NonLeaf ، التي تنمو منها المحطة في الصورة 42. لا نعرف عدد الحجج مقدمًا ، وهناك فواصل في القائمة لا تهمنا في هذه الحالة على الإطلاق.

كيف سنفعل ذلك؟ واصل القراءة.

مصنع دراجات


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

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

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

ثالثًا ، دعنا نتطرق إلى مثال الوظيفة. لنفترض أن لدينا رمزًا مثل هذا:

int f(int a, int b)
{
  int c = a + b;
  c *= 2;
  if (c < 42) return c;
  return 42;
}

أعرف أن هذا الرمز غبي ، لكن دعنا نركز الآن على كيفية ظهور الشجرة. لقد رأينا بالفعل إعلان وظيفة ، وهنا نحتاج إلى جسمه:

صورة 4

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

هل لاحظت؟

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

في المجموع ، لدينا حالتان على الأقل:

  • قائمة منفصلة.
  • القائمة الكاملة.

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

void ProcessArguments(Ptree* arglist)
{
  if (!arglist) return;

  Ptree* args = Second(arglist);
  while (args)
  {
    Ptree* p = args->Car();
    if (!Eq(p, ','))
    {
      ProcessArg(p);
    }

    args = args->Cdr();
  }
}

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

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

لذا ، أولاً ، تقبل هذه الدالة قائمة الحجج بين قوسين كمدخلات. شيء من هذا القبيل:

(42 ، 23) يتم استدعاء

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

يمكن العثور على وظائف مشابهة ذات منطق متغير قليلاً في العديد من الأماكن ، خاصةً في الرمز القديم.

مثال آخر. كيف أعرف إذا كان هناك استدعاء لوظيفة معينة في كتلة معينة من التعليمات البرمجية؟ مثل هذا:

bool IsFunctionCalled(const Ptree* body, std::string_view name)
{
  if (!arglist) return;

  const Ptree* statements = body;
  while (statements)
  {
    const Ptree* cur = statements->Car();
    if (IsA(cur, ntExprStatement) && IsA(cur->Car(), ntFuncallExpr))
    {
      const Ptree* funcName = First(cur->Car());
      if (Eq(funcName, name))
        return true;
    }

    statements = statements->Cdr();
  }
  return false;
}

ملحوظة. قد يلاحظ القارئ اليقظ. كم عمره؟ هناك std :: string_view العصي. كل شيء بسيط ، حتى أن أقدم رمز يتم إعادة هيكلته تدريجيًا ولن يبقى أي شيء من هذا القبيل تدريجيًا.

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

دعونا نحاول تحقيق ذلك.

ضع الشجرة في الصندوق


هدفنا هو جعل الشجرة تتصرف مثل حاوية STL. علاوة على ذلك ، لا ينبغي أن نهتم بالبنية الداخلية للقوائم ، نريد تكرارها بشكل موحد فوق العقد ، على سبيل المثال ، على النحو التالي:

void DoSomethingWithTree(const Ptree* tree)
{
  ....
  for (auto cur : someTreeContainer)
  {
    ....
  }
}

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

في أبسط الحالات ، يبدو المكرر كما يلي:

template <typename Node_t,
          std::enable_if_t<std::is_base_of_v<Node_t, Ptree>, int>>
class PtreeIterator
{
public:
  using value_type = Node_t;
  using dereference_type = value_type;
  using reference = std::add_lvalue_reference_t<value_type>;
  using pointer   = std::add_pointer_t<value_type>;
  using difference_type =
    decltype(std::declval<pointer>() - std::declval<pointer>());
  using iterator_category = std::forward_iterator_tag;

public:
  PtreeIterator(Node_t* node) noexcept : m_node{ node } {}
  ....

  PtreeIterator& operator++() noexcept
  {
    m_node = Rest(m_node);
    return *this;
  }
  dereference_type operator*() const noexcept
  {
    return static_cast<dereference_type>(First(m_node));
  }

private:
  Node_t* m_node = nullptr;
};

حتى لا أفسد الشفرة ، أزلت بعض التفاصيل. النقاط الرئيسية هنا هي dereference والزيادة. القالب مطلوب بحيث يمكن للمكرر العمل مع كل من البيانات الثابتة وغير الثابتة.

الآن سنكتب الحاوية التي سنضع فيها عقدة الشجرة. إليك الخيار الأبسط:

template <typename Node_t>
class PtreeContainer
{
public:
  using Iterator = PtreeIterator<Node_t>;
  using value_type = typename Iterator::dereference_type;
  using size_type  = size_t;
  using difference_type =
        typename Iterator::difference_type;

public:
  PtreeContainer(Node_t* nodes) :
    m_nodes{ nodes }
  {
    if (IsLeaf(m_nodes))
    {
      m_nodes = nullptr;
    }
  }

  ....

  Iterator begin() const noexcept
  { 
    return m_nodes;
  }
  Iterator end() const noexcept
  { 
    return nullptr; 
  }
  bool empty() const noexcept
  {
    return begin() == end();
  }

private:
  Node_t* m_nodes = nullptr;
};

لقد انتهينا ، يمكننا التفريق ، شكرا لاهتمامكم.

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

ليست مشكلة ، نحن فقط نضيف معلمة قالب إضافية إلى المكرر. دعنا نقول مثل هذا:

enum class PtreeIteratorTag : uint8_t
{
  Statement,
  List
};

template <typename Node_t, PtreeIteratorTag tag,
  std::enable_if_t<std::is_base_of_v<Node_t, Ptree>, int> = 0>
class PtreeIterator { .... };

كيف سيساعدنا ذلك؟ ابتدائي. سوف نتحقق من هذه المعلمة بزيادات ونتصرف وفقًا لذلك. لحسن الحظ ، في C ++ 17 يمكننا حل هذا في مرحلة التجميع باستخدام بناء if constexpr :

PtreeIterator& operator++() noexcept
{
  if constexpr (tag == PtreeIteratorTag::Statement)
  {
    m_node = Rest(m_node);
  }
  else
  {
    m_node = RestRest(m_node);
  }
  return *this;
}

هذا أفضل ، الآن يمكننا اختيار مكرر لتناسب احتياجاتنا. ماذا تفعل بالحاويات؟ يمكنك ، على سبيل المثال ، ما يلي:

template <typename Node_t, PtreeIteratorTag tag>
class PtreeContainer
{
public:
  using Iterator = PtreeIterator<Node_t, tag>;
  ....
};

الآن ، هل انتهينا بالتأكيد؟ في الواقع ، ليس حقًا.

لكن هذا ليس كل شيء


دعونا نلقي نظرة على هذا الرمز:

void ProcessEnum(Ptree* argList, Ptree* enumPtree)
{
  const ptrdiff_t argListLen = Length(argList);
  if (argListLen < 0) return;

  for (ptrdiff_t i = 0; i < argListLen; ++i)
  {
    std::string name;
    Ptree* elem;

    const EGetEnumElement r = GetEnumElementInfo(enumPtree, i, elem, name);
    ....
  }
}

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

void ProcessEnum(const Ptree* argList)
{
  for (auto elem : PtreeContainer<const Ptree, PtreeIteratorTag::List>(argList))
  {
    auto name = PtreeToString(elem);
    ....
  }
}

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

حسنًا ، دعنا نضيف متغيرًا ونفسد رمزنا الجميل:

void ProcessEnum(const Ptree* argList)
{
  size_t i = 0;
  for (auto elem : PtreeContainer<const Ptree, PtreeIteratorTag::List>(argList))
  {
    auto name = PtreeToString(elem);
    ....
    UseIndexSomehow(i++);
  }
}

لا يزال خيارًا عمليًا ، لكني أتفاعل شخصيًا مع شيء كهذا:

صورة 7

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

template <typename Node_t, PtreeIteratorTag tag,
  std::enable_if_t<std::is_base_of_v<Node_t, Ptree>, int>>
class PtreeCountingIterator
{
public:
  using size_type = size_t;
  using value_type = Node_t;
  using dereference_type = std::pair<value_type, size_type>;
  using reference = std::add_lvalue_reference_t<value_type>;
  using pointer = std::add_pointer_t<value_type>;
  using difference_type =
        decltype(std::declval<pointer>() - std::declval<pointer>());
  using iterator_category = std::forward_iterator_tag;

public:
  PtreeCountingIterator(Node_t* node) noexcept : m_node{ node } {}
  ....

  PtreeCountingIterator& operator++() noexcept
  {
    if constexpr (tag == PtreeIteratorTag::Statement)
    {
      m_node = Rest(m_node);
    }
    else
    {
      m_node = RestRest(m_node);
    }

    ++m_counter;
    return *this;
  }

  dereference_type operator*() const noexcept
  {
    return { static_cast<value_type>(First(m_node)), counter() };
  }

private:
  Node_t* m_node = nullptr;
  size_type m_counter = 0;
};

الآن يمكننا كتابة هذا الرمز ، أليس كذلك؟

void ProcessEnum(const Ptree* argList)
{
  for (auto [elem, i] :
            PtreeCountedContainer<const Ptree, PtreeIteratorTag::List>(argList))
  {
    auto name = PtreeToString(elem);
    ....
    UseIndexSomehow(i);
  }
}

بشكل عام ، يمكننا ذلك حقًا ، ولكن هناك مشكلة واحدة. إذا نظرت إلى هذا الرمز ، فقد تلاحظ أننا قدمنا ​​كيانًا آخر - شيء اسمه PtreeCountedContainer . يبدو أن الوضع يزداد تعقيدًا. لا أرغب في التلاعب بأنواع مختلفة من الحاويات ، وبالنظر إلى أنها متشابهة من الداخل ، تصل اليد نفسها إلى ماكينة حلاقة Occam.

سيتعين علينا استخدام مكرر كمعلمة قالب للحاوية ، ولكن المزيد عن ذلك لاحقًا.

أنواع حديقة الحيوان


شتت عن عدادات وأصناف التكرار لمدة دقيقة. في السعي وراء تجاوز عالمي للعقد ، نسينا أهم شيء - الشجرة نفسها.

ألق نظرة على هذا الرمز:

int a, b, c = 0, d;

ما نراه في الشجرة:

صورة 13

دعنا الآن نكرر قائمة المذيعين ، ولكن أولاً سأخبرك بشيء آخر عن الشجرة. طوال الوقت قبل ذلك ، كنا نتعامل مع مؤشر لفئة Ptree . هذه هي الفئة الأساسية التي يتم وراثة جميع أنواع العقد الأخرى منها. من خلال واجهاتهم يمكننا الحصول على معلومات إضافية. على وجه الخصوص، يمكن للالعلوي عقدة في الصورة يعود لنا قائمة المعلنون دون استخدام الوظائف ذات المنفعة مثل الأولى و الثانية . أيضا ، نحن لا نحتاج إلى طرق منخفضة المستوى Car و Cdr (مرحبا لمحبي لغة Lisp). هذا أمر جيد للغاية ، لأنه في التشخيص يمكننا تجاهل تنفيذ الشجرة. أعتقد أن الجميع يوافق على أن التجريد الحالي سيء للغاية.

هذه هي الطريقة التي يبدو بها تجاوز جميع المذيعين:

void ProcessDecl(const PtreeDeclarator* decl) { .... }

void ProcessDeclarators(const PtreeDeclaration* declaration)
{
  for (auto decl : declaration->GetDeclarators())
  {
    ProcessDecl(static_cast<const PtreeDeclarator*>(decl));
  }
}

إرجاع الأسلوب GetDeclarators حاوية قابلة للتكرار . نوعه في هذه الحالة هو PtreeContainer <const Ptree ، PtreeIteratorTag :: List> .

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

يبدو أن الوقت قد حان لتغيير المكرر مرة أخرى وإضافة القدرة على الإرسال.

template <typename Node_t, typename Deref_t, PtreeIteratorTag tag,
  std::enable_if_t<std::is_base_of_v<Node_t, Ptree>, int>>
class PtreeIterator
{
public:
  using value_type = Deref_t;
  using dereference_type = value_type;
  using reference = std::add_lvalue_reference_t<value_type>;
  using pointer = std::add_pointer_t<value_type>;
  using difference_type =
        decltype(std::declval<pointer>() - std::declval<pointer>());
  using iterator_category = std::forward_iterator_tag;
  ....
}

لكي لا نكتب كل وسيطات النموذج هذه يدويًا في كل مرة ، نضيف بعض الأسماء المستعارة لجميع المناسبات:

template <typename Node_t, typename Deref_t = std::add_pointer_t<Node_t>>
using PtreeStatementIterator =
PtreeIterator<Node_t, Deref_t, PtreeIteratorTag::Statement>;

template <typename Node_t, typename Deref_t = std::add_pointer_t<Node_t>>
using PtreeListIterator =
PtreeIterator<Node_t, Deref_t, PtreeIteratorTag::List>;

template <typename Node_t, typename Deref_t = std::add_pointer_t<Node_t>>
using PtreeStatementCountingIterator =
PtreeCountingIterator<Node_t, Deref_t, PtreeIteratorTag::Statement>;

template <typename Node_t, typename Deref_t = std::add_pointer_t<Node_t>>
using PtreeListCountingIterator =
PtreeCountingIterator<Node_t, Deref_t, PtreeIteratorTag::List>;

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

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

صورة 39

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

سندرس هذا السؤال في القسم التالي.

سحر النمط


إذن هذا ما نحتاجه:

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

بادئ ذي بدء ، نحن بحاجة إلى ربط نوع الحاوية بطريقة ما بنوع المكرر من خلال معلمات القالب. إليك ما حدث:

template <template <typename, typename> typename FwdIt,
          typename Node_t,
          typename Deref_t = std::add_pointer_t<Node_t>>
class PtreeContainer
{
public:
  using Iterator = FwdIt<Node_t, Deref_t>;
  using value_type = typename Iterator::dereference_type;
  using size_type  = size_t;
  using difference_type = typename Iterator::difference_type;

public:
  PtreeContainer(Node_t* nodes) :
    m_nodes{ nodes }
  {
    if (IsLeaf(m_nodes))
    {
      m_nodes = nullptr;
    }
  }

  ....
  Iterator begin() const noexcept
  { 
    return m_nodes;
  }
  Iterator end() const noexcept
  { 
    return nullptr; 
  }
  bool empty() const noexcept
  {
    return begin() == end();
  }
  ....

private:
  Node_t* m_nodes = nullptr;
};

أيضا ، يمكنك إضافة المزيد من الطرق إلى الحاوية. على سبيل المثال ، هذه هي الطريقة التي يمكننا بها معرفة عدد العناصر:

difference_type count() const noexcept
{
  return std::distance(begin(), end());
}

أو هنا عامل الفهرسة:

value_type operator[](size_type index) const noexcept
{
  size_type i = 0;
  for (auto it = begin(); it != end(); ++it)
  {
    if (i++ == index)
    {
      return *it;
    }
  }

  return value_type{};
}

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

لسهولة الاستخدام ، أضف الأسماء المستعارة:

template <typename Node_t, typename Deref_t = std::add_pointer_t<Node_t>>
using PtreeStatementList =
PtreeContainer<PtreeStatementIterator, Node_t, Deref_t>;

template <typename Node_t, typename Deref_t = std::add_pointer_t<Node_t>>
using PtreeItemList =
PtreeContainer<PtreeListIterator, Node_t, Deref_t>;

template <typename Node_t, typename Deref_t = std::add_pointer_t<Node_t>>
using PtreeCountedStatementList =
PtreeContainer<PtreeStatementCountingIterator, Node_t, Deref_t>;

template <typename Node_t, typename Deref_t = std::add_pointer_t<Node_t>>
using PtreeCountedItemList =
PtreeContainer<PtreeListCountingIterator, Node_t, Deref_t>;

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

using DeclList =
      Iterators::PtreeItemList<Ptree, PtreeDeclarator*>;
using ConstDeclList =
      Iterators::PtreeItemList<const Ptree, const PtreeDeclarator*>;
             :
void ProcessDecl(const PtreeDeclarator* decl) { .... }

void ProcessDeclarators(const PtreeDeclaration* declaration)
{
  for (auto decl : declaration->GetDeclarators())
  {
    ProcessDecl(decl);
  }
}

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

template <typename Node_t>
PtreeStatementList<Node_t> MakeStatementList(Node_t* node)
{
  return { node };
}

template <typename Node_t>
PtreeItemList<Node_t> MakeItemList(Node_t* node)
{
  return { node };
}

template <typename Node_t>
PtreeCountedStatementList<Node_t> MakeCountedStatementList(Node_t* node)
{
  return { node };
}

template <typename Node_t>
PtreeCountedItemList<Node_t> MakeCountedItemList(Node_t* node)
{
  return { node };
}

أذكر الوظيفة التي عملت مع التعداد . الآن يمكننا كتابتها على النحو التالي:

void ProcessEnum(const Ptree* argList)
{
  for (auto [elem, i] : MakeCountedItemList(argList))
  {
    auto name = PtreeToString(elem);
    ....
    UseIndexSomehow(i);
  }
}

مقارنة مع النسخة الأصلية ، يبدو لي أنه أصبح أفضل:

void ProcessEnum(Ptree* argList, Ptree* enumPtree)
{
  const ptrdiff_t argListLen = Length(argList);
  if (argListLen < 0) return;

  for (ptrdiff_t i = 0; i < argListLen; ++i)
  {
    std::string name;
    Ptree* elem;

    const EGetEnumElement r = GetEnumElementInfo(enumPtree, i, elem, name);
    ....
    UseIndexSomehow(i);
  }
}

هذا كل ما لدي أيها الناس


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

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

ملاحظة


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

الرمز

مكرر منتظم


template <typename Node_t, typename Deref_t, PtreeIteratorTag tag,
std::enable_if_t<std::is_base_of_v<Node_t, Ptree>, int> = 0>
class PtreeIterator
{
public:
  using value_type = Deref_t;
  using dereference_type = value_type;
  using reference = std::add_lvalue_reference_t<value_type>;
  using pointer = std::add_pointer_t<value_type>;
  using difference_type =
        decltype(std::declval<pointer>() - std::declval<pointer>());
  using iterator_category = std::forward_iterator_tag;

public:
  PtreeIterator(Node_t* node) noexcept : m_node{ node } {}
  PtreeIterator() = delete;
  PtreeIterator(const PtreeIterator&) = default;
  PtreeIterator& operator=(const PtreeIterator&) = default;
  PtreeIterator(PtreeIterator&&) = default;
  PtreeIterator& operator=(PtreeIterator&&) = default;

  bool operator==(const PtreeIterator & other) const noexcept
  {
    return m_node == other.m_node;
  }
  bool operator!=(const PtreeIterator & other) const noexcept
  {
    return !(*this == other);
  }
  PtreeIterator& operator++() noexcept
  {
    if constexpr (tag == PtreeIteratorTag::Statement)
    {
      m_node = Rest(m_node);
    }
    else
    {
      m_node = RestRest(m_node);
    }
    return *this;
  }
  PtreeIterator operator++(int) noexcept
  {
    auto tmp = *this;
    ++(*this);
    return tmp;
  }
  dereference_type operator*() const noexcept
  {
    return static_cast<dereference_type>(First(m_node));
  }
  pointer operator->() const noexcept
  {
    return &(**this);
  }

  Node_t* get() const noexcept
  {
    return m_node;
  }

private:
  Node_t* m_node = nullptr;
};

template <typename Node_t, typename Deref_t = std::add_pointer_t<Node_t>>
using PtreeStatementIterator =
PtreeIterator<Node_t, Deref_t, PtreeIteratorTag::Statement>;

template <typename Node_t, typename Deref_t = std::add_pointer_t<Node_t>>
using PtreeListIterator =
PtreeIterator<Node_t, Deref_t, PtreeIteratorTag::List>;

مزيل مع عداد


template <typename Node_t, typename Deref_t, PtreeIteratorTag tag,
std::enable_if_t<std::is_base_of_v<Node_t, Ptree>, int> = 0>
class PtreeCountingIterator
{
public:
  using size_type = size_t;
  using value_type = Deref_t;
  using dereference_type = std::pair<value_type, size_type>;
  using reference = std::add_lvalue_reference_t<value_type>;
  using pointer = std::add_pointer_t<value_type>;
  using difference_type =
        decltype(std::declval<pointer>() - std::declval<pointer>());
  using iterator_category = std::forward_iterator_tag;

 public:
  PtreeCountingIterator(Node_t* node) noexcept : m_node{ node } {}
  PtreeCountingIterator() = delete;
  PtreeCountingIterator(const PtreeCountingIterator&) = default;
  PtreeCountingIterator& operator=(const PtreeCountingIterator&) = default;
  PtreeCountingIterator(PtreeCountingIterator&&) = default;
  PtreeCountingIterator& operator=(PtreeCountingIterator&&) = default;

  bool operator==(const PtreeCountingIterator& other) const noexcept
  {
    return m_node == other.m_node;
  }
  bool operator!=(const PtreeCountingIterator& other) const noexcept
  {
    return !(*this == other);
  }
  PtreeCountingIterator& operator++() noexcept
  {
    if constexpr (tag == PtreeIteratorTag::Statement)
    {
      m_node = Rest(m_node);
    }
    else
    {
      m_node = RestRest(m_node);
    }

    ++m_counter;
    return *this;
  }
  PtreeCountingIterator operator++(int) noexcept
  {
    auto tmp = *this;
    ++(*this);
    return tmp;
  }
  dereference_type operator*() const noexcept
  {
    return { static_cast<value_type>(First(m_node)), counter() };
  }
  value_type operator->() const noexcept
  {
    return (**this).first;
  }

  size_type counter() const noexcept
  {
    return m_counter;
  }
  Node_t* get() const noexcept
  {
    return m_node;
  }

private:
  Node_t* m_node = nullptr;
  size_type m_counter = 0;
};

template <typename Node_t, typename Deref_t = std::add_pointer_t<Node_t>>
using PtreeStatementCountingIterator =
PtreeCountingIterator<Node_t, Deref_t, PtreeIteratorTag::Statement>;

template <typename Node_t, typename Deref_t = std::add_pointer_t<Node_t>>
using PtreeListCountingIterator =
PtreeCountingIterator<Node_t, Deref_t, PtreeIteratorTag::List>;

حاوية عامة


template <template <typename, typename> typename FwdIt,
          typename Node_t, typename Deref_t = std::add_pointer_t<Node_t>>
class PtreeContainer
{
public:
  using Iterator = FwdIt<Node_t, Deref_t>;
  using value_type = typename Iterator::dereference_type;
  using size_type  = size_t;
  using difference_type = typename Iterator::difference_type;

public:
  PtreeContainer(Node_t* nodes) :
    m_nodes{ nodes }
  {
    if (IsLeaf(m_nodes))
    {
      m_nodes = nullptr;
    }
  }

  PtreeContainer() = default;
  PtreeContainer(const PtreeContainer&) = default;
  PtreeContainer& operator=(const PtreeContainer&) = default;
  PtreeContainer(PtreeContainer&&) = default;
  PtreeContainer& operator=(PtreeContainer&&) = default;

  bool operator==(std::nullptr_t) const noexcept
  {
    return empty();
  }
  bool operator!=(std::nullptr_t) const noexcept
  {
    return !(*this == nullptr);
  }
  bool operator==(Node_t* node) const noexcept
  {
    return get() == node;
  }
  bool operator!=(Node_t* node) const noexcept
  {
    return !(*this == node);
  }
  bool operator==(PtreeContainer other) const noexcept
  {
    return get() == other.get();
  }
  bool operator!=(PtreeContainer other) const noexcept
  {
    return !(*this == other);
  }
  value_type operator[](size_type index) const noexcept
  {
    size_type i = 0;
    for (auto it = begin(); it != end(); ++it)
    {
      if (i++ == index)
      {
        return *it;
      }
    }

    return value_type{};
  }

  Iterator begin() const noexcept
  { 
    return m_nodes;
  }
  Iterator end() const noexcept
  { 
    return nullptr; 
  }
  bool empty() const noexcept
  {
    return begin() == end();
  }

  value_type front() const noexcept
  {
    return (*this)[0];
  }
  value_type back() const noexcept
  {
    value_type last{};
    for (auto cur : *this)
    {
      last = cur;
    }

    return last;
  }
  Node_t* get() const noexcept
  {
    return m_nodes;
  }

  difference_type count() const noexcept
  {
    return std::distance(begin(), end());
  }
  bool has_at_least(size_type n) const noexcept
  {
    size_type counter = 0;
    for (auto it = begin(); it != end(); ++it)
    {
      if (++counter == n)
      {
        return true;
      }
    }
    return false;
  }

private:
  Node_t* m_nodes = nullptr;
};

template <typename Node_t, typename Deref_t = std::add_pointer_t<Node_t>>
using PtreeStatementList =
PtreeContainer<PtreeStatementIterator, Node_t, Deref_t>;

template <typename Node_t, typename Deref_t = std::add_pointer_t<Node_t>>
using PtreeItemList =
PtreeContainer<PtreeListIterator, Node_t, Deref_t>;

template <typename Node_t, typename Deref_t = std::add_pointer_t<Node_t>>
using PtreeCountedStatementList =
PtreeContainer<PtreeStatementCountingIterator, Node_t, Deref_t>;

template <typename Node_t, typename Deref_t = std::add_pointer_t<Node_t>>
using PtreeCountedItemList =
PtreeContainer<PtreeListCountingIterator, Node_t, Deref_t>;



إذا كنت ترغب في مشاركة هذه المقالة مع جمهور يتحدث الإنجليزية ، فيرجى استخدام رابط الترجمة: Yuri Minaev. كيف تتسلق شجرة .

All Articles