C++17 با معرفی دو تابع کمکی به نام‌های std::invoke و std::apply، گام بزرگی در جهت استانداردسازی و ساده‌سازی نحوه‌ی فراخوانی اشیاء قابل‌فراخوانی (Callable) برداشت. این دو تابع که هر دو در کتابخانه‌ی استاندارد تعریف شده‌اند، اگرچه شباهت‌های زیادی دارند، اما هرکدام برای سناریوی خاصی طراحی شده‌اند و درک تفاوت آنها برای نوشتن کدهای عمومی و انعطاف‌پذیر بسیار حیاتی است.

std::invoke: فراخوانی یکپارچه‌ی هر شیء قابل‌فراخوانی

std::invoke یک تابع همه‌منظوره است که هر شیء قابل‌فراخوانی (مانند توابع آزاد، اشاره‌گرهای به توابع عضوی، lambdaها و شیءهای دارای عملگر فراخوانی) را با مجموعه‌ای از آرگومان‌های مشخص، فراخوانی می‌کند . این تابع، عملیات INVOKE را که پیش‌تر در بسیاری از بخش‌های کتابخانه‌ی استاندارد مانند std::function و std::bind استفاده می‌شد، به‌صورت صریح در اختیار برنامه‌نویس قرار می‌دهد . مزیت اصلی std::invoke، یکسان‌سازی نحوه‌ی فراخوانی انواع مختلف اشیاء قابل‌فراخوانی است؛ به‌گونه‌ای که دیگر نیازی به تشخیص نوع شیء و استفاده از روش‌های متفاوت برای فراخوانی آن نیست.

std::apply: فراخوانی با آرگومان‌های بسته‌بندی‌شده

در مقابل، std::apply برای موقعیتی طراحی شده که آرگومان‌های مورد نیاز برای فراخوانی، از پیش در یک محفظه‌ی شبه‌تاپلی (tuple-like) مانند std::tuple، std::pair یا std::array بسته‌بندی شده‌اند . این تابع، عناصر موجود در آن محفظه را از بسته‌بندی خارج (unpack) کرده و به‌عنوان آرگومان‌های مجزا به شیء قابل‌فراخوانی ارسال می‌کند . به عبارت دیگر، std::apply پل ارتباطی بین دنیای محفظه‌های داده‌ی ثابت (مانند تاپل‌ها) و دنیای توابعی است که آرگومان‌های جداگانه دریافت می‌کنند .

تفاوت بنیادین: نحوه‌ی دریافت آرگومان‌ها

تفاوت اصلی و اساسی بین این دو تابع، در نحوه‌ی ارائه‌ی آرگومان‌ها به شیء قابل‌فراخوانی است. std::invoke آرگومان‌ها را به‌صورت مجزا و مستقیم دریافت می‌کند، درست مانند فراخوانی معمولی یک تابع. اما std::apply آرگومان‌ها را به‌صورت یک محفظه‌ی واحد دریافت کرده و آنها را برای فراخوانی، از هم باز می‌کند . این تفاوت در عمل بسیار تعیین‌کننده است. به‌عنوان مثال، اگر تابعی داشته باشیم که یک std::tuple را به‌عنوان پارامتر دریافت می‌کند، std::invoke آن را به‌همان شکل به تابع ارسال می‌کند، اما std::apply سعی می‌کند عناصر درون تاپل را به‌عنوان آرگومان‌های جداگانه به تابع منتقل کند که منجر به خطای تطابق نوع می‌شود .

چرا به هر دو نیاز داریم؟

وجود این دو تابع جداگانه، به‌دلیل نیاز به هریک از آنها در سناریوهای متفاوت است . از یک سو، اغلب نیاز داریم تا یک شیء قابل‌فراخوانی را با آرگومان‌هایی که به‌صورت مستقیم در دسترس هستند، فراخوانی کنیم که std::invoke این کار را به‌سادگی انجام می‌دهد. از سوی دیگر، در متاپrogramming و کدنویسی عمومی، بسیار پیش می‌آید که مجموعه‌ای از آرگومان‌ها در قالب یک تاپل ذخیره شده‌اند و می‌خواهیم آنها را به یک تابع ارسال کنیم. std::apply این کار را با حذف پیچیدگی‌های دستی (مانند استفاده از std::index_sequence) انجام می‌دهد . به‌عبارتی، std::apply عملیات غیرضروری بسته‌بندی آرگومان‌ها در تاپل را حذف می‌کند و امکان فراخوانی توابع با آرگومان‌های تاپل‌شده را فراهم می‌آورد.

انعطاف‌پذیری std::apply در پذیرش انواع مختلف

نکته‌ی جالب توجه در مورد std::apply این است که محفظه‌ی ورودی آن لزوماً نباید از نوع std::tuple باشد. این تابع از هر نوعی که از توابع std::get و std::tuple_size پشتیبانی کند، مانند std::pair و std::array، نیز به‌خوبی کار می‌کند . این ویژگی، انعطاف‌پذیری بالایی به برنامه‌نویس می‌دهد و استفاده از std::apply را در سناریوهای مختلف تسهیل می‌کند.

جایگاه این توابع در کدنویسی مدرن

استفاده از این دو تابع، نشان‌دهنده‌ی رویکرد مدرن C++ به سمت نوشتن کدهای عمومی (Generic) و بیانگر است. با std::invoke، نیازی به نوشتن کدهای تکراری برای تشخیص نوع شیء قابل‌فراخوانی نیست و با std::apply، مدیریت تاپل‌ها و بسته‌های آرگومان به‌صورت استاندارد و کارآمد انجام می‌شود. این ابزارها به‌ویژه در کتابخانه‌ها و فریم‌ورک‌هایی که با متاپrogramming سروکار دارند، نقشی کلیدی ایفا می‌کنند. همچنین گفتنی است که پیاده‌سازی std::apply در پس‌زمینه، خود از std::invoke برای انجام فراخوانی نهایی استفاده می‌کند که نشان‌دهنده‌ی ارتباط نزدیک این دو تابع است .

کلیدواژه ها : فراخوانی عمومی در C++-C++17 invoke-C++17 apply-تفاوت invoke و apply-Callable object C++-فراخوانی شیء قابل فراخوانی-unpack tuple C++-std::apply مثال-کاربرد std::invoke-مدیریت آرگومان‌های تابع-فراخوانی با تاپل-آموزش std::apply-آموزش std::invoke-استانداردسازی فراخوانی در C++