C++ 模板元编程 SFINAE 原则深度解析:从原理到实战应用全指南
- 作者: 刘杰
- 来源: 技术那些事
- 阅读:171
- 发布: 2025-07-02 09:57
- 最后更新: 2025-07-03 08:02
简介
模板元编程代码在编译期执行,决定模版的实例化版本。
什么是 SFINAE原则?
Substitudion Failure Is Not An Error 的缩写, 意思是 “替换失败并不是错误”。
SFANAE 是 C++ 语言标准中固有的一部分, 是模板编程中的一个核心特性。 SFINAE 不是可以选择开启或关闭的功能, 而是编译器在处理模板时自然遵循的原则。
SFINAE(Substitution Failure Is Not An Error)的机制确实意味着在模板参数匹配过程中,如果某次替换(Substitution)失败,并不会立即导致编译错误。编译器会继续尝试其他可用的模板特化或重载,寻找一个合适的匹配。只有当所有可能的选项都尝试过,且没有任何一个成功匹配时,才会导致编译失败。
编译期代码执行
模板元编程,主要针对类型的变化,大部分涉及到模板的相关知识。在编译期间,所有涉及到的类型,成了变量。模板类,模板函数,主要用来检测类型参数是否能够匹配,匹配的则会按照最有匹配的规则进行编译(模板匹配的SFINAE原则),不匹配则不会报错也不会执行。
编译期间,不存在代码的实际执行,所以涉及到函数,或者成员函数,只需要有声明即可,而不需要定义。部分模板函数的匹配,是通过函数模板进行的,这时的函数只要有声明即可。确定函数的签名涉及到函数名,函数的参数类型,以及参数的排列顺序。可以通过将模板类型,放到函数的参数进行匹配。
模板元编程,是通过模板匹配进行结果的获取
元编程常用技巧
static
-
用在结构体内的成员变量,主要用来获取值,避免了对结构的实例化(编译期间几乎用不到实例化)
-
用在成员函数之前(静态函数),可以使函数的执行不用实例化相关类,执行函数。
constexpr
- 可以放在变量的声明初始化上,表示编译期间有明确值。
- 放到函数前,表示函数在编译期间就有明确的返回结果。
- 修饰指针,与
const不同,指针加上constexpr只修饰指针的值,不能变。但是指向的对象可以变。即(p=0是错误的,*p=1可以执行) - 无法修饰成员函数,只能作为函数返回值类型,表明该函数返回的是一个编译期可确定的常量。
-
constexpr构造函数必须有⼀个空的函数体,即所有成员变量的初始化都放到初始化列表中。对象调⽤的成员函数必须使⽤constexpr修饰。
示例解析
检测成员函数是否存在
c++
// 常量表达式判断,编译期检测类型之类
if constexpr () {
// do something.
}
c++
// 检查是否存在无参构造函数的Helper结构体
template <typename T>
struct has_no_args_constructor {
template <typename U>
static constexpr std::true_type test(decltype(&U::U)); // 尝试匹配无参构造函数
template <typename>
static constexpr std::false_type test(...); // 匹配失败
using type = decltype(test<T>(0)); // 根据能否调用U::U来选择true_type或false_type
static constexpr bool value = type::value; // 提供结果
};
// 调用代码,检测 Test 类是否有无参构造函数.
if constexpr(has_no_args_constructor<Test>::value) {
// 如果有执行代码
} else {
// 没有执行的代码
}
检查两个类型是否相同
c++
template<typename T, typename X>
constexpr bool is_same = false;
template<typename T>
constexpr bool is_same<T,T> = true;
template<typename T, typename...Args>
struct type_in {}
实例解析
模板参数中,用来增加对模板参数的限制
此处的模板参数,包括类模板或者函数模板中的类型或者非类型模板参数。
c++
// 限制非类型模板参数
template<unsigned N, std::enable_if_t<(N>1 && N < 3), int> = 0>
void test() {
std::cout << N << std::endl;
}
// 限制类型模板参数,打印一个模板类型的类型,模板类型只能为 vector 或者 list
template<
template<typename, typename...> typename Container, typename T, typename ...Args,
typename std::enable_if<std::is_same_v<Container<T, Args...>, std::vector<T>> || std::is_same_v<Container<T, Args...>, std::list<T>>, int>::type = 0>
void print(const Container<T, Args...>& c) {
for (typename Container<T, Args...>::size_type i = 0; i < c.size(); ++i) {
std::cout << c[i] << std::endl;
}
}
模板函数的返回值类型的限制
c++
// 通过限制函数返回值 void,来进行参数类型的匹配,只能是 char,或者 int 类型
template<typename T>
std::enable_if_t<std::is_same_v<T, char> || std::is_same_v<T, int>, void>
test(const T& t)
{
std::cout << t << std::endl;
}
// 返回值类型根据参数类型推导
template<typename T, typename U>
auto test(const T& a, const U& b)->decltype(a + b);
// 这个例子中,使用了逗号表达式来实现一个可以接收任意数量和类型的重载函数。
// 这是C++模板元编程的一个高级技巧。
template<typename... Ts>
struct overload {
// 使用逗号表达式进行模板展开
template<typename T>
auto operator()(T&& t) const -> decltype(std::forward<T>(t)(std::declval<Ts>()...)) {
return std::forward<T>(t)(std::declval<Ts>()...);
}
};
模板函数的参数类型的限制(与模板参数类型限制类似)
c++
#include <iostream>
#include <type_traits>
#include <string>
// 使用SFINAE在函数参数列表中直接限制T只能是std::string或int
template<typename T>
void print(T value,
std::enable_if_t<std::is_same_v<T, int> ||
std::is_same_v<T, std::string>, int> = 0) {
std::cout << "Value: " << value << std::endl;
}
int main() {
print(42); // OK: int 类型
print(std::string("Hi")); // OK: std::string 类型
// 下面的调用会导致编译错误,因为它们不是int或std::string
// print(3.14f); // 编译错误:float 不是允许的类型
// print('c'); // 编译错误:char 不是允许的类型
return 0;
}
c++
template<template<typename, typename...> typename Container, typename T, typename ...Args>
void print(
const Container<T, Args...>& c,
std::enable_if_t<std::is_same_v<std::vector<T>, Container<T, Args...>> || std::is_same_v<std::list<T>, Container<T, Args...>>, int> = 0
) {
for (auto& element : c) {
std::cout << element << std::endl;
}
}
int main() {
std::vector<int> v = { 1, 2, 3, 4 };
// 显示调用
print<std::vector, int, std::allocator<int>>(v);
// 模板自动推断
print(v);
return 0;
}
常用函数
判断类型相同(std::is_same)
cpp
std::is_same
编译期的分支逻辑(std::enable_if)
定位为一个helper模板(助手模板),用于辅助其他模板的设计,表现一种:编译期的分支逻辑(编译期就可以确定走哪条分支)。
匹配重载的函数 / 类时如果没有匹配上,编译器并不会报错,相应的, 这个函数 /或类就不会作为候选。这是一个 C++11 的新特性,也是 enable_if 最核心的原理。
cpp
// STRUCT TEMPLATE enable_if
template <bool _Test, class _Ty = void> //泛化版本
struct enable_if {}; // no member "type" when !_Test
template <class _Ty> // 只有这个偏特化版本存在,才存在一个名字叫做 type 的类型别名(类型)
struct enable_if<true, _Ty> { // type is _Ty for _Test
using type = _Ty;
};