سلسلة ++c للمحترفين الدرس 15: استدعاء الدوال وإعادة عدة قيم منها في Cpp


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

استدعاء الدوال بالقيمة أو بالمرجع

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

الاستدعاء بالقيمة (Call by value)

عند استدعاء دالة، تُنشأ عناصر جديدة في مُكدِّس (stack) البرنامج، ويشمل ذلك بعض المعلومات عن الدالّة وكذلك المساحة (مواقع الذاكرة) المخصّصة للمعامِلات، والقيمة المُعادة.

وعند تمرير معامِل إلى دالّة، تُنسخ قيمة المتغير المُستخدم (أو العنصر الحرفي) إلى الموقع المخصّص في الذاكرة لمُعامل الدالة، هذا يعني أنه سيُوجد حينها موقعان في الذاكرة لهما نفس القيمة، وسنعمل دَاخل الدالة على الموقع المخصّص للمعامل في الذاكرة فقط.

تُحذَف الذاكرة الموجودة في مُكدّس البرنامج بعد الخروج من الدالّة ممّا يؤدّي إلى مسح جميع بيانات استدعاء الدالّة بما في ذلك المواقع في الذاكرة المخصّصة للمعاملات التي استخدمناها داخلَ الدالّة، ومن ثم فإنّ القيم التي غُيِّرت داخل الدالّة لن تؤثر على قيم المتغيرات الخارجية.

int func(int f, int b) {

تُنشأ متغيرات جديدة، وتحمل القيم المنسوخة من خارج f القيمة 0، وقيمة inner_b تساوي 1. نتابع:

    f = 1;
    // تساوي 1 f قيمة
    b = 2;
    // تساوي 2 inner_b قيمة
    return f + b;
}

int main(void) {
    int a = 0;
    int b = 1; //outer_b
    int c;

    c = func(a, b);
    // c تُنسخ القيمة المعادة إلى

    // تساوي الصفر a 
    // تساوي 1 outer_b قيمة
    // هما متغيّران مختلفان outer_b و inner_b 
    // تساوي 3 c قيمة
}

في المثال السابق نشئ متغيرات داخل الدالّة الرئيسية، وعند استدعاء الدوالّ يُنشأ متغيران جديدان: ‎f‎ و ‎inner_b‎، حيث يتشارك ‎b‎ الاسم مع المتغيّر الخارجي ولكن لا يتشارك معه في موقع الذاكرة، كما أنّ سلوك ‎a<->f‎ و ‎b<->b‎ متماثلان هنا.

يوضح الرسم التالي ما يحدث في المُكدّس ولماذا لا يحدث أيّ تغيير على المتغيّر ‎b‎. هذا الرسم ليس دقيقًا تمامًا، ولكنّه يوضّح المثال.

call.png

يُطلق على هذا "استدعاءً بالقيمة" لأننا لا نمرّر المتغيرات نفسها، وإنما نمرّر قيمها فقط.

وإعادة عدة قيم من دالة

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

استخدام الصّفوف std::tuple

الإصدار ≥ C++‎ 11 تستطيع الصفوف (‎std::tuple‎) أن تجمّع أيّ عدد من القيم حتى لو كانت من أنواع مختلفة:

std::tuple < int, int, int, int > foo(int a, int b) { // or auto (C++14)
    return std::make_tuple(a + b, a - b, a * b, a / b);
}

في C++‎ 17، يمكن استخدام قائمة مهيِّئة ذات أقواس معقوصة (braced initializer list):

std::tuple<int, int, int, int> foo(int a, int b) {
return {a + b, a - b, a * b, a / b};
}

قد تكون استعادة القيم المعادة من ‎tuple‎ مرهقة إذ تتطلّب استخدام دالة القالب ‎std::get‎:

auto mrvs = foo(5, 12);
auto add = std::get<0>(mrvs);
auto sub = std::get<1>(mrvs);
auto mul = std::get<2>(mrvs);
auto div = std::get<3>(mrvs);

إذا أمكن التصريح عن الأنواع قبل عودة الدالّة، فيمكن استخدام ‎std::tie‎ لتفريغ الصف ‎tuple‎ في متغيّرات أخرى:

int add, sub, mul, div;
std::tie(add, sub, mul, div) = foo(5, 12);

يمكن استخدام ‎std::ignore‎ إذا انتفت الحاجة إلى قيمة من القيم المعادة:

std::tie(add, sub, std::ignore, div) = foo(5, 12);

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

يمكن استخدام الارتباطات البنيوية (Structured bindings) لتجنّب استخدام ‎std::tie‎:

auto [add, sub, mul, div] = foo(5,12);

إذا أردت إعادة صفٍّ مؤلَّف من مراجعِ القيم اليسارية (lvalue references) بدلًا من صفّ مؤلّف من القيم، فاستخدم ‎std::tie‎ بدلاً من std::make_tuple.

std::tuple < int & , int & > minmax(int & a, int & b) {
    if (b < a)
        return std::tie(b, a);
    else
        return std::tie(a, b);
}

والذي يسمح بما يلي:

void increase_least(int& a, int& b) {
    std::get < 0 > (minmax(a, b)) ++;
}

في بعض الحالات النادرة، قد تُستخدم ‎std::forward_as_tuple‎ بدلاً من ‎std::tie‎ لكن احذر في تلك الحالات إذ قد لا تدوم الكائنات المؤقّتة بما يكفي لاستهلاكها.

الارتباطات البنيوية (Structured Bindings)

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

قدّم الإصدار C++‎ 17 مفهوم الارتباطات البنيويّة التي سهّلت على المبرمجين التعامل مع عدة أنواع للإعادة، إذ أنّك لن تكون مضطرًّا إلى الاعتماد على ‎std::tie()‎ أو تفريغ الصفوف يدويًِّا:

std::map < std::string, int > m;
// أدرج عنصرا في القاموس وتحقق من نجاح الإدراج
auto[iterator, success] = m.insert({
    "Hello",
    42
});
if (success) {
    // شيفرتك هنا
}
// 'second' و 'first' كرّر على كل العناصر دون الحاجة إلى استخدام الاسمين المبهميْن
for (auto
    const& [key, value]: m) {
    std::cout << "The value for " << key << " is " << value << '\n';
}

يمكن استخدام الارتباطات البنيويّة افتراضيًّا مع الأزواج (‎std::pair‎) والصفوف (‎std::tuple‎) وكذلك أيّ نوع تكون بياناته العضويّة غير الساكنة (non-static data members) إما أعضاءً مباشرين علنيين أو أعضاءً من صنف أساسي محدد:

struct A {
    int x;
};
struct B: A {
    int y;
};
B foo();
// استخدام الارتباطات البنيويّة
const auto[x, y] = foo();
// الشيفرة المكافئة بدون استخدام الارتباطات البنيويّة
const auto result = foo();
auto& x = result.x;
auto& y = result.y;

إذا جعلت نوعًا ما "شبيهًا بالصفوف (tuple-like)" فسيعمل تلقائيًا مع ذلك النوع. النوع الشبيه بالصّفوف يتوفّر على التوابع التالية: ‎tuple_size‎ و ‎tuple_element‎ و ‎get‎:

namespace my_ns {
    struct my_type {
        int x;
        double d;
        std::string s;
    };
    struct my_type_view {
        my_type* ptr;
    };
}
namespace std {
template<>
struct tuple_size<my_ns::my_type_view> : std::integral_constant<std::size_t, 3>
{};

template<> struct tuple_element<my_ns::my_type_view, 0>{ using type = int; };
template<> struct tuple_element<my_ns::my_type_view, 1>{ using type = double; };
template<> struct tuple_element<my_ns::my_type_view, 2>{ using type = std::string; };
}

namespace my_ns {
template<std::size_t I>
decltype(auto) get(my_type_view const& v) {
if constexpr (I == 0)
return v.ptr->x;
else if constexpr (I == 1)
return v.ptr->d;
else if constexpr (I == 2)
return v.ptr->s;
static_assert(I < 3, "Only 3 elements");
}
}

ستعمل الآن الشيفرة التالية:

my_ns::my_type t{1, 3.14, "hello world"};

my_ns::my_type_view foo() {
return {&t};
}

int main() {
auto[x, d, s] = foo();
std::cout << x << ',' << d << ',' << s << '\n';
}

استخدام البُنى struct

يمكن استخدام البُنى (‎struct‎) لتجميع عدّة قيم لكي تعيدها دالة:

الإصدار C++‎ 11

struct foo_return_type {
int add;
int sub;
int mul;
int div;
};

foo_return_type foo(int a, int b) {
return {a + b, a - b, a * b, a / b};
}

auto calc = foo(5, 12);

الإصدار ++‎>

يمكن استخدام مُنشئ لتبسيط عمليّة إنشاء القيم المُعادة، بدلاً من الإسناد إلى حقول الصنف فرادى:

struct foo_return_type {
    int add;
    int sub;
    int mul;
    int div;
    foo_return_type(int add, int sub, int mul, int div): add(add), sub(sub), mul(mul), div(div) {}
};

foo_return_type foo(int a, int b) {
    return foo_return_type(a + b, a - b, a * b, a / b);
}

foo_return_type calc = foo(5, 12);

يمكن استرداد النتائج الفردية المُعادة من قِبل دالة ‎foo()‎ عن طريق الوصول إلى المتغيرات الأعضاء لبُنية calc:

std::cout << calc.add << ' ' << calc.sub << ' ' << calc.mul << ' ' << calc.div << '\n';

الناتج:

 17 -7 60 0

ملاحظة: ، تُجمَّع القيم المُعادة معًا عند استخدام البُنى في كائن واحد، ويمكن الوصول إليها باستخدام أسماء ذات معنى (meaningful)، وهذا يساعد على تقليل عدد المتغيرات الخارجية التي أنشئت في نطاق القيم المُعادة.

الإصدار C++‎ 17 ويمكن استخدام الارتباطات البنيويّة لتفريغ بُنية struct المُعادة من دالة، وهذا يجعل معامِلات الخرج على قدم واحدة مع معامِلات الدخل، انظر:

int a=5, b=12;
auto[add, sub, mul, div] = foo(a, b);
std::cout << add << ' ' << sub << ' ' << mul << ' ' << div << '\n';

يطابق خرْجُ هذه الشيفرة خرجَ الشيفرة أعلاه، فما زالت struct تُستخدم لإعادة القيم من الدالة مما يسمح لك بالتعامل مع الحقول بشكل فردي.

استخدام معامِلات الخرج

يمكن استخدام المعاملات لإعادة قيمة واحدة أو أكثر بشرط أن تكون تلك المعاملاتُ مؤشّراتٍ أو مراجعَ غير ثابتة.

المراجع:

void calculate(int a, int b, int & c, int & d, int & e, int & f) {
    c = a + b;
    d = a - b;
    e = a * b;
    f = a / b;
}

المؤشرات:

void calculate(int a, int b, int * c, int * d, int * e, int * f) {
    *c = a + b;
    *d = a - b;
    *e = a * b;
    *f = a / b;
}

تَستخدم بعض المكتبات والأُطُر التعليمة البرمجية ‎#define OUT لتحديد المعاملاتِ التي ستكون معاملاتِ الخرج في بصمة الدالة، رغم انعدام التأثير الوظيفي لها وأنها ستُهمَل عند التصريف، لكنها ستجعل بصمة الدالة أكثر وضوحًا، انظر:

#define OUT
void calculate(int a, int b, OUT int& c) {
    c = a + b;
}

استخدام مستهلِك دالة (Function Object Consumer)

نستطيع توفير مستهلك يُستدعى مع القيم المتعددة ذات الصّلة:

الإصدار ++ C++‎ 11

template < class F >
    void foo(int a, int b, F consumer) {
        consumer(a + b, a - b, a * b, a / b);
    }
// الاستخدام سهل، كما أنه يمكن تجاهل بعض النتائج
foo(5, 12, [](int sum, int, int, int) {
    std::cout << "sum is " << sum << '\n';
});

يُعرَف هذا باسم "نمط التمرير المستمر" (continuation passing style)، يمكنك تكييف دالة تُعيد صفًّا إلى دالة ذات نمط تمرير مستمر عبر ما يلي:

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

template<class Tuple>
struct continuation {
Tuple t;
template<class F>
decltype(auto) operator->*(F&& f)&&{
return std::apply( std::forward<F>(f), std::move(t) );
}
};
std::tuple<int,int,int,int> foo(int a, int b);

continuation(foo(5,12))->*[](int sum, auto&&...) {
std::cout << "sum is " << sum << '\n';
};

ستجد صيغًا أكثر تعقيدًا في C++‎ 14 أو C++‎ 11.

استخدام قالب std::pair

يستطيع قالب البُنية std::pair أن يجمع قيمتيْ إعادة معًا حتى لو كانا من نوعين مختلفين:

#include <utility>
std::pair < int, int > foo(int a, int b) {
    return std::make_pair(a + b, a - b);
}

في C++‎ 11 والإصدارات الأحدث، يمكن استخدام قائمة مُهيِّئة بدلاً من ‎std::make_pair‎:

الإصدار C++‎ 11

#include <utility>
std::pair<int, int> foo(int a, int b) {
return {a+b, a-b};
}

يمكن جلب القيم الفردية للزوج المعاد باستخدام العضوين ‎first‎ و ‎second‎:

std::pair<int, int> mrvs = foo(5, 12);
std::cout << mrvs.first + mrvs.second << std::endl;

سيكون الناتج:

10 

استخدام المصفوفات std::array

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

يُمكن لِمَصفوفة ما (‎std::array‎) أن تُجمّع معًا عددًا ثابتًا من القيم لغرض إعادتها، ويجب أن يكون عدد العناصر معروفًا عند التصريف، كذلك يجب أن تكون جميع القيم المعادة من نفس النوع:

std::array<int, 4> bar(int a, int b) {
return { a + b, a - b, a * b, a / b };
}

تستبدل هذه الطريقةُ نمطَ صياغة المصفوفات في لغة ‏C‏‏ (‎int bar[4]‎)، فميزتها أنّه يمكن الآن استخدام دوال عديدة من مكتبة القوالب القياسية std الخاصة بلغة ‎c++‎، كما أنها توفر عدّة دوال تابعة مفيدة مثل ‎at‎ وهو تابع وصول آمن يتحقَّق من الحدود، وتابع ‎size‎ الذي يعيد حجم المصفوفة.

استخدام مُكرّرات الخرج

يمكن إعادة عدة قيم من نفس النوع بتمرير مُكرّر خرْج إلى الدالة، ويشيع هذا في الدوال العامة مثل خوارزميات المكتبة القياسية. انظر المثال التالي:

template < typename Incrementable, typename OutputIterator >
void generate_sequence(Incrementable from, Incrementable to, OutputIterator output) {
        for (Incrementable k = from; k != to; ++k)
            *output++ = k;
}

مثال تطبيقي:

std::vector<int> digits;
generate_sequence(0, 10, std::back_inserter(digits));
// {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}

استخدام المتجهات ‎std::vector‎

تساعد المتجهات (‎std::vector‎) في إعادة عدد غير ثابت من المتغيّرات من نفس النوع. انظر المثال التالي حيث نستخدم ‎int‎ كنوع بيانات، رغم أن المتجهات يمكنها احتواء أي نوع قابل للنسخ، ستعيد الدالة كل الأعداد الصحيحة بين a و b في متجه ما، وسيكون الحد الأقصى من العناصر التي يمكن للدالة أن تعيدها هو std::vector::max_size على فرض أن ذاكرة النظام تستطيع احتواء ذلك الحجم.

#include <vector>
#include <iostream>
std::vector < int > fillVectorFrom(int a, int b) {
    std::vector < int > temp;
    for (int i = a; i <= b; i++) {
        temp.push_back(i);
    }
    return temp;
}

ستعيِّن الشيفرة التالية المتجه الذي أنشئ داخل الدالة والمملوء إلى المتجه v الجديد، انظر:

int main() {
    std::vector < int > v = fillVectorFrom(1, 10);
    // "1 2 3 4 5 6 7 8 9 10 "
    for (int i = 0; i < v.size(); i++) {
        std::cout << v[i] << " ";
    }
    std::cout << std::endl;
    return 0;
}

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

ترجمة -بتصرّف- للفصل Chapter 24: Returning several values from والفصل Chapter 28: C++ function "call by value" vs. "call by reference"‎ من كتاب C++ Notes for Professionals





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


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



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

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

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


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

تسجيل الدخول

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


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