ميزة لغتي C/C++ عن باقي اللغات

كتبه بركات يوم الأربعاء, 23 أيلول 2015

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

السبب الحقيقي والشيء الذي يميز C/C++ عن كثير من اللغات مثل Java، أنهما لايحتاجان بيئة runtime لتشغيلهما مثل JVM في Java والتي تتولى مسؤولية إدارة الذاكرة والاستثناءات، بل برامجك تترجم مباشرة للأسمبلي، هذه الميزة مهمة عند بناء أنظمة التشغيل "الحقيقة" وليست الدمى، ففي بداية تشغيل النظام، لاتوجد هناك بيئة لتشيغل وإدارة البرنامج، في C++ تحتاج للاستغناء عن الاستثناءات ومعاملات تخصيص الذاكرة new و delete في البداية، طبعاً عند بناء نظام التشغيل لايمكن استخدام معظم دوال المكتبة القياسية، بل تحتاج لإعادة كتابة دوالك الخاصة، إلا أن اللغة نفسها ليست بحاجة لهذه المكتبات لتعمل، لذا ستجد أن لغتي C/C++ مجرد تغليف لأسمبلي المعمارية التي تستخدمها.

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

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

بالمناسبة C++11 وفرت مايسمى بالمؤشرات الذكية smart pointers والتي تقلل أخطاء نسيان تحرير الذاكرة المحجوزة، المؤشرات الذكية خصوصاً std::shared_ptr توفر نوع بدائي من جمع القمامة لايتسبب بإيقاف البرنامج يسمى عداد الإشارة reference counting إلا أنه لايحل مشكلة reference cycles، فهذه تحتاج لجامع قمامة حقيقي. $ طبعاً مع تطور أداء العتاد وزيادة حجم الذواكر هذه الميزات لم تعد مهمة عند البعض مقارنة بسهولة وسرعة التطوير التي توفرها اللغات الأخرى من Java و C#، البعض يرى أنه لامانع من احتياج اللعبة لمعالج i7 وذاكرة 2GB كي تعمل بأداء جيد طالما أن تطويرها أسهل وأسرع مع C#، بينما نفس اللعبة قد تعمل بنفس الأداء على معالج أقل مثل i3 وذاكرة أقل من 1GB لو كتبت باستخدام C/C++، أيضاً هذه اللغات أصبحت تستخدم طريقة "الترجمة في الوقت المناسب" JIT حيث تترجم الـJava bytecode أو CIL في لغات .NET مثل C# إلى تعليمات المعالج الذي يشغل عليه البرنامج عند تشغيله، مما يكسبها سرعة أداء قد تقارب في حالات سرعة برامج C/C++.

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

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

في C++ التعامل مع القوالب كابوس، في كثير من المترجمات، لو حصل خطأ بسبب القوالب ستجد رسالة طويلة ملئية بالطلاسم وفي الأخير السبب أنك نسيت الفاصلة المتقوطة ;!، أيضاً قواعد اللغتين C/C++ معقدة مقارنة بباقي اللغات. انظر هنا، a * b; في C/C++ هذه قد تعني ضرب a مع b وقد تعني أيضاً إنشاء مؤشر نوعه a!، هناك أيضاً أشياء قبيحة في C++ مثل هذا المثال:

#include <iostream>
#include <string>
#include <map>
#include <vector>

int main(void) {
  using namespace std;

  map<string, vector<int> > m;
  //                    ^^^ مسافة!!

  m["a"].push_back(1); m["a"].push_back(2); m["a"].push_back(3);
  m["b"].push_back(4); m["b"].push_back(5);
  m["d"].push_back(6);

  for(map<string, vector<int> >::const_iterator i = m.begin(); i != m.end(); i++)
  {
    cout << i->first << " ->";
    for(vector<int>::const_iterator v = i->second.begin(); v != i->second.end(); v++)
    {
      cout << " " << *v;
    }
    cout << endl;
  }
}

أولاُ لاحاجة للتعليق على مدى قبح القوالب، لكن لاحظ المسافة التي أشرت لها، كل المترجمات المتوافقة مع C++89 ستعطيك خطأ لو أزلتها!، السبب أن >> تعني إزاحة لليمين حيث يترجمها المحلل الصرفي على أنها رمز واحد بينما هي في ذلك المثال رمزين >.

في C++11 هناك الكثير من التحسينات التي أضيفت للغة، كثير من المكتبات والقواعد الجديدة، بعضها:

  • ميزة استنتاج النوع auto.
  • إضافة حلقة تكرار جديدة تشبه for each.
  • المؤشرات الذكية.
  • تعدد المهام threads ومتعلقاتها.
  • التعابير القياسية.

وغيرها الكثير، المثال السابق يمكن إعادة كتابته في C++11 هكذا:

#include <iostream>
#include <string>
#include <map>
#include <vector>

int main(void) {
  using namespace std;

  map<string, vector<int>> m;
  //                   ^^^ لاحاجة الآن للمسافة

  m["a"] = {1, 2, 3};
  m["b"] = {4, 5};
  m["d"] = {6};

  for(auto i : m )
  {
    cout << i.first << " ->";
    for(auto v : i.second)
      cout << " " << v;
    cout << endl;
  }
}

ولاننسى الكثير من المكتبات الرائعة الموجهة لـC/C++ مثل boost حيث توفر كل مايخطر على بالك من مكتبات، وكذلك ظهور مترجمات جديدة مثل clang أصبحت تعطي رسائل أخطاء أوضح وأسهل.