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++