سلسلة ++c للمحترفين الدرس 23: التحميل الزائد للعوامل (Operator Overloading) في Cpp


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

يمكن تعريف عوامل مثل ‎+‎ و ‎->‎ في ++C من أجل استخدامها مع الأنواع المُعرّفة من قِبل المستخدم. فمثلًا، تعرِّف الترويسة العامل ‎+‎ لضمّ (concatenate) السلاسل النصية، وهذا ممكن عن طريق تعريف عامِل باستخدام الكلمة المفتاحية ‎operator‎.

العوامل الحسابية (Arithmetic operators)

من الممكن زيادة تحميل جميع العوامل الحسابية الأساسية:

  • ‎+‎ ، ‎+=‎
  • ‎-‎ ، ‎-=‎
  • ‎*‎ ، ‎*=‎
  • ‎/‎ ، ‎/=‎
  • ‎&‎ ، ‎&=‎
  • ‎|‎ ، ‎|=‎
  • ‎^‎ ، ‎^=‎
  • ‎>>‎ ، ‎>>=‎
  • ‎<<‎ ، ‎<<=‎

يتشابه التحميل الزائد في كل العوامل كما سترى فيما يأتي من الشرح، ولزيادة التحميل خارج الأصناف (‎class‎) والبنيات (‎struct‎)، يجب تطبيق العامل +operator وفق العامل =+operator. انظر المثال التالي:

T operator+(T lhs, const T& rhs) {
    lhs += rhs;
    return lhs;
}
T& operator+=(T& lhs, const T& rhs) {
    // إجراء عملية الجمع   
    return lhs;
}

التحميل الزائد داخل الأصناف والبنيات: انظر المثال التالي حيث يجب تطبيق العامل +operator وفق العامل =+operator.

T operator + (const T & rhs) {
    * this += rhs;
    return *this;
}
T & operator += (const T & rhs) {
    // إجراء عملية الجمع
    return *this;
}

ملاحظة: يجب أن يعيد ‎operator+‎ قيمة غير ثابتة، إذ أنّ إعادة مرجع لن يكون له معنى -إذ يُرجع كائنًا جديدًا- ولا إعادة قيمة ثابتة ‎const‎ كذلك إذ يجب أن تتجنّب عمومًا الإعادة بقيمة ثابتة، ويُمرّر الوسيط الأول بالقيمة (by value)، للسببين التاليين:

  1. نظرًا لأنّك لا تستطيع تعديل الكائن الأصلي، ذلك أن ‎Object foobar = foo + bar;‎ لا ينبغي أن يعدّل ‎foo‎ على أيّ حال لأنه لا فائدة من ذلك.
  2. لا يمكنك جعله ثابتًا لأنّك ستحتاج إلى تعديل الكائن لما أن ‎operator+‎ تُنفَّذ بواسطة ‎operator+=‎ الذي يعدّل الكائن

التمرير بمرجع ثابت &const هو أحد الخيارات المتاحة، لكن سيتعيّن عليك حينها إنشاء نسخة مؤقّتة من الكائن المُمرّر، أما إن مرّرت الوسيط بقيمته (by value) فسيتكفّل المُصرّف بذلك نيابة عنك. كذلك فإن ‎operator+=‎ يعيد مرجعًا إلى نفسه، وهكذا يمكن سَلْسَلَته، لكن لا تستخدم المتغيّر نفسه، إذ أنّ ذلك سيؤدي إلى سلوك غير محدّد.

الوسيط الأوّل هو مرجع نريد تعديله لكنه ليس ثابتًا، لأنك لن تستطيع تعديله عندئذ، ولا ينبغي تعديل الوسيط الثاني، ويُمرَّر بمرجِع ثابت ‎const&‎ لأسباب تتعلق بالأداء، إذ أن تمرير الوسيط بمرجع ثابت أسرع من تمريرِه بالقيمة.

عامل فهرسة المصفوفات (Array subscript operator)

يمكن زيادة تحميل عامل فهرسة المصفوفات ‎[]‎، ويجب عليك دائمًا تطبيق نسختين، إحداهما ثابتة (‎const‎)، والأخرى غير ثابتة، لأنّه إن كان الكائن ثابتًا فلن يستطيع تعديل الكائن المُعاد من قِبل عامل الفهرسة ‎[]‎. تُمرَّر الوسائط بمرجع ثابت (‎const&‎) بدلاً من قيَمها لأنّ التمرير بالمرجع أسرع من التمرير بالقيمة، كما أنها تكون ثابتة حتى لا يُغيِّرَ العامِل الفهرسَ عن طريق الخطأ، وتعيد العوامل القيمة بالمرجع، لأنّها مصممة بطريقة تمكِّنك من تعديل الكائن ‎[]‎ المُعاد، انظر المثال التالي حيث نغير القيمة من 1 إلى 2، إذ لم يكن ذلك ممكنًا إن لم يُعَد بالمرجع.

std::vector<int> v{ 1 };
v[0] = 2; 

لا يمكنك زيادة التحميل إلا داخل صنف أو بنية، انظر المثال التالي حيث يكون I هو نوع الفهرس، ويكون غالبًا عددًا صحيحًا:

T& operator[](const I& index)
    // افعل شيئا ما
    // أعِد شيئًا ما
}
const T& operator[](const I& index) const
    // افعل شيئا ما
    // إعادة شيء ما
}

يمكن إنشاء عدّة عوامل فهرسة [][]... عبر الكائنات الوكيلة (proxy objects). انظر المثال التالي:

template < class T >
    class matrix {
        //  [][] يسمح الصنف بتحميل 
        template < class C >
            class proxy_row_vector {
                using reference = decltype(std::declval < C > ()[0]);
                using const_reference = decltype(std::declval < C
                    const > ()[0]);
                public:
                    proxy_row_vector(C& _vec, std::size_t _r_ind, std::size_t _cols): vec(_vec), row_index(_r_ind), cols(_cols) {}
                const_reference operator[](std::size_t _col_index) const {
                    return vec[row_index * cols + _col_index];
                }
                reference operator[](std::size_t _col_index) {
                    return vec[row_index * cols + _col_index];
                }
                private:
                    C& vec;
                std::size_t row_index;    // فهرس الصفوف
                std::size_t cols;        // عدد الأعمدة في المصفوفة
            };
        using const_proxy = proxy_row_vector <
            const std::vector < T >> ;
        using proxy = proxy_row_vector < std::vector < T >> ;
        public:
            matrix(): mtx(), rows(0), cols(0) {}
        matrix(std::size_t _rows, std::size_t _cols): mtx(_rows*_cols), rows(_rows), cols(_cols) {}
        //  []  متبوعا باستدعاء آخر لـ operator[] استدعاء
        const_proxy operator[](std::size_t _row_index) const {
            return const_proxy(mtx, _row_index, cols);
        }
        proxy operator[](std::size_t _row_index) {
            return proxy(mtx, _row_index, cols);
        }
        private:
            std::vector < T > mtx;
        std::size_t rows;
        std::size_t cols;
    };

عوامل التحويل

يمكنك زيادة تحميل عوامل النوع (type operators) بحيث يمكن تحويل النوع ضمنيًا إلى نوع آخر، ويجب تعريف عامل التحويل في صنف (‎class‎) أو بنية (‎struct‎):

operator T() const { /* إعادة شيء ما */ }

*ملاحظة: يكون العامل ثابتًا حتى يسمح بتحويل الكائنات الثابتة. انظر المثال التالي حيث نحول Text ضمنيًا إلى *const char:

struct Text {
    std::string text;
    // هنا نحوله ضمنيًا:
    /*explicit*/
    operator const char*() const { return text.data(); }
    // ^^^^^^^
    // لتعطيل التحويل الضمني
};
Text t;
t.text = "Hello world!";
// OK
const char* copyoftext = t;

نظرة أخرى على الأعداد المركبة

تستخدم الشيفرة التالية نوعًا يمثل الأعداد المركّبة، حيث يُحوَّل الحقل الأساسي (underlying field) تلقائيًا وفقًا لقواعد تحويل الأنواع وبتطبيق العوامل الأساسية الأربعة (+ و - و * و /) مع عضو من حقل آخر (سواء كان من النوع ‎complex<T>‎، أو من نوع عددي آخر). لنرى الآن المثال التالي الذي يوضّح مفهوم زيادة تحميل العوامل وكيفية استخدام القوالب:

#include <type_traits>

namespace not_std{

using std::decay_t;

//----------------------------------------------------------------
// complex< value_t >
//----------------------------------------------------------------

template<typename value_t>
struct complex
{
value_t x;
value_t y;
complex &operator += (const value_t &x)
{
this->x += x;
return *this;
}
complex &operator += (const complex &other)
{
this->x += other.x;
this->y += other.y;
return *this;
}

complex &operator -= (const value_t &x)
{
this->x -= x;
return *this;
}
complex &operator -= (const complex &other)
{
this->x -= other.x;
this->y -= other.y;
return *this;
}

complex &operator *= (const value_t &s)
{
this->x *= s;
this->y *= s;
return *this;
}
complex &operator *= (const complex &other)
{
(*this) = (*this) * other;
return *this;
}

complex &operator /= (const value_t &s)
{
this->x /= s;
this->y /= s;
return *this;
}

complex &operator /= (const complex &other)
{
(*this) = (*this) / other;
return *this;
}
complex(const value_t &x, const value_t &y)
: x{x}
, y{y}
{}

template<typename other_value_t>
explicit complex(const complex<other_value_t> &other)
: x{static_cast<const value_t &>(other.x)}
, y{static_cast<const value_t &>(other.y)}
{}
complex &operator = (const complex &) = default;
complex &operator = (complex &&) = default;
complex(const complex &) = default;
complex(complex &&) = default;
complex() = default;
};
//  تربيع القيمة المطلقة
template<typename value_t>
value_t absqr(const complex<value_t> &z)
{ return z.x*z.x + z.y*z.y; }

//----------------------------------------------------------------
// operator - (negation) - عامل النفي 
//----------------------------------------------------------------

template<typename value_t>
complex<value_t> operator - (const complex<value_t> &z)
{ return {-z.x, -z.y}; }

//----------------------------------------------------------------
// + عامل
//----------------------------------------------------------------
template<typename left_t,typename right_t>
auto operator + (const complex<left_t> &a, const complex<right_t> &b)
-> complex<decay_t<decltype(a.x + b.x)>>
{ return{a.x + b.x, a.y + b.y}; }

template<typename left_t,typename right_t>
auto operator + (const left_t &a, const complex<right_t> &b)
-> complex<decay_t<decltype(a + b.x)>>
{ return{a + b.x, b.y}; }

template<typename left_t,typename right_t>
auto operator + (const complex<left_t> &a, const right_t &b)
-> complex<decay_t<decltype(a.x + b)>>
{ return{a.x + b, a.y}; }

//----------------------------------------------------------------
//  - عامل
//----------------------------------------------------------------

template<typename left_t,typename right_t>
auto operator - (const complex<left_t> &a, const complex<right_t> &b)
-> complex<decay_t<decltype(a.x - b.x)>>
{ return{a.x - b.x, a.y - b.y}; }

template<typename left_t,typename right_t>
auto operator - (const left_t &a, const complex<right_t> &b)
-> complex<decay_t<decltype(a - b.x)>>
{ return{a - b.x, - b.y}; }

template<typename left_t,typename right_t>
auto operator - (const complex<left_t> &a, const right_t &b)
-> complex<decay_t<decltype(a.x - b)>>
{ return{a.x - b, a.y}; }

//----------------------------------------------------------------
// * عامل
//----------------------------------------------------------------

template<typename left_t, typename right_t>
auto operator * (const complex<left_t> &a, const complex<right_t> &b)
-> complex<decay_t<decltype(a.x * b.x)>>
{
return {
a.x*b.x - a.y*b.y,
a.x*b.y + a.y*b.x
};
}

template<typename left_t, typename right_t>
auto operator * (const left_t &a, const complex<right_t> &b)
-> complex<decay_t<decltype(a * b.x)>>
{ return {a * b.x, a * b.y}; }

template<typename left_t, typename right_t>
auto operator * (const complex<left_t> &a, const right_t &b)
-> complex<decay_t<decltype(a.x * b)>>
{ return {a.x * b, a.y * b}; }

//----------------------------------------------------------------
//  / عامل
//----------------------------------------------------------------

template<typename left_t, typename right_t>
auto operator / (const complex<left_t> &a, const complex<right_t> &b)
-> complex<decay_t<decltype(a.x / b.x)>>
{
const auto r = absqr(b);
return {
( a.x*b.x + a.y*b.y) / r,
(-a.x*b.y + a.y*b.x) / r
};
}

template<typename left_t, typename right_t>
auto operator / (const left_t &a, const complex<right_t> &b)
-> complex<decay_t<decltype(a / b.x)>>
{
const auto s = a/absqr(b);
return {
b.x * s,
-b.y * s
};
}

template<typename left_t, typename right_t>
auto operator / (const complex<left_t> &a, const right_t &b)
-> complex<decay_t<decltype(a.x / b)>>
{ return {a.x / b, a.y / b}; }

}    // not_std فضاء الاسم

int main(int argc, char **argv)
{
using namespace not_std;

complex<float> fz{4.0f, 1.0f};
// complex<double> إنشاء
auto dz = fz * 1.0;

// complex<double> ما يزال
auto idz = 1.0f/dz;

// complex<double> ما يزال
auto one = dz * idz;

// complex<double> أيضا
auto one_again = fz * idz;

// اختبار العامل للتحقق من أن كل شيء سيُصرّف بلا مشاكل

complex<float> a{1.0f, -2.0f};
complex<double> b{3.0, -4.0};

// complex<double> كل هذه من النوع
auto c0 = a + b;
auto c1 = a - b;
auto c2 = a * b;
auto c3 = a / b;

// complex<float> كل هذه من النوع
auto d0 = a + 1;
auto d1 = 1 + a;
auto d2 = a - 1;
auto d3 = 1 - a;
auto d4 = a * 1;
auto d5 = 1 * a;
auto d6 = a / 1;
auto d7 = 1 / a;

// complex<double>  كل هذه من النوعauto e0 = b + 1;
auto e1 = 1 + b;
auto e2 = b - 1;
auto e3 = 1 - b;
auto e4 = b * 1;
auto e5 = 1 * b;
auto e6 = b / 1;
auto e7 = 1 / b;

return 0;
}

العوامل المسماة (Named operators)

يمكنك توسيع C++‎ بالعوامل المسمّاة المحاطة بعوامل ++C القياسية. سنبدأ أولًا بكتابة شيفرة تشكل مكتبة من بضعة أسطر:

namespace named_operator {
    template < class D > struct make_operator {
        constexpr make_operator() {}
    };
    template < class T, char, class O > struct half_apply {
        T && lhs;
    };
    template < class Lhs, class Op >
        half_apply < Lhs, '*', Op > operator * (Lhs && lhs, make_operator < Op > ) {
            return {
                std::forward < Lhs > (lhs)
            };
        }
    template < class Lhs, class Op, class Rhs >
        auto operator*( half_apply<Lhs, '*', Op>&& lhs, Rhs&& rhs ) -> decltype(named_invoke(std::forward < Lhs > (lhs.lhs), Op {}, std::forward < Rhs > (rhs))) {
            return named_invoke(std::forward < Lhs > (lhs.lhs), Op {}, std::forward < Rhs > (rhs));
        }
}

هذه الشيفرة لا تفعل أي شيء حتى الآن. سنضيف الآن المتجهات:

namespace my_ns {
struct append_t : named_operator::make_operator<append_t> {};
constexpr append_t append{};

template<class T, class A0, class A1>
std::vector<T, A0> named_invoke( std::vector<T, A0> lhs, append_t, std::vector<T, A1> const& rhs
)    {
lhs.insert( lhs.end(), rhs.begin(), rhs.end() );
return std::move(lhs);
}
}
using my_ns::append;

std::vector<int> a {1,2,3};
std::vector<int> b {4,5,6};

auto c = a *append* b;

لقد عرّفنا كائنًا ‎append‎ من النوع ‎append_t:named_operator::make_operator<append_t>‎، ثم زدنا بعد ذلك تحميل append_t:named_operator::make_operator<append_t>‎ للأنواع التي نريدها على اليمين واليسار.

ستزيد المكتبةُ تحميل ‎lhs*append_t‎ لإعادة كائن ‎half_apply‎ مؤقّت، كما أنها ستزيد تحميل ‎half_apply*rhs‎ لاستدعاء ‎named_invoke( lhs, append_t, rhs )‎.

ويجب أن ننشئ مفتاح ‎append_t‎ ليستدعي التوقيع المناسب عبر ‎named_invoke‎ متوافق مع البحث القائم على العامل (ADL-friendly) .

لنفترض الآن أنّك تريد إجراء عملية ضرب عنصرًا بعنصر العناصر المصفوفة:

template<class=void, std::size_t...Is>
auto indexer( std::index_sequence<Is...> ) {
return [](auto&& f) {
return f( std::integral_constant<std::size_t, Is>{}... );
};
}
template<std::size_t N>
auto indexer() { return indexer( std::make_index_sequence<N>{} ); }

namespace my_ns {
struct e_times_t : named_operator::make_operator<e_times_t> {};
constexpr e_times_t e_times{};

template<class L, class R, std::size_t N,
class Out=std::decay_t<decltype( std::declval<L const&>()*std::declval<R const&>() )>
>
std::array<Out, N> named_invoke( std::array<L, N> const& lhs, e_times_t, std::array<R, N> const&
rhs ) {
using result_type = std::array<Out, N>;
auto index_over_N = indexer<N>();
return index_over_N([&](auto...is)->result_type {
return {{
(lhs[is] * rhs[is])...
}};    
});
}
}

هذا مثال حيّ على ذلك.

يمكن توسيع هذه الشيفرة لتعمل على الصفوف (tuples) أو الأزواج أو المصفوفات الشبيهة بـ C، أو حتى الحاويات متغيّرة الطول. كذلك تستطيع استخدام عامل نوعي عنصري (element-wise operator type) والحصول منه على ‎lhs *element_wise<'+'>* rhs‎. أيضًا من الممكن استخدام عامِلا الضرب ‎*dot*‎ و ‎*cross*‎.

يمكن توسيع استخدام ‎*‎ ليدعم مُحدّدات (delimiters) أخرى مثل ‎+‎، وتُحدّد أسبقيةُ المُحدّد أسبقيةَ العامِل المسمَّى، وذلك مفيد عند استخدام معادلات الفيزياء في C++‎ إذ لن تحتاج إلّا إلى الحد الأدنى من أقواس ‎()‎ .

نستطيع دعم عوامل ‎->*then*‎ بتغيير طفيف على المكتبة أعلاه، وكذلك توسيع ‎std::function‎ قبل المعيار الذي يتم تحديثه، أو كتابة ‎->*bind*‎ أُحاديّ، بل يمكننا الحصول على عامل مُسمّى إذ نمرِّر العامل ‎Op‎ إلى دالّة الاستدعاء النهائية، مما يسمح بما يلي:

named_operator < '*' > append = [](auto lhs, auto && rhs) {
    using std::begin;
    using std::end;
    lhs.insert(end(lhs), begin(rhs), end(rhs));
    return std::move(lhs);
};

مما ينتج عنه إنشاء عامل مسمّى ومضيف للحاويات في C++‎ 17.

العوامل الأحادية (Unary Operators)

العاملان الأحاديان التاليان يمكن زيادة تحميلهما:

  • ‎‎++foo و foo++‎‎
  • --foo و foo-- ويكون التحميل مُتماثل بالنسبة لكلا النوعين (‎++‎ و ‎--‎)، ولزيادة التحميل خارج الصنف (‎class‎) أو البنية (‎struct‎):
// ++foo العامل المسبق
T & operator++(T & lhs) {
    // إجراء عملية الجمع
    return lhs;
}

يجب أن يُستخدم العامل المسبق ++foo -يُستخدم وسيط int لفصل المُسبِق pre عن اللاحق postfix- بواسطة العامل المسبق foo++، انظر:

T operator++(T & lhs, int) {
    T t(lhs);
    ++lhs;
    return t;
}

التحميل الزائد داخل الصنف (‎class‎) أو البنية (‎struct‎):

// ++foo العامل المسبق
T & operator++() {
    // إجراء عملية الجمع
    return *this;
}

كما فعلنا قبل قليل، يجب أن يُستخدم ++foo -يُستخدم وسيط int لفصل المُسبِق pre عن اللاحق postfix- بواسطة foo++، انظر:

T operator++(int) {
    T t( * this);
    ++( * this);
    return t;
}

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

يُعيد العامل المُلحق (postfix operator) قيمة مؤقتة -القيمة السابقة-، وعليه لا يمكن أن يكون مرجعًا لأنه سيكون مرجعًا إلى قيمة مؤقتة، والتي ستكون قيمة مُهملة (garbage value) في نهاية الدالّة لأنّ المتغيرات المؤقتة تخرج عن النطاق، كذلك لا يمكن أن تكون ثابتة إذ يُفترض أن تستطيع تعديلها مباشرةً.

واعلم أن الوسيط الأوّل هو مرجع غير ثابت للكائن المُستدعِي، لأنه لو كان ثابتًا فلن تتمكّن من تعديله، وإذا لم يكن مرجعًا، فلن يكون بمقدورك تغيير القيمة الأصلية.

يُفضل استخدام السابقة ++ بدلاً من اللاحقة ++ في [حلقات](رابط الفصل 11) ‎for‎ بسبب عملية النسخ (copying) الضرورية في تحميلات العامل المُلحق. ورغم تكافئ الاثنتين وظيفيًا من منظور [حلقة](رابط الفصل 11) ‎for‎، إلا أنه قد تكون هناك ميزة طفيفة في الأداء عند استخدام السابقة ++، وخاصة في الأصناف "الكبيرة" التي تحتوي الكثير من الأعضاء الواجب نسخها. انظر المثال التالي على استخدام السابقة ++ في حلقة for:

for (list < string > ::const_iterator it = tokens.begin(); it != tokens.end();
    ++it) { // it++ لا تستخدم
    ...
}

عوامل الموازنة (Comparison operators)

يمكنك زيادة تحميل جميع عوامل المقارنة:

  • ‎==‎ و ‎!=‎
  • ‎>‎ و ‎<‎
  • ‎>=‎ و ‎<=‎

الطريقة الموصى بها لزيادة تحميل كل هذه العوامل هي باستخدام العامليْن (‎==‎ و ‎<‎) فقط، ثمّ استخدَامِهما لتعريف الباقي. انظر المثال التالي للتحميل خارج الصنف أو البنية:

// لا تستخدم إلا هذين العامليْن
bool operator == (const T & lhs,
    const T & rhs) {
    /* Compare */ }
bool operator < (const T & lhs,
    const T & rhs) {
    /* Compare */ }
// الآن يمكنك تعريف الباقي
bool operator != (const T & lhs,
    const T & rhs) {
    return !(lhs == rhs);
}
bool operator > (const T & lhs,
    const T & rhs) {
    return rhs < lhs;
}
bool operator <= (const T& lhs,
    const T& rhs) {
    return !(lhs > rhs);
}
bool operator >= (const T& lhs,
    const T& rhs) {
    return !(lhs < rhs);
}

زيادة التحميل داخل الصنف أو البنية، لاحظ أن الدوال ثابتة، ذلك أنه إن لم تكن كذلك فلن تستطيع استدعاءها إن كان الكائن ثابتًا:

// لا تستخدم إلا هذين العاملين
bool operator == (const T& rhs) const {
    /* Compare */ }
bool operator < (const T& rhs) const {
    /* Compare */ }
// الآن يمكنك تعريف الباقي
bool operator != (const T& rhs) const {
    return !( *this == rhs);
}
bool operator > (const T& rhs) const {
    return rhs < *this;
}
bool operator <= (const T& rhs) const {
    return !( *this > rhs);
}
bool operator >= (const T& rhs) const {
    return !( *this < rhs);
}

من الواضح أنّ العوامل ستعيد قيمة منطقية (‎bool‎)، أي إمّا ‎true‎ أو ‎false‎. تأخذ جميع العوامل وسائطها كمراجع ثابتة (‎const&‎) لأنّ الشيء الوحيد الذي تفعله هذه العوامل هو المقارنة، لذلك لا ينبغي لها تعديل الكائنات. لاحظ أن التمرير بالمرجع (‎&‎) أسرع من التمرير بالقيمة (by value)، ويكون مرجعًا ثابتًا const للتأكد من أنّ العوامل لن تعدّله.

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

عامل الإسناد (Assignment Operator)

تكمن أهمية عامل الإسناد في أنه يتيح لك تغيير حالة المتغير، وإذا لم تزد تحميل عامل الإسناد في الصنف أو البنية فسيُنشئه المصرِّف تلقائيًا، ويُجري عامل الإسناد المُنشأ تلقائيًا عملية "الإسناد عضوًا بعضو" (memberwise assignment)، أي عن طريق استدعاء عوامل الإسناد على جميع الأعضاء، بحيث يُنسخ كائن إلى آخر، عضوًا بعضو. يجب زيادة تحميل عامل الإسناد عندما لا تكون عملية الإسناد عضوًا بعضو مناسبة للصنف أو البنية خاصتك، كأن تكون بحاجة إلى تنفيذ نسخ عميق (deep copy) لكائن ما.

زيادة تحميل عامل الإسناد ‎=‎ سهل، لكن عليك اتباع بعض الخطوات البسيطة.

  1. اختبار الإسناد الذاتي (Test for self-assignment). هذا الاختبار مهم لسببين:
  • الإسناد الذاتي عملية نسخ لا داعي لها، لذلك ليس من المنطقي إجراؤها.
  • لن تنجح الخطوة التالية في حال استخدام الإسناد الذاتي.
  1. تنظيف البيانات القديمة. يجب استبدال البيانات الجديدة بالبيانات القديمة. ربما تفهم الآن السبب الثاني في الخطوة السابقة إذ أنه في حال حذف محتوى الكائن فستفشل عملية الإسناد الذاتي في تنفيذ عملية النسخ.
  2. نسخ جميع الأعضاء. إذا زدت تحميل عامل الإسناد في صنفك أو بنيتك فلن يُنشأ تلقائيًا بواسطة المُصرّف، لذلك سيقع على عاتقك مسؤولية نسخ جميع الأعضاء من الكائن الآخر.
  3. إعادة ‎*this‎. يُعيد العامل مرجعًا إليه لأجل السماح بالعمليات المتسلسلة (أي int b = (a = 6)‎ +‎ 4;).
// يمثل نوعا ما T 
T & operator = (const T & other) {
    // افعل شيئا ما
    return *this;
}

ملاحظة: يُمرّر ‎other‎ بمرجع ثابت (‎const&‎) لأنه لا ينبغي تغيير الكائن الذي يتم تعيينه، كما أنّ التمرير بالمرجع أسرع من التمرير بالقيمة، وينبغي جعل ‎operator=‎ ثابتًا const للتأكد أنه لن يعدّلها عن طريق الخطأ.

لا يمكن زيادة تحميل عامل الإسناد إلا في الأصناف والبنيات، لأنّ القيمة اليسرى لـ ‎=‎ تكون دائمًا هي الصنفَ أو البنيةَ نفسها، ولا يضمن تعريف العامل كدالّة حرّة (free function) ذلك، لهذا لا يُسمح به.

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

عامل استدعاء الدالّة (Function call operator)

يمكنك زيادة تحميل عامل استدعاء الدالّة ‎()‎، ويجب أن تحدث زيادة التحميل داخل صنف أو بنية:

//R <- نوع القيمة المعادة
R operator()(Type name, Type2 name2, ...) {
    // افعل شيئا ما
    // إعادة قيمة
}
// استخدمه هكذا
R foo = object(a, b, ...);

مثلا:

struct Sum {
    int operator()(int a, int b) {
        return a + b;
    }
};
// انشاء نسخة من البنية
Sum sum;
int result = sum(1, 1); //  2

عامل NOT الثنائي (Bitwise NOT operator)

زيادة تحميل عامل NOT الثنائي (‎~‎) بسيط إلى حد ما. انظر المثال التالي لزيادة التحميل خارج الصنف أو البنية:

T operator~(T lhs) {
    // نفذ العملية
    return lhs;
}

التحميل الزائد داخل الصنف أو البنية ‎class‎ / ‎struct‎:

T operator~() {
    T t( *this);
    // نفذ العملية
    return t;
}

ملاحظة: يعيد عامل ‎operator~‎ بالقيمة (by value) لأنّ عليه أن يُعيد قيمة جديدة (القيمة المُعدّلة) وليس مرجعًا إلى القيمة -سيكون مرجعًا إلى الكائن المؤقّت الذي ستصبح قيمته مُهملة [محذوفة] بمجرد تنفيذ العامل-، كما أنها لا ينبغي أن تكون ثابتة لأنّ شيفرة الاستدعاء يجب أن تكون قادرة على تعديله بعد ذلك (أي يجب أن تكون العبارة ‎int a = ~a+ 1;‎‎ ممكنة).

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

عوامل الإزاحة البِتيّة للدخل/الخرج (Bit shift operators for I/O)

يشيع استخدام العامليْن ‎<<‎ و ‎>>‎ كعوامل للكتابة والقراءة، على الترتيب.

  • std::ostream تزيد تحميل ‎<<‎ لكتابة المتغيرات في [المجرى](رابط الفصل 13) الأساسي (على سبيل المثال: std::cout)
  • std::istream تزيد تحميل ‎>>‎ للقراءة من [المجرى](رابط الفصل 13) الأساسي إلى متغير (على سبيل المثال: std::cin)

ويتماثل أسلوبهما في حال أردت زيادة تحميلهما "بشكل طبيعي" خارج الصنف أو البنية، باستثناء أنّ الوسائط ليست من نفس النوع:

  • نوع القيمة المُعادة هو [المجرى](رابط الفصل 13) الذي تريد زيادة التحميل منه - overload from - (على سبيل المثال، ‎std::ostream‎)، والذي يُمرّر بالمرجع (by reference) للسماح بالعمليات المتسلسلة (التسلسل: ‎std::cout << a << b;‎). مثال: ‎std::ostream&‎
  • ‎lhs‎ سيكون من نفس نوع القيمة المعادة.
  • ‎rhs‎ تمثّل النوع الذي تريد السماح بالتحميل منه (على سبيل المثال ‎T‎)، والذي يُمرَّر بمرجع ثابت بدلاً من تمريره بالقيمة لأسباب تتعلّق بالأداء (لا ينبغي تغيير ‎rhs‎ على أيّ حال). مثال: ‎const Vector&‎. انظر المثال التالي حيث نزيد تحميل >>std::ostream operator للسماح بالخرج من المتجه:
std::ostream& operator<<(std::ostream& lhs, const Vector& rhs) {
    lhs << "x: " << rhs.x << " y: " << rhs.y << " z: " << rhs.z << '\n';
    return lhs;
}
Vector v = {
    1,
    2,
    3
};
// الآن يمكنك فعل ما يلي
std::cout << v;

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

ترجمة -بتصرّف- للفصل Chapter 36: Operator Overloading من كتاب C++ Notes for Professionals





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


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



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

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

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


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

تسجيل الدخول

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


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