بازگشت مقادیر چندگانه از تابع های ++C — راهنمای کاربردی

۸۴۹ بازدید
آخرین به‌روزرسانی: ۲۷ شهریور ۱۴۰۲
زمان مطالعه: ۶ دقیقه
بازگشت مقادیر چندگانه از تابع های ++C — راهنمای کاربردی

شاید تاکنون برایتان پیش آمده باشد که بخواهید مقادیر چندگانه‌ای را از یک تابع یا تابع های ++C بازگشت دهید. بهترین روش برای این کار کدام است؟ در ادامه چند روش برای این کار ارائه کرده‌ایم:

997696
  1. استفاده از پارامترهای خروجی
auto output_1(int &i1) { i1 = 11; return 12; }

2. استفاده از ساختار محلی

auto struct_2() { struct _ { int i1, i2; }; return _{21, 22}; }

3. استفاده از یک std::pair

auto pair_2() { return std::make_pair(31, 32); }

4. استفاده از یک std::tuple

auto tuple_2() { return std::make_tuple(41, 42); }

در این نوشته تلاش می‌کنیم بررسی کنیم که کدام یک از روش‌های فوق برای بازگشت دادن مقادیر چندگانه‌ در تابع‌های ++C بهتر است.

چرا باید بخواهیم مقادیر چندگانه‌ای را بازگشت دهیم؟

یک مثال معمول برای این حالت ()std::from_chars است که یک تابع در ++C نسخه 17 است و شباهت زیادی به ()strtol دارد؛ اما ()from_chars تابعی است که 3 مقدار بازگشت می‌دهد. یک عدد تجزیه‌شده، یک کد خطا و یک اشاره‌گر به کاراکتر نخست نامعتبر.

این تابع از ترکیبی از تکنیک‌ها استفاده می‌کند. عدد به صورت یک پارامتر خروجی بازگشت می‌یابد؛ اما کد خطا و اشاره‌گر به صورت یک ساختار بازگشت می‌یابند. دلایل این کار را در ادامه بررسی می‌کنیم.

بازگشت دادن مقادیر چندگانه با استفاده از پارامترهای خروجی

کد نمونه:

1auto output_1(int &i1) {
2  i1 = 11;   // Output first parameter
3  return 12; // Return second value
4}
5
6// Use volatile pointers so compiler could not inline the function
7auto (*volatile output_1_ptr)(int &i1) = output_1;
8
9int main() {
10  int o1, o2;            // Define local variables
11  o2 = output_1_ptr(o1); // Output 1st param and assign the 2nd
12  printf("output_1 o1 = %d, o2 = %d\n", o1, o2);
13}

این کد به صورت زیر کامپایل می‌شود:

1output_1(int&):
2  mov [rdi], 11       # Output first param to the address in rdi
3  mov eax, 12         # Return second value in eax
4  ret
5
6main:                 # Note: simplified
7  lea rdi, [rsp + 4]  # Load address of the 1st param (on stack)
8  call [output_1_ptr] # Call output_1 using a pointer
9
10  mov esi, [rsp + 4]  # Load 1st param from the stack
11  mov ecx, eax        # Load 2nd param from eax
12  call printf

این روش مزایا و معایبی دارد:

مزایا

  • این یک روش کلاسیک و درک آن آسان است.
  • با استاندارد ++C کار می‌کند که شامل استاندارد C یعنی استفاده از اشاره‌گرها است.
  • از overloading تابع پشتیبانی می‌کند.

معایب

  • آدرس پارامتر اول باید پیش از فراخوانی تابع بارگذاری شده باشد.
  • پارامتر نخست با استفاده از پشته ارسال می‌شود و از این رو کند است.
  • به دلیل وجود System V AMD64 ABI، می‌توانیم ثبات‌هایی تا 6 آدرس را ارسال کنیم. برای ارسال بیش از 6 پارامتر باید از پشته استفاده شود که باز هم آن را کندتر می‌سازد.

برای نمایش عملی آخرین عیب در فهرست فوق کد نمونه‌ای با 7 پارامتر خروجی ارائه کرده‌ایم:

1// Output more than 6 params
2int output_7(int &i1, int &i2, int &i3, int &i4,
3             int &i5, int &i6, int &i7) {
4  i1 = 11;
5  i2 = 12;
6  i3 = 13;
7  i4 = 14;
8  i5 = 15;
9  i6 = 16;
10  i7 = 17;
11  return 18;
12}

و دی‌اسمبل کد ()output_7 به صورت زیر است:

1output_7(int&, int&, int&, int&, int&, int&, int&):
2  mov [rdi], 11      #
3  mov [rsi], 12      # Addresses of the first 6 params get passed
4  mov [rdx], 13      # via rdi, rsi, rdx, rcx, r8, and r9
5  mov [rcx], 14      # according to System V AMD64 ABI
6  mov [r8], 15       # (for Linux, macOS, FreeBSD etc)
7  mov [r9], 16       #
8  mov rax, [rsp + 8] # But address for the 7th is on the stack,
9  mov [rax], 17      # which is slow
10  mov eax, 18
11  ret

آدرس هفتم از طریق پشته ارسال می‌شود و از این رو آدرس را روی پشته قرار می‌دهیم که در این صورت باید آن را دوباره از پشته بخوانیم و سپس مقدار را به آن آدرس خروجی بدهیم. می‌بینید که عملیات حافظه زیادی مورد نیاز است.

بازگشت مقادیر چندگانه با استفاده از ساختار محلی

کد نمونه:

1auto struct_2() {
2  struct _ {        // Declare a local structure with 2 integers
3    int i1, i2;
4  };
5  return _{21, 22}; // Return the local structure
6}
7
8// Use volatile pointers so compiler could not inline the function
9auto (*volatile struct_2_ptr)() = struct_2;
10
11int main() {
12  auto [s1, s2] = struct_2_ptr(); // Structured binding declaration
13  printf("struct_2 s1 = %d, s2 = %d\n", s1, s2);
14}

Disassembly:

1struct_2():
2  movabs rax, 0x1600000015  # Just return 2 integers in rax
3  ret
4
5main:                 # Note: simplified
6  call [struct_2_ptr] # No need to load output param addresses
7  mov rdx, rax        # Just use the values returned in rax
8  shr rdx, 32         # High 32 bits of rax
9  mov rcx, rax
10  mov esi, ecx        # Low 32 bits of rax
11  call printf

این روش نیز مزایا و معایبی دارد که در ادامه بررسی کرده‌ایم:

مزایا

  • با هر استاندارد ++C که شامل C نیز می‌شود کار می‌کند؛ با این حال ساختار باید بیرون از دامنه تابع اعلان شود.
  • در ثبات‌ها تا 128 بیت را بازگشت می‌دهد و دیگر نیازی به پشته نیست. بنابراین سریع است.
  • به آدرس پارامترها نیاز ندارد و بدین ترتیب کامپایلر بهتر می‌تواند کد را بهینه‌سازی کند.

معایب

  • نیازمند اعلان binding ساخت‌یافته در نسخه 17 ++C است.
  • این تابع نمی‌تواند overload شود، چون نوع بازگشتی بخشی از شناسه تابع است.

وقتی تلاش می‌کنیم مقادیر بیشتری را بازگشت دهیم چه اتفاقی رخ می‌دهد؟ بر اساس System V AMD64 ABI مقادیر تا 128 بیت در RAX و RDX ذخیره می‌شوند. بنابراین تا چهار عدد صحیح 32 بیتی را می‌توان در این ثبات‌ها بازگشت داد. با این حال اگر به یک بایت بیشتر نیاز داشته باشیم باید از پشته استفاده کنیم.

در این مورد نیز همچنان نیاز نداریم آدرس‌های پارامترهای خروجی از قبل بارگذاری شده باشند و از این ‌رو سریع‌تر از روش پارامترهای خروجی است.

بازگشت مقادیر چندگانه با استفاده از std::pair

کد نمونه:

1auto pair_2() { return std::make_pair(31, 32); } // Just one line!
2
3// Use volatile pointers so compiler could not inline the function
4auto (*volatile pair_2_ptr)() = pair_2;
5
6int main() {
7  auto [p1, p2] = pair_2_ptr();  // Structured binding declaration
8  printf("pair_2 p1 = %d, p2 = %d\n", p1, p2);
9}

کد ایجاد شده اسمبلی به صورت زیر است:

1pair_2():
2  movabs rax, 0x200000001f  # Just return 2 integers in rax
3  ret
4
5main:                 # Note: simplified
6  call [pair_2_ptr]   # Just call the function
7  mov rdx, rax        # Use the values returned in rax
8  shr rdx, 32
9  mov rcx, rax
10  mov esi, ecx
11  call printf

مزایا

  • تنها به یک خط کد نیاز دارد.
  • نیازی به اعلان ساختار محلی وجود ندارد.
  • همانند ساختارها تا 128 بیت را در ثبات‌ها بازگشت می‌دهد و به پشته نیازی نیست.

معایب

  • Pair به این معنی است که تنها دو مقدار بازگشت می‌یابند.
  • همانند ساختار، تابع نمی‌تواند overload شود.

بازگشت مقادیر چندگانه با استفاده از std::tuple

کد نمونه:

1auto tuple_2() { return std::make_tuple(41, 42); } // Just one line!
2
3// Use volatile pointers so compiler could not inline the function
4auto (*volatile tuple_2_ptr)() = tuple_2;
5
6int main() {
7  auto [t1, t2] = tuple_2_ptr();  // Structured binding declaration
8  printf("tuple_2 t1 = %d, t2 = %d\n", t1, t2);
9}

این کد به صورت زیر کامپایل می‌شود:

1tuple_2():
2  movabs rax, 0x290000002a. # Good start, but...
3  mov [rdi], rax            # Indirect write to a output parameter?
4  mov rax, rdi              # Return the address of the parameter
5  ret
6
7main:                 # Note: simplified
8  mov rdi, rsp        # Pass stack pointer as a parameter
9  call [tuple_2_ptr]  # Call the function
10  mov edx, [rsp]      # Get the values from the stack
11  mov esi, [rsp + 4]
12  call printf

مزایا

  • کد منبع همانند روش std::pair یک‌خطی است.
  • برخلاف std::pair به راحتی می‌توان مقادیر بیشتری اضافه کرد.

معایب

متأسفانه disassembly یک کیسه مخلوط است. ما باید یک آدرس از چندتایی خروجی را به تابع ارسال کنیم، یعنی برای هر عنصر چندتایی به یک آدرس نیاز داریم. حتی در مورد دو عدد صحیح (64 بیت)، مقادیر بازگشتی همواره روی پشته هستند و از این رو کند است.

چه می‌شود اگر مقادیر بیشتری را به tuple اضافه کنیم؟

افزودن مقادیر بیشتر تغییر زیادی در disassembly ایجاد نمی‌کند، چون ما همچنان تنها یک آدرس برای اشاره به پشته بازگشت می‌دهیم و سپس مقادیر را زیر آن آدرس قرار می‌دهیم و در نهایت آن‌ها را مجدداً با استفاده از printf() از پشته بارگذاری می‌کنیم.

این ساختار کندتر از روش pair و ساختار است چون هر دوی آن‌ها تا 128 بیت را در رجیسترها بازگشت می‌دهند. اما از روش پارامترهای خروجی سریع‌تر است، چون در آن روش باید چندین آدرس را به تابع ارسال کنیم و نه فقط یک آدرس.

سخن پایانی

سریع‌ترین روش برای بازگشت پارامترهای چندگانه در نسخه C++ 17، استفاده از ساختار محلی و std::pair است. std::pair در مواردی که باید دو مقدار بازگشت یابند به عنوان سریع‌ترین و ساده‌ترین روش بیشترین ترجیح را دارد. استفاده از پارامترهای خروجی در مواردی که تابع overload می‌شود مورد نیاز است. به همین دلیل است که ()std::from_chars از پارامترهای خروجی استفاده می‌کند و یک ساختار بازگشت می‌دهد.

به طور خلاصه باید گفت که std::pair راحت‌ترین و سریع‌ترین روش برای بازگشت دو مقدار است. اگر بخواهیم بیش از دو مقدار را بازگشت دهیم، باید از ساختار محلی (سریع‌تر) یا از std::tuple (راحت‌تر) استفاده کنیم.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
applied
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *