تحليل كود مشروع DeepSpeech أو لماذا لا يستحق الكتابة في مساحة الاسم std

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



image1.png


المقدمة



لقد بحثنا بشكل متكرر عن الأخطاء في المشاريع باستخدام التعلم الآلي ، ولم يكن DeepSpeech استثناءً بالنسبة لنا. ليس من المستغرب ، لأن هذا المشروع يحظى بشعبية كبيرة: في وقت كتابة هذا التقرير ، كان لديه بالفعل أكثر من 15 ألف نجمة على GitHub.



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



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



بهذا تنتهي المقدمة القصيرة - حان الوقت للانتقال إلى تحليل الأخطاء. بالمناسبة ، إذا أتيت إلى هنا لمعرفة إجابة السؤال الذي طرحته في عنوان المقال (لماذا لا يجب عليك الكتابة في مساحة الاسم std) ، يمكنك إلقاء نظرة على نهاية المقالة على الفور. هناك مثال مثير للاهتمام ينتظرك هناك!



نظرة عامة على 10 تحذيرات مثيرة للاهتمام صادرة عن المحلل



تحذير 1



V773 تم الخروج من الوظيفة بدون تحرير مؤشر "البيانات". من الممكن حدوث تسرب للذاكرة. 311



// EditFstData method implementations: just the Read method.
template <typename A, typename WrappedFstT, typename MutableFstT>
EditFstData<A, WrappedFstT, MutableFstT> *
EditFstData<A, WrappedFstT, MutableFstT>::Read(std::istream &strm,
                                               const FstReadOptions &opts)
{
  auto *data = new EditFstData<A, WrappedFstT, MutableFstT>();
  // next read in MutabelFstT machine that stores edits
  FstReadOptions edits_opts(opts);

  ....
  
  std::unique_ptr<MutableFstT> edits(MutableFstT::Read(strm, edits_opts));
  if (!edits) return nullptr; // <=

  ....
}


يحتوي مقتطف الشفرة هذا على مثال كلاسيكي لتسرب الذاكرة: تستدعي وظيفة القراءة " return nullptr " دون تحرير الذاكرة المخصصة مع التعبير " EditFstData " الجديد . مع هذا الخروج من الوظيفة (بدون استدعاء حذف البيانات ) ، سيتم حذف المؤشر نفسه فقط ، ولن يتم استدعاء مدمر الكائن الذي يشير إليه. وبالتالي ، سيستمر تخزين الكائن في الذاكرة ، ولن يكون من الممكن حذفه أو استخدامه.



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



تحذير 2



V1062 تحدد فئة "DfsState" عامل تشغيل "جديد" مخصص. يجب أيضًا تحديد عامل التشغيل "delete". dfs-visit.h 62



// An FST state's DFS stack state.
template <class FST>
struct DfsState {
public:
  ....
  void *operator new(size_t size, 
                     MemoryPool<DfsState<FST>> *pool) {
    return pool->Allocate();
  }
  ....
}


لا يقف PVS-Studio ثابتًا ويستمر في إضافة تشخيصات جديدة. يُعد مقتطف الشفرة هذا مثالاً رائعًا لإظهار عمل أحدث تشخيص برقم V1062 .



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



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



void *Allocate() {
  if (free_list_ == nullptr) {
    auto *link = static_cast<Link *>(mem_arena_.Allocate(1));
    link->next = nullptr;
    return link;
  } else {
    auto *link = free_list_;
    free_list_ = link->next;
    return link;
  }
}


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



لكن انتظر لحظة! تحتوي بضعة أسطر فقط أدناه على الوظيفة التالية:



void Free(void *ptr) {
  if (ptr) {
    auto *link = static_cast<Link *>(ptr);
    link->next = free_list_;
    free_list_ = link;
  }
}


هذا يعني أن لدينا بالفعل وظائف جاهزة للتخصيص والإصدار. على الأرجح ، كان على المبرمج كتابة عامل التشغيل الخاص به "delete" ، باستخدام وظيفة Free () لتحريره .



اكتشف المحلل ثلاثة أخطاء أخرى على الأقل:



  • V1062 تحدد فئة "VectorState" عامل تشغيل "جديد" مخصص. يجب أيضًا تحديد عامل التشغيل "delete". متجه- fst.h 31
  • V1062 تحدد فئة "CacheState" عامل تشغيل "جديد" مخصص. يجب أيضًا تحديد عامل التشغيل "delete". مخبأ. ح 65


تحذير 3



V703 من الغريب أن يقوم حقل "المسار الأول" في الفئة المشتقة "ShortestPathOptions" بالكتابة فوق الحقل في الفئة الأساسية "ShortestDistanceOptions". تحقق من الخطوط: أقصر مسار. h: 35 ، أقصر مسافة. h: 34. أقصر طريق ح 35



// Base class
template <class Arc, class Queue, class ArcFilter>
struct ShortestDistanceOptions {
  Queue *state_queue;    // Queue discipline used; owned by caller.
  ArcFilter arc_filter;  // Arc filter (e.g., limit to only epsilon graph).
  StateId source;        // If kNoStateId, use the FST's initial state.
  float delta;           // Determines the degree of convergence required
  bool first_path;       // For a semiring with the path property (o.w.
                         // undefined), compute the shortest-distances along
                         // along the first path to a final state found
                         // by the algorithm. That path is the shortest-path
                         // only if the FST has a unique final state (or all
                         // the final states have the same final weight), the
                         // queue discipline is shortest-first and all the
                         // weights in the FST are between One() and Zero()
                         // according to NaturalLess.

  ShortestDistanceOptions(Queue *state_queue, ArcFilter arc_filter,
                          StateId source = kNoStateId,
                          float delta = kShortestDelta)
      : state_queue(state_queue),
        arc_filter(arc_filter),
        source(source),
        delta(delta),
        first_path(false) {}
};
// Derived class
template <class Arc, class Queue, class ArcFilter>
struct ShortestPathOptions
    : public ShortestDistanceOptions<Arc, Queue, ArcFilter> {
  using StateId = typename Arc::StateId;
  using Weight = typename Arc::Weight;

  int32 nshortest;    // Returns n-shortest paths.
  bool unique;        // Only returns paths with distinct input strings.
  bool has_distance;  // Distance vector already contains the
                      // shortest distance from the initial state.
  bool first_path;    // Single shortest path stops after finding the first
                      // path to a final state; that path is the shortest path
                      // only when:
                      // (1) using the ShortestFirstQueue with all the weights
                      // in the FST being between One() and Zero() according to
                      // NaturalLess or when
                      // (2) using the NaturalAStarQueue with an admissible
                      // and consistent estimate.
  Weight weight_threshold;  // Pruning weight threshold.
  StateId state_threshold;  // Pruning state threshold.

  ShortestPathOptions(Queue *queue, ArcFilter filter, int32 nshortest = 1,
                      bool unique = false, bool has_distance = false,
                      float delta = kShortestDelta, bool first_path = false,
                      Weight weight_threshold = Weight::Zero(),
                      StateId state_threshold = kNoStateId)
      : ShortestDistanceOptions<Arc, Queue, ArcFilter>(queue, filter,
                                                       kNoStateId, delta),
        nshortest(nshortest),
        unique(unique),
        has_distance(has_distance),
        first_path(first_path),
        weight_threshold(std::move(weight_threshold)),
        state_threshold(state_threshold) {}
};


موافق ، ليس من السهل العثور على خطأ محتمل ، أليس كذلك؟



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



لفهم ما أعنيه بشكل أفضل ، أقترح التفكير في مثال قصير تركيبي من وثائقنا. لنفترض أن لدينا الكود التالي:



class U {
public:
  int x;
};

class V : public U {
public:
  int x;  // <= V703 here
  int z;
};


هنا تم تجاوز الاسم x داخل الفئة المشتقة. السؤال الآن هو: ما هي القيمة التي ستطبعها الشفرة التالية؟



int main() {
  V vClass;
  vClass.x = 1;
  U *uClassPtr = &vClass;
  std::cout << uClassPtr->x << std::endl;
  ....
}


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



تعد الأسماء المتداخلة في التسلسل الهرمي للفئة خطأً محتملاً يجب تجنبه :)



تحذير 4



V1004 تم استخدام مؤشر "aiter" بشكل غير آمن بعد التحقق من أنه مقابل nullptr. تحقق من الخطوط: 107 ، 119.زيارة. ح 119



template <....>
void Visit(....)
{
  ....
  // Deletes arc iterator if done.
  auto *aiter = arc_iterator[state];
  if ((aiter && aiter->Done()) || !visit) {
    Destroy(aiter, &aiter_pool);
    arc_iterator[state] = nullptr;
    state_status[state] |= kArcIterDone;
  }
  // Dequeues state and marks black if done.
  if (state_status[state] & kArcIterDone) {
    queue->Dequeue();
    visitor->FinishState(state);
    state_status[state] = kBlackState;
    continue;
  }
  const auto &arc = aiter->Value();       // <=
  ....
}


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



في هذه الحالة ، دعنا نرى ما يحدث للعيار إذا كان يساوي صفرًا بالفعل. أولاً ، سيتم تحديد هذا المؤشر في العبارة " if ((aiter && aiter-> Done ()) ||! Visit) ". سيساوي هذا الشرط خطأ ، ولن ندخل في فرع هذا إذا . وبعد ذلك ، وفقًا لجميع شرائع الأخطاء الكلاسيكية ، سيتم إلغاء الإشارة إلى المؤشر الفارغ: ' aiter-> Value ()؛". ينتج عن هذا إلغاء المرجع سلوك غير محدد.



تحذير 5



يحتوي المثال التالي على خطأين في وقت واحد:



  • V595 تم استخدام مؤشر 'istrm' قبل أن يتم التحقق منه مقابل nullptr. فحص الأسطر: 60 ، 61. mapped-file.cc 60
  • V595 تم استخدام مؤشر 'istrm' قبل أن يتم التحقق منه مقابل nullptr. فحص الأسطر: 39 ، 61. mapped-file.cc 39


MappedFile *MappedFile::Map(std::istream *istrm, bool memorymap,
                            const string &source, size_t size) {
  const auto spos = istrm->tellg();        // <=
  ....
  istrm->seekg(pos + size, std::ios::beg); // <=
  if (istrm) {                             // <=
    VLOG(1) << "mmap'ed region of " << size
            << " at offset " << pos
            << " from " << source
            << " to addr " << map;
  return mmf.release();
  }
  ....
}


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



image2.png


تحذير 6



V730 لم تتم تهيئة كافة أعضاء فئة داخل المنشئ. ضع في اعتبارك فحص: Stone_written_. ersatz_progress.cc 14



ErsatzProgress::ErsatzProgress()
  : current_(0)
  , next_(std::numeric_limits<uint64_t>::max())
  , complete_(next_)
  , out_(NULL)
{}


يحذرنا المحلل من أن المنشئ لا يهيئ جميع حقول بنية ErzatzProgress . دعنا نقارن هذا المُنشئ بقائمة الحقول في هذا الهيكل:



class ErsatzProgress {
  ....
private:
    void Milestone();

    uint64_t current_, next_, complete_;
    unsigned char stones_written_;
    std::ostream *out_;
};


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



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



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



تخميني أنه لم تتم تهيئة حقل Stone_written_ عن طريق الخطأ عندما رأيت مُنشئًا آخر على بعد بضعة أسطر أدناه:



ErsatzProgress::ErsatzProgress(uint64_t complete,
                               std::ostream *to,
                               const std::string &message)
  : current_(0)
  , next_(complete / kWidth)
  , complete_(complete)
  , stones_written_(0)
  , out_(to)
{
  ....
}


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



تحذير 7



V780 لا يمكن تهيئة الكائن "& params" من النوع غير الخامل (non-PDS) باستخدام وظيفة memset. 261



/* Not the best numbering system,
   but it grew this way for historical reasons
 * and I want to preserve existing binary files. */
typedef enum
{
  PROBING=0,
  REST_PROBING=1,
  TRIE=2,
  QUANT_TRIE=3,
  ARRAY_TRIE=4,
  QUANT_ARRAY_TRIE=5
}
ModelType;

....

struct FixedWidthParameters {
  unsigned char order;
  float probing_multiplier;
  // What type of model is this?
  ModelType model_type;
  // Does the end of the file 
  // have the actual strings in the vocabulary?
  bool has_vocabulary;
  unsigned int search_version;
};

....

// Parameters stored in the header of a binary file.
struct Parameters {
  FixedWidthParameters fixed;
  std::vector<uint64_t> counts;
};

....

void BinaryFormat::FinishFile(....)
{
  ....
  // header and vocab share the same mmap.
  Parameters params = Parameters();
  memset(&params, 0, sizeof(Parameters)); // <=
  ....
}


لفهم هذا التحذير ، أقترح أولاً فهم نوع نظام التوزيع العام. PDS هي اختصار لـ Passive Data Structure ، وهي بنية بيانات بسيطة. في بعض الأحيان بدلاً من "PDS" يقولون "POD" - "بيانات قديمة بسيطة". بعبارات بسيطة (أقتبس من ويكيبيديا الروسية ) ، نوع PDS هو نوع بيانات يحتوي على ترتيب محدد بشكل صارم للحقول في الذاكرة ، والذي لا يتطلب قيود الوصول والتحكم التلقائي. ببساطة ، إنه نوع بيانات يحتوي على أنواع مضمنة فقط.



الميزة المميزة لأنواع POD هي أنه يمكن تغيير المتغيرات من هذه الأنواع ومعالجتها باستخدام وظائف إدارة الذاكرة البدائية (memset و memcpy وما إلى ذلك). ومع ذلك ، لا يمكن قول هذا عن أنواع "غير نظام التوزيع العام": مثل هذا التعامل على مستوى منخفض من قيمها يمكن أن يؤدي إلى أخطاء خطيرة. على سبيل المثال ، تسرب الذاكرة أو التنظيف المزدوج لنفس المورد أو السلوك غير المحدد.



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



تحذير 8



V575 يتم تمرير المؤشر الفارغ المحتمل إلى وظيفة "memcpy". افحص الحجة الأولى. فحص السطور: 73، 68.modelstate.cc 73



Metadata*
ModelState::decode_metadata(const DecoderState& state, 
                            size_t num_results)
{
  ....
  Metadata* ret = (Metadata*)malloc(sizeof(Metadata));
  ....
  memcpy(ret, &metadata, sizeof(Metadata));
  return ret;
}


يخبرنا التحذير التالي أنه يتم تمرير مؤشر فارغ إلى وظيفة memcpy . نعم ، في الواقع ، إذا فشلت وظيفة malloc في تخصيص الذاكرة ، فستعيد NULL . في هذه الحالة ، سيتم تمرير هذا المؤشر إلى وظيفة memset ، حيث سيتم إلغاء الإشارة إليه - وبالتالي ، تعطل برنامج ساحر.



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



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



تحذير 9



سبب التحذير التالي هو نفس أسباب التحذير السابق. صحيح أنه يشير إلى خطأ مختلف قليلاً.



V769يمكن أن يكون المؤشر "middle_begin_" في تعبير "middle_begin_ + (counts.size () - 2)" فارغًا. في مثل هذه الحالة ، ستكون القيمة الناتجة عديمة المعنى ويجب عدم استخدامها. فحص الأسطر: 553 ، 552. search_trie.cc 553



template <class Quant, class Bhiksha> class TrieSearch {
....
private:
  ....
  Middle *middle_begin_, *middle_end_;
  ....
};

template <class Quant, class Bhiksha>
uint8_t *TrieSearch<Quant, Bhiksha>::SetupMemory(....)
{
  ....
  middle_begin_
    = static_cast<Middle*>(malloc(sizeof(Middle) * (counts.size() - 2)));
  middle_end_ = middle_begin_ + (counts.size() - 2);
  ....
}


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



تحذير 10



حسنًا ، أخيرًا ، تم العثور على المثال الأكثر إثارة للاهتمام في رأيي في مكتبة kenlm المضمنة في DeepSpeech:



V1061 قد يؤدي توسيع مساحة الاسم "std" إلى سلوك غير محدد. 210- نورة



// Dirty hack because g++ 4.6 at least wants
// to do a bunch of copy operations.
namespace std {
inline void iter_swap(util::SizedIterator first,
                      util::SizedIterator second)
{
  util::swap(*first, *second);
}
} // namespace std


الحيلة المسماة "الحيلة القذرة" في التعليق قذرة حقًا. النقطة المهمة هي أن مثل هذا التوسع في مساحة الاسم std يمكن أن يؤدي إلى سلوك غير محدد.



لماذا ا؟ لأن محتويات مساحة الاسم std يتم تحديدها حصريًا من قبل لجنة المعايير. هذا هو السبب في أن المعيار الدولي للغة C ++ يحظر صراحة توسيع الأمراض المنقولة جنسياً بهذه الطريقة.



المعيار الأخير المدعوم في g ++ 4.6 هو C ++ 03. هنا اقتباس مترجم من مسودة العمل النهائية لـ C ++ 03(راجع البند 17.6.4.2.1): "سلوك برنامج C ++ غير محدد إذا كان يضيف تعريفات أو تعريفات إلى مساحة الاسم std أو مساحة الاسم المتداخلة ، ما لم يتم تحديد خلاف ذلك." ينطبق هذا الاقتباس على جميع المعايير اللاحقة (C ++ 11 و C ++ 14 و C ++ 17 و C ++ 20).



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



لانيحتوي std بالفعل على وظيفة تسمى iter_swap (ملاحظة: دالة قالب) ، فمن المنطقي أن نفترض أن المبرمج أراد توسيع قدراته حتى يتمكن من العمل مع النوع use :: SizedIterator . ولكن هذا هو الحظ السيئ: فبدلاً من إضافة التخصص إلى قالب الوظيفة ، كتب المبرمج ببساطة حملًا زائدًا عاديًا . كان يجب أن يكتب على هذا النحو:



namespace std {
template <>
inline void iter_swap(util::SizedIterator first,
                      util::SizedIterator second)
{
  util::swap(*first, *second);
}
} // namespace std


ومع ذلك ، فإن هذا الرمز ليس بهذه البساطة أيضًا. النقطة المهمة هي أن هذا الرمز سيكون صالحًا فقط حتى معيار C ++ 20. نعم ، لاحظ أيضًا أن تخصصات قوالب الوظائف تؤدي إلى سلوك غير محدد (انظر مسودة العمل النهائية C ++ 20 ، القسم 16.5.4.2.1). ونظرًا لأن هذا الرمز ينتمي إلى مكتبة ، فمن المحتمل أن يتم إنشاؤه عاجلاً أم آجلاً باستخدام العلم -std = C ++ 20 . بالمناسبة ، يميز PVS-Studio أي إصدار من المعيار يتم استخدامه في الكود ، وبناءً على ذلك ، يصدر أو لا يصدر تحذيرًا. انظر بنفسك: مثال لـ C ++ 17 ، مثال لـ C ++ 20 .



في الواقع ، يمكنك القيام بذلك بشكل أسهل. لإصلاح الخطأ ، تحتاج فقط إلى نقل التعريف الخاص بك لـ iter_swapفي نفس مساحة الاسم التي تحدد فئة SizedIterator . في هذه الحالة ، في الأماكن التي يتم فيها استدعاء iter_swap ، تحتاج إلى إضافة "using std :: iter_swap؛". اتضح مثل هذا (تعريف SizedIterator الطبقة و UTIL :: مبادلة () وظيفة تم تغيير البساطة):



namespace util
{
  class SizedIterator
  {
  public:
    SizedIterator(int i) : m_data(i) {}

    int& operator*()
    {
      return m_data;
    }
  private:
    int m_data;
  };

  ....

  inline void iter_swap(SizedIterator first,
                        SizedIterator second)
  {
    std::cout << "we are inside util::iter_swap" << std::endl;
    swap(*first, *second);
  }
}


int main()
{
  double d1 = 1.1, d2 = 2.2;
  double *pd1 = &d1, *pd2 = &d2;
  util::SizedIterator si1(42), si2(43);

  using std::iter_swap;

  iter_swap(pd1, pd2);
  iter_swap(si1, si2); // "we are inside util::iter_swap"

  return 0;
}


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



وبعد ذلك - voila - ستعمل وظيفة iter_swap المخصصة كما ينبغي دون أي "حيل قذرة" وأعمال السحر الأخرى :)



image3.png


خاتمة



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



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



إذا كنت تقوم بالتطوير في C أو C ++ أو C # أو Java وكنت مثلي مهتمًا بموضوع التحليل الثابت ، فأقترح تجربة PVS-Studio بنفسك. يمكنك تنزيله من الرابط .









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



All Articles