SFINAE, Type Traits & enable if
Поймите, что сбой замены не является принципом ошибки, используйте черты типа для интроспекции компиляции и ограничивайте шаблоны с помощью enable if.
Что такое SFINAE?
SFINAE расшифровывается как «Substitution Failure Is Not An Error». Когда компилятор пытается инстанцировать шаблон, он заменяет выведенные или заданные типы в шаблон. Если эта замена производит недействительный тип или выражение в непосредственном контексте объявления шаблона, компилятор не испускает ошибку — он просто удаляет этот шаблон из набора перегрузок и переходит к проверке других кандидатов.
Этот механизм является основой шаблонного метапрограммирования в C++11/14. Он позволяет писать шаблоны, которые являются условно доступными на основе свойств аргументов их типа.
Ключевая фраза — «немедленный контекст». Сбои замещения глубоко внутри функционального тела являются жёсткими ошибками , а не SFINAE. Только сбои в подписи функции (тип возврата, типы параметров, список параметров шаблона или требуется пункт) запускают SFINAE.
Исполнитель: Enable if
std::enable_if определяет тип type, равный T, только если Condition является true. Если Condition является false, то член type не существует, вызывая отказ замены (SFINAE). std::enable_if_t является сокращением от typename std::enable_if.
#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.
Черты запросов (возврат кайфа через суффикс ::value или _v):std::is_integral_v — это целочисленный тип?std::is_floating_point_v — это тип с плавающей запятой?
— std::is_pointer_v — это указатель?std::is_class_v — это класс/структура?
- std::is_const_v - T-квалифицирован?std::is_same_v — T и U одного типа?
— std::is_base_of_v — База является базой производных?std::is_convertible_v — можно ли конвертировать в To?
— std::is_constructible_v — можно ли построить из аргов?
Черты трансформации (возврат измененного типа через суффикс ::type или _t):
- std::remove_reference_t - полоски & или &&
- std::remove_const_t - полоски const
- std::decay_t - применяет распад массива-точечного, функции-точечного и удаляет cv/reference
- std::conditional_t - тройка по времени компиляции: если Cond, то Tstd::common_type_t - тип, который все Ts могут конвертировать в
Оригинальное название: Void t & the Detection Idiom
std::void_t<...> (C++17) отображает любую последовательность типов в void. Его сила заключается в SFINAE: если какой-либо тип в списке недействителен, специализация отбрасывается. Это позволяет использовать идиому обнаружения — проверять, имеет ли тип конкретный элемент, метод или вложенный тип.
#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
}Склейка с SFINAE
Вы можете использовать decltype для запуска SFINAE в зависимости от того, является ли выражение действительным. Этот метод часто сочетается с оператором запятой, чтобы отбросить результат выражения и вернуть желаемый тип.
#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 — он существует только тогда, когда условие истинно.обеспечивает характеристики запросов времени компиляции (is_integral_v) и характеристики трансформации (remove_reference_t)std::void_tпозволяет распознавать идиому — проверять, есть ли у типа конкретные члены или операции.- Сообщения об ошибках SFINAE, как известно, нечитаемы — концепции C++20 являются современной заменой.
Тест — проверьте свои знания
(20 XP)1. Что означает SFINAE и что это значит?
2. Чего достигает `std::void_t<decltype(std::declval<T>().size())>` в шаблонной специализации?
3. Чем занимается `std::decay_t<T>`?