سلسلة ++c للمحترفين الدرس 31: ما يجدر بك معرفته عما في المكتبة القياسية std في Cpp


محمد الميداوي

std::optional: القيم الاختيارية

تُستخدم القيم الاختيارية (المعروفة أيضًا باسم "أنواع الشّك") لتمثيل نوع قد يكون محتواه موجودًا أو لا، وقد قُدِّمت في C++‎ 17 على هيئة صنف ‎std::optional‎. فمثلًا، قد يحتوي كائن من النوع ‎std::optional<int>‎ على قيمة من النوع ‎int‎، أو قد لا يحتوي على أيّ قيمة. وتُستخدَم القيم الاختيارية إمّا لتمثيل قيمة قد لا تكون موجودة، أو كنوعٍ للقيمة المعادة من دالّة قد تفشل أحيانًا في إعادة نتيجة ذات معنى.

استخدام قيمة اختيارية لتمثيل غياب القيمة

كانت المؤشّرات التي تحمل القيمة ‎nullptr‎ قبل C++‎ 17 تمثّل عدم وجود أيّ قيمة، يعد هذا حلاً جيدًا للكائنات الكبيرة التي خُصِّصت ديناميكيًا وتُدار بواسطة مؤشّرات. لكن لا يعمل هذا الحلّ بشكل جيّد مع الأنواع الصغيرة أو الأوّلية (primitive) مثل ‎int‎، والتي نادرًا ما تُخصّص أو تُدار ديناميكيًا من قبل المؤشّرات، ويوفّر ‎std::optional‎ حلاً مثاليًا لهذه المشكلة الشائعة.

في المثال أدناه، عرّفنا البنية Person‎، والتي تمثل شخصًا، هذا الشخص يمكنه أن يمتلك حيوانًا أليفًا (pet)، لكنّ ذلك ليس ضروريًا. ولكي نخبر المصرّف بأنّ الحقل pet اختياري هنا، فسنُصرّح عنه بواسطة المُغلّف ‎std::optional‎.

#include <iostream>
#include <optional>
#include <string>

struct Animal {
    std::string name;
};

struct Person {
    std::string name;
    std::optional < Animal > pet;
};

int main() {
    Person person;
    person.name = "John";
    if (person.pet) {
        std::cout << person.name << "'s pet's name is " <<
            person.pet - > name << std::endl;
    } else {
        std::cout << person.name << " is alone." << std::endl;
    }
}

القيم الاختيارية كقيمة معادة

انظر المثال التالي:

std::optional < float > divide(float a, float b) {
    if (b != 0. f) return a / b;
    return {};
}

في المثال أعلاه، سنعيد الكسر ‎a/b‎، ولكن إذا لم يكن الكسر مُعرّفا (إن كان ‎b‎ يساوي 0 مثلًا)، فسَنعيد القيمة الاختيارية الفارغة. هذا مثال أكثر تعقيدًا:

template < class Range, class Pred >
    auto find_if( Range&& r, Pred&& p ) {
        using std::begin;
        using std::end;
        auto b = begin(r), e = end(r);
        auto r = std::find_if(b, e, p);
        using iterator = decltype(r);
        if (r == e)
            return std::optional < iterator > ();
        return std::optional < iterator > (r);
    }
template < class Range, class T >
    auto find( Range&& r, T const& t ) {
        return find_if( std::forward<Range>(r), [&t](auto&& x){return x==t;} );
    }

تبحث الدالّة ‎‎find( some_range, 7 )‎ في الحاوية أو النطاق ‎some_range‎ عن شيء يساوي العدد ‎7‎، وتفعل الدالة ‎find_if‎ عبر دالّة شرطية (predicate)، فتعيد إمّا قيمة اختيارية فارغة إذا لم يُعثر على أيّ شيء يساوي العدد ‎7‎، أو عنصرًا اختياريا يحتوي على مُكرّر إلى العنصر في حال كان موجودًا. هذا يتيح لك القيام بما يلي:

if (find(vec, 7)) {
    // code
}

أو حتى:

if (auto oit = find(vec, 7)) {
    vec.erase( * oit);
}

دون الحاجة إلى استخدام مُكرّرات begin/end أو إجراء الاختبارات.

value_or

انظر المثال التالي:

void print_name( std::ostream& os, std::optional<std::string> const& name ) {
std::cout "Name is: " << name.value_or("<name missing>") << '\n';
}

يعيد التابع ‎value_or‎ القيمة المخزّنة في القيمة الاختيارية، أو يعيد الوسيط إذا لم يكن هناك أيّ شيء مُخزّن. يتيح لك هذا إمكانية أخذ القيمة الاختيارية (التي يمكن أن تكون فارغة) وتحديد سلوك افتراضي عندما تكون بحاجة إلى قيمة، وهكذا الطريقة، يمكن ترك تحديد "السلوك الافتراضي" إلى أن تكون هناك حاجة إليه، بدلًا من إنشاء قيمة افتراضية داخل مُحرِّك ما.

مقاربات أخرى للقيم الاختيارية

هناك العديد من الطرق الأخرى لحلّ المشكلة التي تحلها القيم الاختياريّة ‎std::optional‎، لكن لا طريقة كاملة من تلك الطرق:

القيم الاختيارية مقابل المؤشّر

في بعض الحالات، يمكننا تمثيل "اختياريّةِ كائنٍ" عبر توفير مؤشّر يشير إلى كائن موجود أو مؤشّر فارغ ‎nullptr‎ للإشارة إلى فشل العمليّة. ولكنّ استخدام هذه الطريقة يقتصر على الحالات التي تكون فيها الكائنات موجودة بالفعل - بالمقابل، يمكن للقيم الاختيارية ‎optional‎ أن تُستخدَم لإعادة كائنات جديدة دون الحاجة إلى تخصيص الذاكرة.

القيم الاختيارية مقابل القيم التنبيهية

من المقاربات الشائعة استخدام قيمة خاصّة للإشارة إلى أنّ القيمة لا معنى لها، وقد تكون هذه القيمة مثلًا 0 أو ‎-1 بالنسبة للأعداد الصحيحة، أو ‎nullptr‎ بالنسبة للمؤشّرات.

مثلًا، لنفترض أنّ هناك دالّة تحاول العثور على فهرس أول ظهور لحرف في سلسلة نصية، في حال كان الحرف موجودًا في السلسلة النصية، فستعيد فهرس أول ظهور له، أمّا في حال لم يكن موجودًا، فستعيد القيمة ‎-1 للدلالة على أنّ الحرف غير موجود في السلسلة النصية. القيمة ‎-1 تمثل القيمة التنبيهية، لأنها تنبّهنا إلى أمر ما (في هذا المثال، تنبِّهنا إلى عدم وجود الحرف المبحوث عنه في السلسلة النصية).

المشكلة في هذه المقاربة أنّها تقلل من مساحة القيم الصالحة (لا يمكنك التمييز بين القيمة 0 الصالحة والقيمة 0 التي لا معنى لها)، وليست كل الأنواع فيها خيار طبيعي للقيم التنبيهية.

القيم الاختيارية مقابل الأزواج std::pair,>

من المقاربات الشائعة أيضًا توفير زوج يكون أحد عُنصَريه قيمةً بوليانية ‎bool‎ للإشارة إلى كون القيمة ذات معنى أم لا، ويشترط هذا أن يكون نوع القيمة قابلًا للإنشاء افتراضيًا (default-constructible) في حالة حدوث خطأ، وهو أمر غير ممكن في بعض الأنواع، وقد يكون ممكنًا ولكن غير مرغوب بالنسبة لأنواع أخرى. بالمقابل، لا تحتاج القيم الاختيارية ‎optional<T>‎ في حال حدوث خطأ إلى بناء أي شيء.

استخدام القيم الاختيارية لتمثيل فشل دالة

قبل C++‎ 17، كانت الدوالّّ تمثِّل الفشلَ عادة بإحدى الطرق التالية:

  • إعادة مؤشّر فارغ.
  • على سبيل المثال، استدعاء دالّة ‎Delegate *App::get_delegate()‎ على نسخة من الصنف ‎App‎ ليس لها مُفوّض (delegate) سيعيد ‎nullptr‎.
  • يُعدّ هذا حلاً جيدًا للكائنات التي خُصِّصت ديناميكيًا أو الكائنات الكبيرة التي تُدار عبر المُؤشّرات، لكنه ليس حلاً جيدًا بالنسبة للكائنات الصغيرة التي عادةً ما تكون مرصوصة (stack-allocated) وتُمرّر عن طريق النسخ.
  • تحجز قيمة محدّدة من النوع المُعاد للإشارة إلى الفشل.
  • على سبيل المثال، قد يؤدي استدعاء دالّة ‎unsigned shortest_path_distance(Vertex a, Vertex b)‎ على رأسين (vertices) غير مُتصلين إلى إعادة 0 للإشارة إلى هذه الحقيقة.
  • يتم إقران القيمة المُعادة مع قيمة بوليانية ‎bool‎ لتحديد ما إذا كانت القيمة المعادة ذات معنى أم لا.
  • على سبيل المثال، يؤدي استدعاء دالّة ‎std::pair<int, bool> parse(const std::string &str)‎ باستخدام وسيط يحتوي سلسلة نصّية لا تتضمّن عددًا صحيحًا إلى إعادة زوج يضمّ عددًا صحيحا غير محدد وقيمة بوليانية تساوي ‎false‎.

في هذا المثال، يُعطى لزيد حيوانين أليفَين، سوسن وسوسان، ثم تُستدعى الدالّة ‎Person::pet_with_name()‎ لاسترداد الكلب "وافي". ونظرًا لأنّ زيد لا يملك كلبًا باسم "وافي"، فإن الدالّة ستفشل، وستُعاد القيمة ‎std::nullopt‎ بدلاً من ذلك.

#include <iostream>
#include <optional>
#include <string>
#include <vector>

struct Animal {
    std::string name;
};
struct Person {
    std::string name;
    std::vector < Animal > pets;

    std::optional < Animal > pet_with_name(const std::string & name) {
        for (const Animal & pet: pets) {
            if (pet.name == name) {
                return pet;
            }
        }
        return std::nullopt;
    }
};
int main() {
    Person john;
    zaid.name = "زيد";

    Animal susan;
    susan.name = "سوسن";
    zaid.pets.push_back(susan);

    Animal susanne;
    susanne.name = "سوسان";
    zaid.pets.push_back(susanne);

    std::optional < Animal > dog = john.pet_with_name("وافي");
    if (dog) {
        std::cout << "يملك زيد حيوانًا أليفًا اسمه وافي" << std::endl;
    } else {
        std::cout <<”لا يملك زيد حيوانًا أليفًا اسمه وافي" << std::endl;
    }
}

std::function: تغليف الكائنات القابلة للاستدعاء

يوضح المثال التالي طريقة استخدام std::function:

#include <iostream>
#include <functional>
std:: function < void(int,
    const std::string & ) > myFuncObj;
void theFunc(int i,
    const std::string & s) {
    std::cout << s << ": " << i << std::endl;
}
int main(int argc, char * argv[]) {
    myFuncObj = theFunc;
    myFuncObj(10, "hello world");
}

استخدام std::function مع std::bind

إن احتجت إلى استدعاء دالّة مع تمرير وسائط إليها فإنّ استخدام ‎std::function‎ مع ‎std::bind‎ سيعطيك حلولًا فعّالة للغاية كما هو موضّح أدناه.

class A {
    public:
        std:: function < void(int,
            const std::string & ) > m_CbFunc = nullptr;
    void foo() {
        if (m_CbFunc) {
            m_CbFunc(100, "event fired");
        }
    }
};
class B {
    public:
        B() {
            auto aFunc = std::bind( & B::eventHandler, this, std::placeholders::_1,
                std::placeholders::_2);
            anObjA.m_CbFunc = aFunc;
        }
    void eventHandler(int i,
        const std::string & s) {
        std::cout << s << ": " << i << std::endl;
    }
    void DoSomethingOnA() {
        anObjA.foo();
    }
    A anObjA;
};
int main(int argc, char * argv[]) {
    B anObjB;
    anObjB.DoSomethingOnA();
}

ربط std::function مع نوع آخر قابل للاستدعاء

يوضح المثال التالي كيفية استخدام std::function لاستدعاء دالة من نمط C، ودالة تابعة لصنف، وعامل ()operator، ودالة لامدا. ويتم استدعاء الدالة من خلال الوسائط الصحيحة ووسائط بترتيب مختلف، وكذلك بأنواع وأعداد مختلفة.

#include <iostream>
#include <functional>
#include <iostream>
#include <vector>

using std::cout;
using std::endl;
using namespace std::placeholders;
// دالّة بسيطة لتُستدعى
double foo_fn(int x, float y, double z) {
    double res = x + y + z;
    std::cout << "foo_fn called with arguments: " <<
        x << ", " << y << ", " << z <<
        " result is : " << res <<
        std::endl;
    return res;
}

// بنية مع دالة تابعة لاستدعائها
struct foo_struct {
    // الدالة المراد استدعاؤها
    double foo_fn(int x, float y, double z) {
        double res = x + y + z;
        std::cout << "foo_struct::foo_fn called with arguments: " <<
            x << ", " << y << ", " << z <<
            " result is : " << res <<
            std::endl;
        return res;
    }

    // هذا التابع له بصمة مختلفة، مع ذلك يمكن استخدامه
    // لاحظ أنّ ترتيب المعاملات قد تغير
    double foo_fn_4(int x, double z, float y, long xx) {
        double res = x + y + z + xx;
        std::cout << "foo_struct::foo_fn_4 called with arguments: " <<
            x << ", " << z << ", " << y << ", " << xx <<
            " result is : " << res <<
            std::endl;
        return res;
    }

    // جعل الكائن بأكمله قابلا للاستدعاء operator() التحميل الزائد للعامل
    double operator()(int x, float y, double z) {
        double res = x + y + z;
        std::cout << "foo_struct::operator() called with arguments: " <<
            x << ", " << y << ", " << z <<
            " result is : " << res <<
            std::endl;
        return res;
    }
};
int main(void) {
    // typedefs
    using function_type = std:: function < double(int, float, double) > ;
    // foo_struct نسخة
    foo_struct fs;

    // سنخزّن هنا كل الدوالّ المربوطة
    std::vector < function_type > bindings;

    // var #1 - يمكن استخدام دالّة بسيطة وحسب
    function_type var1 = foo_fn;
    bindings.push_back(var1);

    // var #2 - يمكنك استخدام تابع
    function_type var2 = std::bind( & foo_struct::foo_fn, fs, _1, _2, _3);
    bindings.push_back(var2);

    // var #3 - يمكنك استخدام تابع مع بصمة مختلفة
    // لها عدد مختلف من المعاملات ومن أنواع مختلفة foo_fn_4 
    function_type var3 = std::bind(&foo_struct::foo_fn_4, fs, _1, _3, _2, 0l);
    bindings.push_back(var3);

    // var #4 -  مُحمَّل تحميلا زائدا operator() يمكنك استخدام كائن ذي معامل
    function_type var4 = fs;
    bindings.push_back(var4);

    // var #5 - lambda يمكنك استخدام دالّة
    function_type var5 = [](int x, float y, double z) {
        double res = x + y + z;
        std::cout << "lambda  called with arguments: " <<
            x << ", " << y << ", " << z <<
            " result is : " << res <<
            std::endl;
        return res;
    };
    bindings.push_back(var5);

    std::cout << "Test stored functions with arguments: x = 1, y = 2, z = 3" <<
        std::endl;

    for (auto f: bindings)
        f(1, 2, 3);

}

انظر هذا المثال الحي.

الناتج:

Test stored functions with arguments: x = 1, y = 2, z = 3
foo_fn called with arguments: 1, 2, 3 result is : 6
foo_struct::foo_fn called with arguments: 1, 2, 3 result is : 6
foo_struct::foo_fn_4 called with arguments: 1, 3, 2, 0 result is : 6
foo_struct::operator() called with arguments: 1, 2, 3 result is : 6
lambda  called with arguments: 1, 2, 3 result is : 6

تخزين وسائط الدالة في صف std::tuple

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

#include <iostream>
#include <functional>
#include <tuple>
#include <iostream>

 // دالّة بسيطة لاستدعائها
double foo_fn(int x, float y, double z) {
    double res = x + y + z;
    std::cout << "foo_fn called. x = " << x << " y = " << y << " z = " << z <<
        " res=" << res;
    return res;
}

// مساعِدات من أجل تسريع الصف.
template < int... > struct seq {};
template < int N, int...S > struct gens: gens < N - 1, N - 1, S... > {};
template < int...S > struct gens < 0, S... > {
    typedef seq < S... > type;
};

//استدعاء المساعِدات
template < typename FN, typename P, int...S >
    double call_fn_internal(const FN & fn,
        const P & params,
            const seq < S... > ) {
        return fn(std::get < S > (params)...);
    }

// std::tuple استدعاء الدالّة مع الوسائط المُخزنة في
template < typename Ret, typename...Args >
    Ret call_fn(const std:: function < Ret(Args...) > & fn,
        const std::tuple < Args... > & params) {
        return call_fn_internal(fn, params, typename gens < sizeof...(Args) > ::type());
    }
int main(void) {

    // الوسائط
    std::tuple < int, float, double > t = std::make_tuple(1, 5, 10);

    // الدالّة المراد استدعاؤها
    std:: function < double(int, float, double) > fn = foo_fn;

    // استدعاء الدالّة مع تمرير الوسائط المُخزّنة إليها
    call_fn(fn, t);
}

هذا مثال حي

الناتج:

foo_fn called. x = 1 y = 5 z = 10 res=16

استخدام std::function مع تعابير لامدا والصنف std::bind

#include <iostream>
#include <functional>

using std::placeholders::_1; // std::bind ستُستخدَم في مثال
int stdf_foobar(int x, std:: function < int(int) > moo) {
    return x + moo(x); // std::function moo استدعاء
}

int foo (int x) { return 2+x; }

int foo_2 (int x, int y) { return 9*x + y; }

int main()
{
int a = 2;

    /* مؤشّرات الدوالّ */
    std::cout << stdf_foobar(a, & foo) << std::endl; // 6 ( 2 + (2+2) )
    // stdf_foobar(2, foo) يمكن أن تكون أيضا

    /* Lambda تعابير */
    /* std::function في كائن lambda يمكن تخزين دالّة مُغلقة من تعبير
     */
   std::cout << stdf_foobar(a,
[capture_value](int param) -> int { return 7 + capture_value * param;
})
<< std::endl;
    // result: 15 ==  value + (7 * capture_value * value) == 2 + (7 + 3 * 2)

    /* std::bind تعابير */
    /* std::bind يمكن تمرير نتيجة التعبير
     * مثلا عبر ربط المعاملات باستدعاء مؤشّر دالّة
     */
    int b = stdf_foobar(a, std::bind(foo_2, _1, 3));
    std::cout << b << std::endl;
    // b == 23 == 2 + ( 9*2 + 3 )
    int c = stdf_foobar(a, std::bind(foo_2, 5, _1));
    std::cout << c << std::endl;
    // c == 49 == 2 + ( 9*5 + 2 )
    return 0;
}

الحمل الزائد للدوال (function overhead)

يمكن أن تتسبب std::function‎ في حِمل زائد كبير لأنّ std::function‎ لها دلالات قيمية (value semantics)، فسيكون من اللازم أن تنسخ أو تنقل ما استُدعِي إليها. ولكن بما أنها تستطيع أخذ كائن قابل للاستدعاء من أيّ نوع، فسيتعينّ عليها في كثير من الأحيان أن تخصّص ذاكرة ديناميكية بذلك.

تحتوي بعض تطبيقات ‎function‎ على ما يسمى "تحسين الكائنات الصغيرة" (small object optimization)، وفيها تُخزّن الأنواع الصغيرة مثل مؤشّرات الدوالّ أو مؤشّرات الأعضاء أو الكائنات الدالّية (Functors) ذات الحالة الصغيرة، مباشرة في كائن الدالّة ‎function‎. لكنّ هذا لن يعمل إلّا إن كان النوع قابلًا للإنشاء النقلي عند الاعتراض (noexcept move constructible). أيضًا، لا يتطلب معيار C++‎ أن توفّر جميع التطبيقات مثل هذا التحسين. إليك المثال التالي:

// ملف الترويسة
using MyPredicate = std::function<bool(const MyValue &, const MyValue &)>;
void SortMyContainer(MyContainer &C, const MyPredicate &pred);
// الملف المصدري
void SortMyContainer(MyContainer &C, const MyPredicate &pred)
{
std::sort(C.begin(), C.end(), pred);
}

يُعدّ معامل القالب هو الحلّ المفضّل لـ ‎SortMyContainer‎، ولكن إن افترضنا أنّ هذا غير ممكن أو غير مرغوب فيه لسبب من الأسباب، فلن تحتاج ‎SortMyContainer‎ إلى تخزين ‎pred‎ إلا في إطار استدعائها. ومع ذلك، فقد تُخصّص لـ ‎pred‎ ذاكرةً إن كان الكائن الدّالِّي (functor) المُعطى ذا حجم غير معروف.

تخصّص ‎function‎ الذاكرة لأنّها تحتاج إلى شيءٍ لتنسخ أو تنقل إليه، كما أنها تأخذ ملكية الكائن القابل للاستدعاء الذي مُرِّر إليها، لكنّ ‎SortMyContainer‎ لا تحتاج إلى امتلاك المُستدعِي ولكنها تحتاج إلى مرجع إليه وحسب، لذا فلا داعي لاستخدام ‎function‎ هنا. كذلك لا يوجد أي نوع دالّة قياسي يشير حصرًا إلى كائنٍ قابل للاستدعاء، لذلك سيكون عليك أن تنتظر إيجاد حلّ لهذا، أو يمكنك التعايش مع الحمل الزائد.

أيضّا، لا تملك ‎function‎ أيّ وسيلة فعّالة للتحكم في الموضع الذي تأتي منه تخصيصات الذاكرة الخاصّة بالكائن. صحيح أنّ لها مُنشئات تأخذ كائن تخصيص ‎allocator‎، ولكنّ العديد من التقديمات لا تقدّمها بالشكل الصحيح … أو لا تقدما بتاتًا.

الإصدار ≥ C++‎ 17

لم يعُد المنشئ ‎function‎ الذي يأخذ كائن تخصيصٍ ‎allocator‎ جزءًا من النوع. لذلك لا توجد أيّ طريقة لإدارة التخصيص. واعلم أن استدعاء ‎function‎ أبطأ من استدعاء المحتويات مباشرةً. كذلك يجب أن يكون الاستدعاء عبر function غير مباشر لأنّ نُسَخَ ‎function‎ يمكن أن تحتوي كائنًا قابلًا للاستدعاء، ويكافئ الحِمل الزائد الناتج عن استدعاء ‎function‎ الحِمل الزائد الناتج عن استدعاء دالّة وهمية.

std::forward_list: القوائم الإدراجية

النوع std::forward_list هو حاوية تدعم الإدراج السريع للعناصر من أي مكان فيها وكذلك إزالة تلك العناصر، لكنها لا تدعم الوصول العشوائي السريع.

يُنفَّذ النوع std::forward_list كقائمة مرتبطة أحادية (singly-linked list)، وليس لها عمومًا أيّ حِمل زائد (overhead) مقارنة بتطبيقها في C. وتوفّر هذه الحاوية، مقارنة بـ ‎std::list‎، مساحة تخزين أكثر كفاءة عند غياب الحاجة للتكرار ثنائي الاتجاه (bidirectional iteration). انظر المثال التالي:

#include <forward_list>
#include <string>
#include <iostream>

template < typename T >
    std::ostream & operator << (std::ostream & s,
        const std::forward_list < T > & v) {
        s.put('[');
        char comma[3] = {
            '\0',
            ' ',
            '\0'
        };
        for (const auto & e: v) {
            s << comma << e;
            comma[0] = ',';
        }
        return s << ']';
    }

int main() {
    // c++11 صياغة قائمة المهييء في
    std::forward_list < std::string > words1 {
        "the",
        "frogurt",
        "is",
        "also",
        "cursed"
    };
    std::cout << "words1: " << words1 << '\n';

    // words2 == words1
    std::forward_list < std::string > words2(words1.begin(), words1.end());
    std::cout << "words2: " << words2 << '\n';

    // words3 == words1
    std::forward_list < std::string > words3(words1);
    std::cout << "words3: " << words3 << '\n';

    // words4 is {"Mo", "Mo", "Mo", "Mo", "Mo"}
    std::forward_list < std::string > words4(5, "Mo");
    std::cout << "words4: " << words4 << '\n';
}

الناتج:

words1: [the, frogurt, is, also, cursed]
words2: [the, frogurt, is, also, cursed]
words3: [the, frogurt, is, also, cursed]
words4: [Mo, Mo, Mo, Mo, Mo]

التوابع

إليك قائمة التوابع الخاصة بالنوع std::forward_list:

اسم التابع التعريف
operator= يعيّن قيمًا إلى الحاوية
assign يعيّن قيمًا إلى الحاوية
get_allocator يعيد المخصِّص المرتبط به (associated allocator)
front يصل إلى العنصر الأول
before_begin يعيد مكررًا إلى العنصر قبل البداية
cbefore_begin يعيد مكررًا ثابت إلى العنصر قبل البداية
begin يعيد مكررًا إلى البداية
cbegin يعيد مكررًا ثابت إلى البداية
end يعيد مكررًا إلى النهاية
cend يعيد مكررًا إلى النهاية
empty يتحقق إن كانت الحاوية فارغة
max_size يعيد الحد الأقصى للعدد الممكن من العناصر
clear يمسح المحتويات
insert_after يُدرِج عناصرًا بعد عنصر ما
emplace_after ينشئ عنصرًا مكان عنصر آخر
erase_after يمحو عنصرًا موجودا بعد عنصر ما
push_front يدرج عنصرًا في البداية
emplace_front ينشئ عنصرًا في البداية
pop_front يزيل العنصر الأول
resize يغير عدد العناصر المخزنة
swap يبدل المحتويات
merge يدمج قائمتين مرتبتين
splice_after ينقل عناصر من قائمة أمامية أخرى
remove يزيل العناصر التي تحقق شرطا محددا
remove_if يزيل العناصر التي تحقق شرطا محددا
reverse يعكس ترتيب العناصر
unique يزيل العناصر المتساوية المتتالية
sort يرتب العناصر

std::pair: الأزواج

عوامل الموازنة

معامِلات هذه العوامل هي ‎lhs‎ و ‎rhs‎:

  • ‎operator==‎ - يتحقّق هذا العامل من أنّ عناصر كلا الزوجين ‎lhs‎ و ‎rhs‎ متساويان، وتكون القيمة المُعادة هي true إن كان lhs.first == rhs.first و lhs.second == rhs.second، وإلّا فستكون false.
std::pair < int, int > p1 = std::make_pair(1, 2);
std::pair < int, int > p2 = std::make_pair(2, 2);
if (p1 == p2)
    std::cout << "equals";
else
    std::cout << "not equal"; // ستُظهر التعليمة هذا، لأن الزوجين غير متماثلين
  • ‎operator!=‎ - يتحقّق هذا العامل ممّا إذا كان أيّ من عناصر الزوجيين ‎lhs‎ و ‎rhs‎ غير متساويين، وتكون القيمة المُعادة هيtrue إن كان ‎lhs.first != rhs.first أو lhs.second != rhs.second، وإلا فستُعاد القيمة false.
  • ‎operator<‎ - إذا كان lhs.first<rhs.first‎، فسيُعيد true وإن كان rhs.first<lhs.first‎ فسيُعيد false. وكذلك إن كان ‎lhs.second<rhs.second‎ فسيُعيد ‎true‎، أما خلاف ذلك سيُعيد ‎false‎.
  • ‎operator<=‎ - يُعيد ‎!(rhs<lhs)‎
  • ‎operator>‎ - يعيد ‎rhs<lhs
  • ‎operator>=‎ - يعيد ‎!(lhs<rhs)‎

هذا مثال آخر يستخدم حاويات أزواج، ويستخدم العامل ‎operator<‎ لترتيب الحاوية.

#include <iostream>
#include <utility>
#include <vector>
#include <algorithm>
#include <string>

int main()
{
 std::vector<std::pair<int, std::string>> v = {    {2, "baz"},
{2, "bar"},
{1, "foo"} };
    std::sort(v.begin(), v.end());

    for (const auto & p: v) {
        std::cout << "(" << p.first << "," << p.second << ") ";
        // (1,foo) (2,bar) (2,baz) :الناتج
    }
}

إنشاء زوج والوصول إلى عناصره

تتيح لنا الأزواج أن نعامل كائِنين كما لو كانا كائنًا واحدًا، ويمكن إنشاء الأزواج بسهولة بمساعدة دالّة القالب std::make_pair. وهناك طريقة أخرى، وهي إنشاء زوج وتعيين عنصُريه (‎first‎ و ‎second‎) لاحقًا.

#include <iostream>
#include <utility>

int main() {
    std::pair < int, int > p = std::make_pair(1, 2); // إنشاء الزوج
    std::cout << p.first << " " << p.second << std::endl; // الوصول إلى العناصر

    // يمكننا أيضا إنشاء الزوج وتعيين عناصره لاحقا
    std::pair < int, int > p1;
    p1.first = 3;
    p1.second = 4;
    std::cout << p1.first << " " << p1.second << std::endl;

  // يمكننا أيضا إنشاء زوج باستخدام منشئ
    std::pair < int, int > p2 = std::pair < int, int > (5, 6);
    std::cout << p2.first << " " << p2.second << std::endl;
    return 0;
}

std::atomics: الأنواع الذرية

كل استنساخ (instantiation) وتخصيص للقالب ‎std::atomic‎ يعرّف نوعًا ذريًا (atomic type)، فإن قامت أحد الخيوط (threads) بالكتابة في كائن ذرّي أثناء قراءة مسلك آخر منه، فإنّ السّلوك سيكون مُعرّفًا بشكل جيد، ولن يحدث أيّ مشكل.

إضافة إلى ذلك، قد يؤدّي الدخول إلى الكائنات الذرية إلى تهيئة تزامن بين الخيوط ويطلب دخولًا غير ذرّي للذاكرة (non-atomic memory accesses) كما هو مُعرَّف من قِبل ‎std::memory_order‎. يمكن استنساخ std::atomic مع أيّ نوع قابل للنسخ (‎TriviallyCopyable type T. std::atomic‎)، لكن std::atomic ليست قابلة للنسخ أو النقل.

توفّر المكتبة القياسية تخصيصات للقالب std::atomic للأنواع التالية:

1) تعريف تخصيص كامل للنوع البولياني ‎bool‎، وعُرِّفت قيمة التعريف النوعي (typedef) الخاصة به بحيث يُتعامل معه على أنّه نوع ذرّي std::atomic<T>‎ غير مُخصَّص، فيما عدا أنّه ستكون له مخطط (layout) قياسي، ومنشئ افتراضي أولي، ومدمّرات واضحة، كما سيدعم صيغة التهيئة الإجمالية (aggregate initialization syntax):

Typedef التخصيص
std::atomic_bool std::atomic<bool>

2) تخصيصات كاملة وتعريفات نوعية typedefs للأنواع العددية الصحيحة، كما يلي:

Typedef التخصيص
std::atomic_char std::atomic<char>
std::atomic_char std::atomic<char>
std::atomic_schar std::atomic<signed char>
std::atomic_uchar std::atomic<unsigned char>
std::atomic_short std::atomic<short>
std::atomic_ushort std::atomic<unsigned short>
std::atomic_int std::atomic<int>
std::atomic_uint std::atomic<unsigned int>
std::atomic_long std::atomic<long>
std::atomic_ulong std::atomic<unsigned long>
std::atomic_llong std::atomic<long long>
std::atomic_ullong std::atomic<unsigned long long>
std::atomic_char16_t std::atomic<char16_t>
std::atomic_char32_t std::atomic<char32_t>
std::atomic_wchar_t std::atomic<wchar_t>
std::atomic_int8_t std::atomic<std::int8_t>
std::atomic_uint8_t std::atomic<std::uint8_t>
std::atomic_int16_t std::atomic<std::int16_t>
std::atomic_uint16_t std::atomic<std::uint16_t>
std::atomic_int32_t std::atomic<std::int32_t>
std::atomic_uint32_t std::atomic<std::uint32_t>
std::atomic_int64_t std::atomic<std::int64_t>
std::atomic_uint64_t std::atomic<std::uint64_t>
std::atomic_int_least8_t std::atomic<std::int_least8_t>
std::atomic_uint_least8_t std::atomic<std::uint_least8_t>
std::atomic_int_least16_t std::atomic<std::int_least16_t>
std::atomic_uint_least16_t std::atomic<std::uint_least16_t>
std::atomic_int_least32_t std::atomic<std::int_least32_t>
std::atomic_uint_least32_t std::atomic<std::uint_least32_t>
std::atomic_int_least64_t std::atomic<std::int_least64_t>
std::atomic_uint_least64_t std::atomic<std::uint_least64_t>
std::atomic_int_fast8_t std::atomic<std::int_fast8_t>
std::atomic_uint_fast8_t std::atomic<std::uint_fast8_t>
std::atomic_int_fast16_t std::atomic<std::int_fast16_t>
std::atomic_uint_fast16_t std::atomic<std::uint_fast16_t>
std::atomic_int_fast32_t std::atomic<std::int_fast32_t>
std::atomic_uint_fast32_t std::atomic<std::uint_fast32_t>
std::atomic_int_fast64_t std::atomic<std::int_fast64_t>
std::atomic_uint_fast64_t std::atomic<std::uint_fast64_t>
std::atomic_intptr_t std::atomic<std::intptr_t>
std::atomic_uintptr_t std::atomic<std::uintptr_t>
std::atomic_size_t std::atomic<std::size_t>
std::atomic_ptrdiff_t std::atomic<std::ptrdiff_t>
std::atomic_intmax_t std::atomic<std::intmax_t>
std::atomic_uintmax_t std::atomic<std::uintmax_t>

هذا مثال بسيط على استخدام std::atomic_int:‎

#include <iostream>       // std::cout
#include <atomic>         // std::atomic, std::memory_order_relaxed
#include <thread>         // std::thread

std::atomic_int foo(0);

void set_foo(int x) {
    foo.store(x, std::memory_order_relaxed); // تعيين القيمة الذرية
}

void print_foo() {
    int x;
    do {
        x = foo.load(std::memory_order_relaxed); // الحصول على القيمة الذرّية
    } while (x == 0);
    std::cout << "foo: " << x << '\n';
}

int main() {
    std::thread first(print_foo);
    std::thread second(set_foo, 10);
    first.join();
    //second.join();
    return 0;
}
// foo: 10

std::variant: المتغايرات

إنشاء مؤشرات للتوابع الزائفة (Create pseudo-method pointers)

يمكنك استخدام كائن متغَاير (Variant) للشطب الخفيف للنوع (light weight type erasure). انظر المثال التالي:

template < class F >
    struct pseudo_method {
        F f;
        // C++17 السماح باستنتاج نوع الصنف في
        pseudo_method(F && fin): f(std::move(fin)) {}
      // عامل بحث كوينج->* لا بأس بما أنه تابع زائف
  template < class Variant > // متغاير LHS  للتحقق من أنّ  SFINAE إضافة اختبار
            friend decltype(auto) operator->*( Variant&& var, pseudo_method const& method ) {
                // تعيد تعبير لامدا يعيد توجيه استدعاء دالة ما var->*method
                // مما يجعلها تبدو كأنها تتصرف كمؤشّر تابع
                return [&](auto&&...args)->decltype(auto) {
                    // للحصول على نوع المتغاير visit استخدم
                    return std::visit(
                        [&](auto&& self)->decltype(auto) {
                            return method.f( decltype(self)(self), decltype(args)(args)... );
                        },
                        std::forward < Var > (var)
                 );
             };
         }
 };

يؤدي هذا إلى إنشاء نوع يزيد تحميل العامل ‎operator->*‎ بمتغاير ‎Variant‎ على الجانب الأيسر. في المثال التالي، سنستخدم استنتاج نوع الصنف الخاص بـ C++ 17 من أجل إيجاد وسيط القالب لـ print، يجب أن يكون self هو أول وسيط يأخذه تابع لامدا الزائف، ثم يأخذ بقية الوسائط ثم يستدعي الدالة.

pseudo_method print = [](auto&& self, auto&&...args)->decltype(auto) {
return decltype(self)(self).print( decltype(args)(args)... );
};

والآن إن كان لدينا نوعان لكل منهما تابع ‎print‎:

struct A {
    void print(std::ostream & os) const {
        os << "A";
    }
};
struct B {
    void print(std::ostream & os) const {
        os << "B";
    }
};

لاحظ أنّهما نوعان غير مترابطان، نستطيع تنفيذ ما يلي:

std::variant<A,B> var = A{};
(var->*print)(std::cout);

سيتم إرسال الاستدعاء مباشرة إلى ‎A::print(std::cout)‎، لكن لو هيأنا ‎var‎ باستخدام ‎B{}‎، فسيتم إرساله إلى ‎B::print(std::cout)‎.

وإذا أنشأنا نوعًا جديدًا C …

struct C {};

… فسيكون لدينا:

std::variant<A,B,C> var = A{};
(var->*print)(std::cout);

ستفشل عملية التصريف، لأنه لا يوجد تابع ‎C.print(std::cout)‎.

سوف يسمح توسيع الشيفرة أعلاه باكتشاف واستخدام دوال ‎print‎، ربّما باستخدام ‎if constexpr‎ ضمن التابع الزائف ‎print‎.

هذا مثال حي يستخدم ‎boost::variant‎ بدلاً من ‎std::variant‎.

الاستخدامات الرئيسية للمتغايرات

تنشي الشيفرة التالية متغايرًا (اتحادًا موسومًا tagged union) يمكنه تخزين إمّا عدد صحيح (‎int‎) وإمّا سلسلة نصية‎string‎.

std::variant< int, std::string > var;

يمكننا تخزين أحد هذين النوعين في الكائن المتغاير:

var = "hello"s;

ويمكننا الوصول إلى مُحتوياته عبر ‎std::visit‎:

// "hello\n" طباعة
visit( [](auto&& e) {
        std::cout << e << '\n';
    },
    var);

عن طريق تمرير دالّّة لامدا متعددة الأشكال أو كائن دالّّة مشابه، وإذا كنا متأكّدين من النوع، فيمكننا الحصول عليه على النحو التالي:

auto str = std::get<std::string>(var);

ولكن هذا سوف يرفع اعتراضًا إن أخطأنا تقدير النوع.

auto* str  = std::get_if<std::string>(&var);

إن أخطأت التقدير فستُعاد القيمة ‎nullptr‎.

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

كما تتيح لك المُتغايرات تخزين قيم من عدّة أنواع في متغيّر واحد بأمان وكفاءة. فهي أساسًا اتحادات‎union‎ ذكية وآمنة.

إنشاء متغاير

هذا لا يشمل المُخصِّصات (allocators).

struct A {};
struct B { B()=default; B(B const&)=default; B(int){}; };
struct C { C()=delete; C(int) {}; C(C const&)=default; };
struct D { D( std::initializer_list<int> ) {}; D(D const&)=default; D()=default; };

std::variant < A, B > var_ab0; //  A() يحتوي
std::variant < A, B > var_ab1 = 7; //  a B(7)  يحتوي
std::variant < A, B > var_ab2 = var_ab1; //  a B(7)  يحتوي
std::variant < A, B, C > var_abc0 {
    std::in_place_type < C > , 7
}; //  a C(7) يحتوي
std::variant < C > var_c0; // C  لأجل ctor غير قانوني، لا توجد قيمة افتراضية لـ
std::variant<A,D> var_ad0( std::in_place_type<D>, {1,3,3,4} ); //  D{1,3,3,4} يحتوي
std::variant < A, D > var_ad1(std::in_place_index < 0 > ); //  A{} يحتوي
std::variant<A,D> var_ad2( std::in_place_index<1>, {1,3,3,4} );  // D{1,3,3,4} يحتوي

std::iomanip و std::any

std::setprecision

عند استخدام std::setprecision في التعبير ‎out << setprecision(n)‎ أو ‎in >> setprecision(n)‎، فإنّها تضبط معامل الدقة (precision parameter) الخاصّ بمجرى الخرج أو الدخل عند القيمة n.

معامل هذه الدالّة يكون عددًا صحيحًا، ويمثل قيمة الدقة الجديدة. انظر المثال التالي:

#include <iostream>
#include <iomanip>
#include <cmath>
#include <limits>

int main() {
    const long double pi = std::acos(-1.L);
    std::cout << "default precision (6): " << pi << '\n' <<
        "std::precision(10):    " << std::setprecision(10) << pi << '\n' <<
        "max precision:         " <<
        std::setprecision(std::numeric_limits < long double > ::digits10 + 1) <<
        pi << '\n';
}
//Output
// 3.14159 :  في الدقة الافتراضية (6) يكون الناتج
// std::precision(10):    3.141592654
// 3.141592653589793239  :الدقة القصوى

std::setfill

عند استخدام std::setfill في تعبير ‎out << setfill(c)‎، فإنّها تُضبط محرف الملء (fill character) الخاصّ بمجرى الخرج عند القيمة ‎c‎.

ملاحظة: يمكن الحصول على محرف الملء الحالي عبر الدالّة ‎std::ostream::fill‎. مثال:

#include <iostream>
#include <iomanip>
int main() {
    std::cout << "default fill: " << std::setw(10) << 42 << '\n' <<
        "setfill('*'): " << std::setfill('*') <<
        std::setw(10) << 42 << '\n';
}
// 42  :الافتراضي
// setfill('*'): ********42

std::setiosflags

عند استخدام std::setiosflags في التعبير ‎out << setiosflags(mask)‎ أو ‎in >> setiosflags(mask)‎، فإنّها تضبط كل رايات التنسيق (format flags) الخاصّة بمجرى الخرج أو الدخل كما هو محدّد من قِبل القناع (mask).

هذه قائمة بكل رايات ‎std::ios_base::fmtflags‎:

  • ‎dec‎: استخدام أساس عشري لدخل وخرج (I / O) الأعداد الصحيحة
  • ‎oct‎: استخدام أساس ثماني (octal base) لدخل وخرج الأعداد الصحيحة
  • ‎hex‎: استخدام أساس ست عشري (hexadecimal base) لدخل وخرج الأعداد الصحيحة
  • ‎basefield‎ - ‎dec‎|‎oct‎|‎hex‎|‎0‎: مفيدة لتقنيع (masking) العمليات
  • ‎left‎: التعديل الأيسر (إضافة محارف الملْء إلى اليمين)
  • ‎right‎: التعديل الأيمن (إضافة محارف الملء إلى اليسار)
  • ‎internal‎: التعديل الداخلي (إضافة محارف الملء إلى نقطة معيّنة في الداخل)
  • adjustfield - left|right|internal : مفيدة لتقنيع العمليات.
  • ‎scientific‎: توليد الأنواع العددية العشرية باستخدام الصيغة العلمية، أو الصيغة الثمانية (hex notation) في حال اقترنت بـ fixed.
  • ‎fixed‎: توليد الأنواع العددية العشرية باستخدام الصيغة الثابتة (fixed notation)، أو الصيغة الثمانية (hex notation) في حال اقترنت بـ scientific. *‎floatfield‎ - ‎scientific‎|‎fixed‎|(scientific‎|‎fixed‎)|‎0‎: مفيدة لتقنيع لعمليات
  • ‎boolalpha‎: إدراج واستخراج نوع منطقي وفق تنسيق أبجدي رقمي
  • ‎showbase‎: إنشاء سابقة (prefix) تشير إلى الأساس الرقمي لخرج الأعداد الصحيحة، وتتطلّب إشارة إلى العُملة في حال الدخل والخرج الماليّ.
  • ‎showpoint‎: إنشاء محرفَ الفاصلة العشرية (decimal-point character) دون قيد أو شرط لخرج الأعداد العشرية
  • ‎showpos‎: توليد المحرف ‎+‎ للأعداد غير السالبة
  • ‎skipws‎: تخطي المسافات البيضاء الموجودة في البداية قبل عمليات الإدخال
  • ‎unitbuf‎: نقل (flush) الناتج بعد كل عملية خرج
  • ‎uppercase‎: استبدال بعض الأحرف الصغيرة بالأحرف الكبيرة المقابلة لها في بعض مخرجات عمليات الإخراج.

أمثلة على المعدِّلات:

#include <iostream>
#include <string>
#include<iomanip>

int main() {
    int l_iTemp = 47;
    std::cout << std::resetiosflags(std::ios_base::basefield);
    std::cout << std::setiosflags(std::ios_base::oct) << l_iTemp << std::endl;
    // ==> 57
    std::cout << std::resetiosflags(std::ios_base::basefield);
    std::cout << std::setiosflags(std::ios_base::hex) << l_iTemp << std::endl;
    // ==> 2f
    std::cout << std::setiosflags(std::ios_base::uppercase) << l_iTemp << std::endl;
    // ==> 2F
    std::cout << std::setfill('0') << std::setw(12);
    std::cout << std::resetiosflags(std::ios_base::uppercase);
    std::cout << std::setiosflags(std::ios_base::right) << l_iTemp << std::endl;
    // ==> 00000000002f

    std::cout << std::resetiosflags(std::ios_base::basefield | std::ios_base::adjustfield);
    std::cout << std::setfill('.') << std::setw(10);
    std::cout << std::setiosflags(std::ios_base::left) << l_iTemp << std::endl;
    // ==> 47........

    std::cout << std::resetiosflags(std::ios_base::adjustfield) << std::setfill('#');
    std::cout << std::setiosflags(std::ios_base::internal | std::ios_base::showpos);
    std::cout << std::setw(10) << l_iTemp << std::endl;
    // ==> +#######47

    double l_dTemp = -1.2;
    double pi = 3.14159265359;
    std::cout << pi << "    " << l_dTemp << std::endl;
    // ==> +3.14159   -1.2
    std::cout << std::setiosflags(std::ios_base::showpoint) << l_dTemp << std::endl;
    // ==> -1.20000
    std::cout << setiosflags(std::ios_base::scientific) << pi << std::endl;
    // ==> +3.141593e+00
    std::cout << std::resetiosflags(std::ios_base::floatfield);
    std::cout << setiosflags(std::ios_base::fixed) << pi << std::endl;
    // ==> +3.141593
    bool b = true;
    std::cout << std::setiosflags(std::ios_base::unitbuf | std::ios_base::boolalpha) << b;
    // ==> true
    return 0;
}

std::setw

انظر المثال التالي حيث يطبع السطر الثاني val في أقصى يسار شاشة الخرج، بينما يطبعها السطر الثالث في حقل إخراج طوله 10 بدءًا من النهاية اليمنى للحقل:

int val = 10;
std::cout << val << std::endl;
std::cout << std::setw(10) << val << std::endl;

يكون الخرج ما يلي:

10
10
1234567890

(السطر الأخير موجود للمساعدة على رؤية مواضع الأحرف).

عندما نحتاج إلى أن يكون الخرج مُنسّقًا بتنسيق معيّن، فقد نحتاج إلى ضبط عرض الحقل، ويمكن القيام بذلك باستخدام std::setw و std::iomanip. توضّح الشيفرة التالية صيغة ‎std::setw‎:

std::setw(int n)

يمثّل n في هذا المثال طول حقل الخرج الذي سيُعيَّن.

std::any

يوضح المثال التالي كيفية استخدام std::any:

std::any an_object{ std::string("hello world") };
if (an_object.has_value()) {
std::cout << std::any_cast<std::string>(an_object) << '\n';
}
try {
std::any_cast<int>(an_object);
} catch(std::bad_any_cast&) {
std::cout << "Wrong type\n";
}
std::any_cast<std::string&>(an_object) = "42";
std::cout << std::any_cast<std::string>(an_object) << '\n';

المخرجات الناتجة:

hello world
Wrong type
42

std::set و std::multiset: المجموعات والمجموعات المتعددة

تمثّل المجموعات "‎set‎" نوعًا من الحاويات عناصرها مُرتّبة وغير مكرّرة، أمّا المجموعات المتعدّدة ‎multiset‎، فتشبه المجموعات العادية، لكن العناصر المتعددة تكون لها نفس القيمة.

تغيير الترتيب الافتراضي لمجموعة ما

لدى الصّنفين ‎set‎ و ‎multiset‎ توابع مقارنة افتراضية، ولكن قد تحتاج أحيانًا في بعض الحالات إلى زيادة تحميلها. فمثلًا، لنفترض أنّنا نخزّن سلاسل نصية في مجموعة ما، ونحن نعلم أن تلك السلاسل تحتوي على قيم رقمية فقط. يكون الترتيب الافتراضي قائمًا على المقارنة الأبجدية للسلاسل النصّية، وعليه فلن يتطابق الترتيب مع الترتيب الرقمي. وإن أردت ترتيبها ترتيبًا عدديًا فستحتاج إلى كائن دالّي (functor) لزيادة تحميل تابع الموازنة:

#include <iostream>
#include <set>
#include <stdlib.h>

struct custom_compare final {
    bool operator()(const std::string & left,
        const std::string & right) const {
        int nLeft = atoi(left.c_str());
        int nRight = atoi(right.c_str());
        return nLeft < nRight;
    }
};

int main() {
 std::set<std::string> sut({"1", "2", "5", "23", "6", "290"});

    std::cout << "### Default sort on std::set<std::string> :" << std::endl;
    for (auto && data: sut)
        std::cout << data << std::endl;

    std::set<std::string, custom_compare> sut_custom({"1", "2", "5", "23", "6", "290"},
custom_compare {}); 

    std::cout << std::endl << "### Custom sort on set :" << std::endl;
    for (auto && data: sut_custom)
        std::cout << data << std::endl;

    auto compare_via_lambda = [](auto &&lhs, auto &&rhs){ return lhs > rhs; };
 using set_via_lambda = std::set<std::string, decltype(compare_via_lambda)>;
 set_via_lambda sut_reverse_via_lambda({"1", "2", "5", "23", "6", "290"},
compare_via_lambda);

    std::cout << std::endl << "### Lambda sort on set :" << std::endl;
    for (auto && data: sut_reverse_via_lambda)
        std::cout << data << std::endl;

    return 0;
}

يكون الخرج ما يلي:

### Default sort on std::set<std::string> :
1
2
23
290
5
6
### Custom sort on set :
1
2
5
6
23
290
### Lambda sort on set :
6
5
290
23
2
1

في المثال أعلاه، يمكن استخدام ثلاث طرق مختلفة لإضافة عمليات مقارنة إلى المجموعات "‎std::set‎"، ولكلّ منها فوائدها.

الترتيب الافتراضي

يستخدم الترتيب الافتراضي عامل المقارنة الخاصّ بالمفتاح (الوسيط الأول للقالب)، وغالبًا ما يكون المفتاح إعدادًا افتراضيًا مناسبًا للدالّة ‎std::less<T>‎. وستستخدم هذه الدالة العامل ‎operator<‎ الخاص بالكائن ما لم تكن قد خُصِّصت، هذا مفيد خاصّة عندما تحاول شيفرة أخرى استخدام ترتيب معيّن، إذ يجعل الشيفرة متناسقة.

ستؤدي كتابة الشيفرة بهذه الطريقة إلى تسهيل تحديثها عندما تكون تغييرات المفتاح جزءًا من واجهة برمجية (API)، فمثلًا إن كان لدينا صنف يحتوي على عضوين، وسيتغيّر إلى صنف يحتوي 3 أعضاء، فستُحدَّث جميع النُّسخ عبر تحديث ‎operator<‎ الخاص بالصنف. وكما تتوقع، فإن استخدام التصنيف الافتراضي كخيار افتراضي منطقي ومقبول.

الترتيب المُخصّص

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

يمكن أيضًا استخدام الترتيب المُخصّص في حال كنت ترغب في مقارنة المؤشّرات (الذكية) استنادًا إلى الكائن الذي تشير إليه، أو في حال كنت تحتاج إلى قيود خاصّة في عملية المقارنة، كمقارنة الأزواج ‎std::pair‎ بقيمة العنصر الأول ‎first‎ فقط.

يجب أن تحرص على أن يكون الترتيب مستقرًّا (stable sorting) عند إنشاء عامل مقارنة، أي أنّ نتيجة عامل المقارنة بعد الإدراج يجب ألا تتغيّر، وإلا فهذا يعني أنّ السلوك غير محدّد. لذلك احرص على ألّا يستخدم عامل المقارنة إلّا البيانات الثابتة (الأعضاء، الدوالّ الثابتة، …).

وستصادف غالبًا -كما في المثال أعلاه- أصنافًا بدون عوامل مقارنة، وينتج عن هذا منشئاتٌ افتراضية ومنشئاتُ نسخ (copy constructors)، ويسمح لك المُنشئ الافتراضي بحذف النسخة في وقت الإنشاء، كما أنّ منشئ النسخ ضروريّ لأنّ المجموعة تأخذ نسخة من مُعامل المقارنة.

الترتيب عبر تعابير لامدا

تعابير لامدا هي طريقة مختصرة لكتابة الدوالّ، وتتيح لنا كتابة امل المقارنة في سطور قليلة مما يسهل قراءة الشيفرة الكلّية.

ما يعيب استخدام تعابير لامدا هو أنّه سيكون لكلّ واحد منها نوع محدّد في وقت التصريف، لذلك سيكون التعبير ‎decltype(lambda)‎ مختلفًا في كل تُصرَّف نفس وحدة التصريف (ملف cpp) عند وجود أكثر من وحدة تصريف تكون مُدرجة في ملفات الترويسة، ولهذا يوصى باستخدام كائنات الدوالّ كعوامل مقارنة عند استخدامها داخل ملفات الترويسة.

سترى هذا النوع من الإنشاء غالبًا عند استخدام مجموعة "‎std::set‎" ضمن النطاق المحلي للدالّة. في حين يُفضَّل استعمال كائن دالّة عند استخدامه كوسيط لدالّة أو كعضو في صنف.

خيارات الترتيب الأخرى

نظرًا لأنّ عامل المقارنة الخاصّ بالمجموعات ‎std::set‎ عبارة عن وسيط قالب، فيمكن استخدام جميع الكائنات القابلة للاستدعاء كعوامل مقارنة، وما الأمثلة أعلاه إلا حالات خاصّة وحسب، ولا توجد قيود على هذه الكائنات القابلة للاستدعاء إلا ما يلي:

  • يجب أن تكون نسخة قابلة للإنشاء النّسخي (copy constructable)
  • ويجب أن تكون قابلة للاستدعاء مع وسيطين من نوع المفتاح نفسه (التحويلات الضمنية مسموح بها رغم عدم استحسانها، لأنها قد تضرّ بالأداء).

حذف قيم من مجموعة

إذا كنت تريد إفراغ المجموعة أو المجموعة المتعدّدة من كل عناصرها، فيمكنك استخدام ‎clear‎:

 std::set < int > sut;
sut.insert(10);
sut.insert(15);
sut.insert(22);
sut.insert(3);
sut.clear(); // يساوي 0 sut  حجم

ثم يمكنك استخدام التابع ‎erase‎ الذي يوفّر بعض الوظائف التي تشبه عملية الإدراج:

std::set < int > sut;
std::set < int > ::iterator it;

sut.insert(10);
sut.insert(15);
sut.insert(22);
sut.insert(3);
sut.insert(30);
sut.insert(33);
sut.insert(45);

// الحذف البسيط
sut.erase(3);

// استخدام مكرّر
it = sut.find(22);
sut.erase(it);

// حذف مجال من القيم
it = sut.find(33);
sut.erase(it, sut.end());

std::cout << std::endl << "Set under test contains:" << std::endl;
for (it = sut.begin(); it != sut.end(); ++it) {
    std::cout << * it << std::endl;
}

الخرج سيكون:

Set under test contains:



10

15

30

تنطبق كلّ هذه التوابع أيضًا على المجموعات المتعدّدة ‎multiset‎، يرجى ملاحظة أنّه في حال طلبت حذف عنصر من مجموعة متعدّدة ‎multiset‎ وكان ذلك العنصر مُكرَّرًا، فستُحذَف جميع العناصر التي تساوي ذلك العنصر.

إدراج قيم في مجموعة

هناك ثلاث طرق لإدراج العناصر في المجموعات.

  1. إدراج بسيط للقيمة باستخدام التابع insert الذي يعيد زوجًا، ممّا يسمح للمُستدعي بالتحقق مما إذا كان الإدراج قد تمّ أم لا.
  2. يمكن الإدراج بإعطاء تلميح عن الموضع الذي ستُدرج فيه القيمة، والهدف من ذلك هو تحسين وقت الإدراج، لكن المشكلة أنّنا لا نعرف دائمًا الموضع الذي يجب أن تُدرج فيه القيمة. انتبه في هذه الحالة لأن طريقة إعطاء التلميح تختلف بحسب إصدارات المصرّفات.
  3. أخيرًا، يمكنك إدراج عدة قيم عن طريق إعطاء مؤشّر للبداية (مُضمّن) والنهاية (غير مُضمّن).
#include <iostream>
#include <set>

int main() {
    std::set < int > sut;
    std::set < int > ::iterator it;
    std::pair < std::set < int > ::iterator, bool > ret;
    // إدراج بسيط
    sut.insert(7);
    sut.insert(5);
    sut.insert(12);

    ret = sut.insert(23);
    if (ret.second == true)
        std::cout << "# 23 has been inserted!" << std::endl;

    ret = sut.insert(23); // بما أنها مجموعة، والعدد 23 موجودا سلفا فيها، فستفشل عملية الإدراج
    if (ret.second == false)
        std::cout << "# 23 already present in set!" << std::endl;

    // إدراج مع تلميح لتسريع الأداء
    it = sut.end();
    // وما بعده C++11 هذه الحالة محسَّنة في
    // بالنسبة للإصدارات السابقة، يمكن التأشير إلى العنصر الذي يسبق موضع الإدراج
    sut.insert(it, 30);
    // إدراج مجال من القيم
    std::set < int > sut2;
    sut2.insert(20);
    sut2.insert(30);
    sut2.insert(45);
    std::set < int > ::iterator itStart = sut2.begin();
    std::set < int > ::iterator itEnd = sut2.end();

    sut.insert(itStart, itEnd); // يُستثنى المكرر الثاني من الإدراج
    std::cout << std::endl << "Set under test contains:" << std::endl;
    for (it = sut.begin(); it != sut.end(); ++it) {
        std::cout << * it << std::endl;
    }
    return 0;
}

سينتج لنا الخرج التالي:

# 23 has been inserted!
# 23 already present in set!
Set under test contains:
5
7
12
20
23
30
45

إدراج القيم في مجموعة متعددة

جميع طرق الإدراج الخاصّة بالمجموعات تنطبق أيضًا على المجموعات المتعدّدة، لكن هناك خيار آخر، وهو تمرير قائمة تهيئة initializer_list:

auto il = {
    7,
    5,
    12
};
std::multiset < int > msut;
msut.insert(il);

البحث عن القيم في المجموعات والمجموعات المتعدّدة

هناك عدّة طرق للبحث عن قيمة معيّنة في مجموعة ‎std::set‎ أو مجموعة متعدّدة ‎std::multiset‎، وللحصول على مُكرِّر يشير إلى موضع أوّل ظهور لمفتاح مُعيّن، يمكن استخدام الدالّة ‎find()‎ التي تعيد ‎end()‎ إذا لم يكن المفتاح موجودًا.

std::set < int > sut;
sut.insert(10);
sut.insert(15);
sut.insert(22);
sut.insert(3); // 3, 10, 15, 22 

auto itS = sut.find(10); // *itS == 10 القيمة موجودة، لذا
itS = sut.find(555); // itS == sut.end() لم يُعثَر على القيمة، لذا

std::multiset < int > msut;
sut.insert(10);
sut.insert(15);
sut.insert(22);
sut.insert(15);
sut.insert(3); // 3, 10, 15, 15, 22 

auto itMS = msut.find(10);

الطريقة الأخرى هي استخدام الدالّة ‎count()‎، والتي تحسُب عدد القيم المطابقة التي عُثِر عليها في المجموعة أو المجموعة المتعدّدة (في حالة المجموعات، ستكون القيمة المُعادة إمّا 0 أو 1).

باستخدام نفس القيم المذكورة أعلاه، سيكون لدينا:

int result = sut.count(10); // 1
result = sut.count(555); // 0

result = msut.count(10); // 1
result = msut.count(15); // 2

في حالة المجموعات المتعدّدة، يمكن أن تكون هناك عدّة عناصر لها نفس القيمة، وللحصول على مجالٍ (range) يمثّل تلك العناصر يمكن استخدام الدالّة ‎equal_range()‎ التي تُعيد زوجًا يتألّف من مُكرّر الحدّ الأدنى (مُضمّن) ومُكرّر الحدّ الأعلى (غير مضمَن) على التوالي. وإذا لم يكن المفتاح موجودًا فسيشير كلا المُكرِّران إلى أقرب قيمة عليا وفق تابع المقارنة المُستخدم لترتيب المجموعة المتعدّدة المُعطاة.

auto eqr = msut.equal_range(15);
auto st = eqr.first; //  '15' يشير إلى العنصر الأول
auto en = eqr.second; // '22' يشير إلى العنصر
eqr = msut.equal_range(9); //  '10' يشيران إلى  eqr.second و eqr.first كل من

std::integer_sequence: تسلسلات الأعداد الصحيحة

يمثّل قالب الصنف ‎std::integer_sequence<Type, Values...>‎ سلسلة من القيم العددية الصحيحة من نوع ‎Type‎، حيث ‎Type‎ هو أحد أنواع الأعداد الصحيحة المُضمّنة.

تُستخدم هذه التسلسلات عند تنفيذ قوالب الأصناف أو الدوالّ التي تحتاج إلى الوصول الموضعي (positional access)، وتحتوي المكتبة القياسية أيضًا على أنواع مصنَعيّة (factory types) تنشئ تسلسلات تصاعدية من الأعداد الصحيحة انطلاقًا من عدد العناصر المُراد.

تحويل صفّ std::tuple<T...>‎ إلى معاملات دالّة

يمكن استخدام صفّ ‎std::tuple<T...>‎ لتمرير عدّة قيم إلى دالّة، فمثلًا يمكن استخدامه لتخزين سلسلة من المعاملات على شكل صف انتظار (queue)، ويجب تحويل عناصر هذه الصفوف عند معالجتها إلى وسائط استدعاء للدالة.

#include <array>
#include <iostream>
#include <string>
include <tuple>
#include <utility>

 // ----------------------------------------------------------------------------
// الدوالّ المراد استدعاؤها
void f(int i, std::string
    const & s) {
    std::cout << "f(" << i << ", " << s << ")\n";
}
void f(int i, double d, std::string
    const & s) {
    std::cout << "f(" << i << ", " << d << ", " << s << ")\n";
}
void f(char c, int i, double d, std::string
    const & s) {
    std::cout << "f(" << c << ", " << i << ", " << d << ", " << s << ")\n";
}
void f(int i, int j, int k) {
    std::cout << "f(" << i << ", " << j << ", " << k << ")\n";
}

// ----------------------------------------------------------------------------
// الدالّة الفعلية التي توسّع الصف
template < typename Tuple, std::size_t...I >
    void process(Tuple
        const & tuple, std::index_sequence < I... > ) {
        f(std::get < I > (tuple)...);
    }

// الواجهة المراد استدعاؤها، للأسف يجب أن تُرسل إلى دالّة أخرى لاستخلاص سلسلة الفهارس المُنشأة
// std::make_index_sequence<N> من
template < typename Tuple >
    void process(Tuple
        const & tuple) {
        process(tuple, std::make_index_sequence < std::tuple_size < Tuple > ::value > ());
    }

// ----------------------------------------------------------------------------
int main() {
    process(std::make_tuple(1, 3.14, std::string("foo")));
    process(std::make_tuple('a', 2, 2.71, std::string("bar")));
    process(std::make_pair(3, std::string("pair")));
    process(std::array < int, 3 > {
        1,
        2,
        3
    });
}

طالما كان الصنف يدعم ‎std::get<I>(object)‎ و ‎std::tuple_size<T>::value‎، فيمكن توسيعه باستخدام الدالّة ‎process()‎ أعلاه، إذ أنّ الدالّة نفسها مستقلّة تمامًا عن عدد الوسائط.

إنشاء حزمة مُعاملات مُكوّنة من أعداد صحيحة

تُستخدَم std::integer_sequence لتخزين سلسلة من الأعداد الصحيحة التي يمكن تحويلها إلى حُزمة معاملات، وفائدتها الرئيسيّة هو إمكانية إنشاء قوالب الأصناف المصنعيّة التي ستنشئ تلك التسلسلات:

#include <iostream>
#include <initializer_list>
#include <utility>

template < typename T, T...I >
    void print_sequence(std::integer_sequence < T, I... > ) {
        std::initializer_list < bool > {
            bool(std::cout << I << ' ')...
        };
        std::cout << '\n';
    }

template < int Offset, typename T, T...I >
    void print_offset_sequence(std::integer_sequence < T, I... > ) {
        print_sequence(std::integer_sequence < T, T(I + Offset)... > ());
    }

int main() {
    // تحديد التسلسلات بشكل صريح
    print_sequence(std::integer_sequence < int, 1, 2, 3 > ());
    print_sequence(std::integer_sequence < char, 'f', 'o', 'o' > ());
    // توليد التسلسلات
    print_sequence(std::make_index_sequence < 10 > ());
    print_sequence(std::make_integer_sequence < short, 10 > ());
    print_offset_sequence < 'A' > (std::make_integer_sequence < char, 26 > ());
}

يَستخدم قالب الدّالة ‎print_sequence()‎ قائمة التهيئة ‎std::initializer_list<bool>‎ عند توسيع تسلسل الأعداد الصحيحة لضمان ترتيب التقييم، وتجنّب إنشاء متغيّر [مصفوفة] غير مستخدم.

تحويل سلسلة من الفهارس إلى نُسخ من عنصر ما

يؤدي توسيع حزمة معاملات من الفهارس في تعبير فاصلة (comma expression) يحمل قيمةً ما، إلى إنشاء نسخة من القيمة المقابلة لكل فهرس. ويرى المُصرِّفان ‎gcc‎ و ‎clang‎ أنّ الفهرس ليس له أيّ تأثير، لذا يطلقان تحذيرًا بشأنه (يمكن إسكات ‎gcc‎ عبر تحويل الفهرس إلى قيمة فارغة ‎void‎):

#include <algorithm>
#include <array>
#include <iostream>
#include <iterator>
#include <string>
#include <utility>

template < typename T, std::size_t...I >
    std::array < T, sizeof...(I) > make_array(T
        const & value, std::index_sequence < I... > ) {
        return std::array < T, sizeof...(I) > {
            (I, value)...
        };
    }

template < int N, typename T >
    std::array < T, N > make_array(T
        const & value) {
        return make_array(value, std::make_index_sequence < N > ());
    }

int main() {
    auto array = make_array < 20 > (std::string("value"));
    std::copy(array.begin(), array.end(),
        std::ostream_iterator < std::string > (std::cout, " "));
    std::cout << "\n";
}

هذا الدرس جزء من سلسلة مقالات عن C++‎.

ترجمة -بتصرّف- للفصول 51 وحتى 60 من كتاب C++ Notes for Professionals





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن