رفتن به محتوا

SFINAE، ویژگی‌های نوع و enable_if

اصل «شکست جایگزینی خطا نیست» (SFINAE) را درک کنید، از ویژگی‌های نوع برای درون‌نگری زمان کامپایل استفاده کنید و قالب‌ها را با enable_if محدود کنید.

SFINAE چیست؟

SFINAE مخفف Substitution Failure Is Not An Error (شکست جایگزینی خطا نیست) است. وقتی کامپایلر سعی می‌کند یک قالب را نمونه‌سازی کند، انواع استنتاج‌شده یا مشخص‌شده را در قالب جایگزین می‌کند. اگر این جایگزینی نوع یا عبارت نامعتبری در زمینه بلافصل اعلان قالب تولید کند، کامپایلر خطا صادر نمی‌کند — بلکه به‌سادگی آن قالب را از مجموعه بارگذاری مجدد حذف می‌کند و به امتحان گزینه‌های دیگر می‌پردازد.

این مکانیزم پایه متابرنامه‌نویسی قالب در ‎C++‎11/14 است. به شما اجازه می‌دهد قالب‌هایی بنویسید که به‌صورت شرطی بر اساس ویژگی‌های آرگومان‌های نوعشان در دسترس باشند.

عبارت کلیدی «زمینه بلافصل» است. شکست‌های جایگزینی در عمق بدنه تابع خطاهای سخت هستند، نه SFINAE. فقط شکست‌ها در امضای تابع (نوع بازگشتی، انواع پارامتر، لیست پارامتر قالب، یا عبارت requires) باعث SFINAE می‌شوند.

std::enable_if

std::enable_if یک نوع عضو type برابر با T فقط در صورتی تعریف می‌کند که Condition برابر true باشد. اگر Condition برابر false باشد، عضو type وجود ندارد و باعث شکست جایگزینی (SFINAE) می‌شود. نام مستعار std::enable_if_t کوتاه‌شده typename std::enable_if::type است.

enable_if.cpp
#include <iostream>
#include <type_traits>

// Only enabled for integral types
template <typename T>
std::enable_if_t<std::is_integral_v<T>, T>
safe_divide(T a, T b) {
    if (b == 0) return 0;  // safe fallback for integers
    return a / b;
}

// Only enabled for floating-point types
template <typename T>
std::enable_if_t<std::is_floating_point_v<T>, T>
safe_divide(T a, T b) {
    if (b == 0.0) return std::numeric_limits<T>::quiet_NaN();
    return a / b;
}

// enable_if in template parameter (alternative placement)
template <typename T,
         std::enable_if_t<std::is_arithmetic_v<T>, int> = 0>
void process(T value) {
    std::cout << "Processing arithmetic value: " << value << '\n';
}

int main() {
    std::cout << safe_divide(10, 3)    << '\n'; // 3 (integer)
    std::cout << safe_divide(10.0, 3.0) << '\n'; // 3.33333 (double)
    std::cout << safe_divide(10, 0)    << '\n'; // 0 (safe fallback)
    std::cout << safe_divide(10.0, 0.0) << '\n'; // nan

    process(42);
    // process(std::string("hello")); // ERROR: no matching function
}

کتابخانه <type_traits>

هدر کتابخانه جامعی از ابزارهای درون‌نگری و تبدیل نوع در زمان کامپایل ارائه می‌دهد. آن‌ها ستون فقرات برنامه‌نویسی مبتنی بر SFINAE را تشکیل می‌دهند.

ویژگی‌های پرس‌وجو (یک bool از طریق ::value یا پسوند _v برمی‌گردانند):
- std::is_integral_v — آیا T یک نوع صحیح است؟
- std::is_floating_point_v — آیا T یک نوع اعشاری است؟
- std::is_pointer_v — آیا T یک اشاره‌گر است؟
- std::is_class_v — آیا T یک کلاس/struct است؟
- std::is_const_v — آیا T دارای واجد شرایط const است؟
- std::is_same_v — آیا T و U نوع یکسانی هستند؟
- std::is_base_of_v — آیا Base پایه Derived است؟
- std::is_convertible_v — آیا From می‌تواند به‌صورت ضمنی به To تبدیل شود؟
- std::is_constructible_v — آیا T می‌تواند از Args ساخته شود؟

ویژگی‌های تبدیل (نوع تغییریافته را از طریق ::type یا پسوند _t برمی‌گردانند):
- std::remove_reference_t — & یا && را حذف می‌کند
- std::remove_const_t — const را حذف می‌کند
- std::decay_t — تبدیل آرایه به اشاره‌گر، تابع به اشاره‌گر تابع و حذف cv/reference را اعمال می‌کند
- std::conditional_t — عملگر سه‌تایی زمان کامپایل: اگر Cond آنگاه T وگرنه F
- std::common_type_t — نوعی که همه Ts‌ها می‌توانند به آن تبدیل شوند

std::void_t و الگوی تشخیص

std::void_t<...> (‎C++‎17) هر دنباله‌ای از انواع را به void نگاشت می‌کند. قدرت آن در SFINAE نهفته است: اگر هر نوعی در لیست نامعتبر باشد، تخصص کنار گذاشته می‌شود. این الگوی تشخیص (detection idiom) را ممکن می‌کند — بررسی اینکه آیا یک نوع عضو، متد یا نوع تودرتوی خاصی دارد.

void_t_detection.cpp
#include <iostream>
#include <type_traits>
#include <string>
#include <vector>

// Primary template: fallback, T does NOT have .size()
template <typename T, typename = void>
struct has_size : std::false_type {};

// Specialization: matches only if T has a .size() method
template <typename T>
struct has_size<T, std::void_t<decltype(std::declval<T>().size())>>
    : std::true_type {};

template <typename T>
constexpr bool has_size_v = has_size<T>::value;

// Use the trait to branch behavior
template <typename T>
void describe(const T& value) {
    if constexpr (has_size_v<T>) {
        std::cout << "Container with " << value.size() << " elements\n";
    } else {
        std::cout << "Not a container: " << value << '\n';
    }
}

int main() {
    static_assert(has_size_v<std::string>);     // true
    static_assert(has_size_v<std::vector<int>>); // true
    static_assert(!has_size_v<int>);             // false

    describe(std::vector<int>{1, 2, 3}); // Container with 3 elements
    describe(42);                        // Not a container: 42
}

decltype با SFINAE

می‌توانید از decltype در نوع بازگشتی دنباله‌دار برای فعال‌سازی SFINAE بر اساس معتبر بودن یک عبارت استفاده کنید. این تکنیک اغلب با عملگر ویرگول ترکیب می‌شود تا نتیجه عبارت کنار گذاشته شده و نوع مطلوب بازگردانده شود.

decltype_sfinae.cpp
#include <iostream>
#include <type_traits>

// Only callable if a + b is valid
template <typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
    return a + b;
}

// Only callable if t.begin() and t.end() are valid
template <typename Container>
auto element_count(const Container& c)
    -> decltype(c.begin(), c.end(), std::size_t{}) {
    std::size_t count = 0;
    for (auto it = c.begin(); it != c.end(); ++it) ++count;
    return count;
}

int main() {
    std::cout << add(1, 2.5)   << '\n'; // 3.5
    std::cout << add(std::string("a"), std::string("b")) << '\n'; // ab

    std::vector<int> v{1, 2, 3, 4};
    std::cout << element_count(v) << '\n'; // 4
    // add(std::string("a"), 5); // SFINAE: no match, a+b invalid
}
دام

وقتی SFINAE تمام بارگذاری‌های مجدد قابل اجرا را حذف کند، کامپایلر دیواری از متن خطا تولید می‌کند که هر کاندیدایی که امتحان کرده و دلیل شکست هر کدام را لیست می‌کند. این پیام‌ها به‌طور معروفی سخت‌خوان هستند — اغلب صدها خط برای یک استفاده نادرست واحد.

این را در نظر بگیرید: شما safe_divide("hello", "world") را فراخوانی می‌کنید. کامپایلر هر دو بارگذاری مجدد را امتحان می‌کند، هر دو در SFINAE شکست می‌خورند و شما پیام خطایی دریافت می‌کنید که enable_if، is_integral، is_floating_point و جزئیات داخلی ویژگی‌های نوع را ذکر می‌کند — هیچ‌کدام به شما کمک نمی‌کنند بفهمید مشکل واقعی «رشته‌ها اعداد نیستند» است.

این یکی از قوی‌ترین انگیزه‌ها برای مفاهیم ‎C++‎20 است که پیام‌های خطای واضح و خوانا تولید می‌کنند مانند: «محدودیت std::integral برای T = const char* برآورده نشد».

بهترین شیوه

اگر کد ‎C++‎20 می‌نویسید، تقریباً در تمام موارد مفاهیم را بر SFINAE ترجیح دهید. مفاهیم خواناتر هستند، به‌طور طبیعی ترکیب می‌شوند و پیام‌های خطای بسیار بهتری تولید می‌کنند. SFINAE را برای موارد زیر نگه دارید:

- کدی که باید با ‎C++‎11/14/17 کامپایل شود
- الگوهای تشخیص پیچیده‌ای که معادل مفهوم استانداردی ندارند
- کد کتابخانه‌ای که باید از چند نسخه استاندارد ++C پشتیبانی کند

حتی در کد ‎C++‎17، وقتی می‌توانید منطق را درون یک تابع واحد بیان کنید، if constexpr با ویژگی‌های نوع را بر enable_if ترجیح دهید.

نکات کلیدی
  • SFINAE: اگر جایگزینی آرگومان‌های قالب نوع نامعتبری در زمینه بلافصل ایجاد کند، قالب بی‌صدا از تشخیص بارگذاری مجدد حذف می‌شود
  • std::enable_if_t ابزار کلاسیک SFINAE است — فقط وقتی Condition صحیح باشد وجود دارد
  • ویژگی‌های پرس‌وجوی زمان کامپایل (is_integral_v) و ویژگی‌های تبدیل (remove_reference_t) ارائه می‌دهد
  • std::void_t الگوی تشخیص را ممکن می‌کند — بررسی اینکه آیا یک نوع اعضا یا عملیات خاصی دارد
  • پیام‌های خطای SFINAE به‌طور معروفی ناخوانا هستند — مفاهیم ‎C++‎20 جایگزین مدرن هستند

آزمون — دانش خود را بسنجید

(20 امتیاز)

1. SFINAE مخفف چیست و به چه معناست؟

2. `std::void_t<decltype(std::declval<T>().size())>` در تخصص قالب چه کاری انجام می‌دهد؟

3. `std::decay_t<T>` چه کاری انجام می‌دهد؟