本文是AI Chat系列文章的第8篇,介绍C++中的SFINAE机制。
Introduction
在C++中,如果你有接触Template metaprogramming(TMP),那么SFINAE就是一个绕不去的存在。其全称为”Substitution Failure Is Not An Error”,指的是模板替换中发生的Failure就不是一个Error。Failure都不是Error,那岂不是可以为所欲为了?它究竟想解决一个什么问题?SFINAE到底是不是Error?接下来我们一探究竟。
SFINAE is indeed ERROR
我们写一个简单的例子:
#include <iostream>
#include <type_traits>
template<int I>
void div(char(*)[I % 2 == 1] = nullptr)
{
// this overload is selected when I is odd
std::cout << "div() called - I is odd" << std::endl;
}
int main() {
div<1>();
div<2>();
return 0;
}
div模板函数接收一个编译时常量整数I,如果I为奇数,那么char(*)[true]
等价于 char(*)[1]
, 其定义一个指向大小为1的char数组的指针,其是一个有效的类型,函数重载成功,div<1>()
可以正确编译使用。
但是如果I为偶数, char(*)[false]
等价于 char(*)[0]
,其表征一个指向大小为0的数组的指针,在C++中这是非法操作,因此div<2>()
编译不会通过。
那么问题来了,不是说”Substitution Failure Is Not An Error”吗,为啥编译不通过呢?
这里援引一下cppreference中关于SFINAE的一段描述:
This rule applies during overload resolution of function templates: When substituting the explicitly specified or deduced type for the template parameter fails, the specialization is discarded from the overload set instead of causing a compile error.
通俗的解释,如果substituion failure发生在重载解析的时候,其就不是Error,其他情况下该报错还是得报错。
对上面的程序,添加一个重载实现,如下所示,那么程序就可以正常编译了。
template<int I>
void div(char(*)[I % 2 == 0] = nullptr)
{
// this overload is selected when I is even
std::cout << "div() called - I is even" << std::endl;
}
A Step Further
下面例子实现了一个简易的Matrix类。鉴于Vector也是一个Matrix,因此其很自然的想基于TMP思想实现一个只适用于Vector的函数vectorOnlyFunc。
#include <iostream>
#include <type_traits>
// Test class template with different SFINAE scenarios
template<typename T, int Rows, int Cols>
class Matrix {
public:
using Scalar = T;
enum{_Rows = Rows, _Cols = Cols};
// Constructor
Matrix() {
std::cout << "Matrix<" << typeid(T).name() << ", " << Rows << ", " << Cols << "> constructed" << std::endl;
}
template<typename std::enable_if_t<Rows == 1 || Cols == 1, bool> = true>
void vectorOnlyFunc() {
std::cout << "vectorOnlyFunc() called - only for vectors" << std::endl;
}
template<int _OtherCols,
typename std::enable_if_t<Rows == 1 || _OtherCols == 1, bool> = true>
void vectorMultiply(const Matrix<T, Cols, _OtherCols>& other) {
std::cout << "vectorMultiply() called with " << _OtherCols << " columns" << std::endl;
}
};
int main() {
// Test 1: 2x2 matrix (non-vector)
Matrix<float, 2, 2> matrix2x2;
Matrix<float, 1, 3> vector1x3;
// This will work - SFINAE with function template parameter
vector1x3.vectorOnlyFunc();
return 0;
}
直接上结果,这个例子是编译不通过的。你可能会疑惑,我实例化了一个2x2的matrix和一个1x3的vector,我也没有针对matrix调用vectorOnlyFunc,为什么会编译不通过?
g++ -std=c++17 -Wall -Wextra -O2 -o sfinae_test sfinae_test.cpp
sfinae_test.cpp: In instantiation of ‘class Matrix<float, 2, 2>’:
sfinae_test.cpp:31:25: required from here
sfinae_test.cpp:16:10: error: no type named ‘type’ in ‘struct std::enable_if<false, bool>’
16 | void vectorOnlyFunc() {
| ^~~~~~~~~~~~~~
其实,上面这个例子是大家刚接触模板编程时很容易犯的一个错误。这里我们来问一下AI,下面是AI的答复:
You’re absolutely right! This confirms the issue. Even though you never call the test function, it still fails to compile. This is a classic SFINAE problem.
The issue is that the function template is being instantiated during class template instantiation, not when it’s called. When you create a
Matrix<Scalar, 2, 2>
object, the compiler tries to instantiate ALL member functions of the class template, including your test function.
For a 2×2 matrix, the condition
_Rows == 1 || _Cols == 1
evaluates tofalse
, sostd::enable_if<false, bool>::type
has notype
member, causing the compilation error.
The problem with your current approach is that SFINAE only works at function call time, not at class template instantiation time. When the class template is instantiated, all member functions must be valid, even if they’re never called.
For your use case,
static_assert
is probably the cleanest solution since you want to prevent the function from being used on non-vector matrices.
简单概述一下,虽然matrix2x没有调用vectOnlyFunc,但是其在实例化的时候,由于std::enable_if_t<Rows == 1 || Cols == 1, bool>
都是确定的类型,没有待替换的模板类型,因此其被立即解析。由于Rows == 1 || Cols == 1
为false,std::enable_if_t<false, bool>
是一个未定义的类型,从而导致编译错误。这种情况下,SFINAE机制不是免死金牌,也是不受用的。
那么,正确的实现应该是什么样子的呢,AI给出了下面的几种机制:
// 1. **Use `std::enable_if_t` with a dummy parameter:**
template<typename Dummy = void,
typename std::enable_if_t<_Rows == 1 || _Cols == 1, Dummy>* = nullptr>
void vectorOnlyFunc() {
std::cout << "vectorOnlyFunc" << std::endl;
}
// 2. **Use `static_assert` (Recommended for this case):**
void vectorOnlyFunc() {
static_assert(_Rows == 1 || _Cols == 1, "This function is only valid for vectors");
std::cout << "vectorOnlyFunc" << std::endl;
}
简单介绍下上面提到的2种方案:
- 方案1,使用一个dummy的模板类型。加入Dummy类型后,vectorOnlyFunc不会在类的实例化的时候就触发编译,只有遇到调用的地方才会。
- 方案2,使用
static_assert
,在不满足Vector条件的时候就触发编译报警。
其实,这两种方案本质是差不多的,都是在遇到调用的时候,检测到条件不满足就触发编译报警。从代码可读性的角度来看,第二种方案反而更好一些。这一点,在cppreference也有提到:
Where applicable, tag dispatch, if constexpr(since C++17), and concepts (since C++20) are usually preferred over use of SFINAE. static_assert is usually preferred over SFINAE if only a conditional compile time error is wanted.(since C++11)
Right way to utilize SFINAE?
那么,最终的问题来了,SFINAE机制究竟怎么用才是正道?那自然是函数重载了。
下面是实现一个Matrix的点乘操作的例子。一般情况下,矩阵乘的结果还是一个矩阵。但是如果是向量点乘,那么其结果应该是一个数才更合理。 这个时候利用SFINAE机制来实现函数的重载就相当方便了:
template<typename T, int Rows, int Cols>
class Matrix {
public:
enum{_Rows = Rows, _Cols = Cols};
template <int _Other_Cols, typename std::enable_if<_Rows != 1 || _Other_Cols != 1, bool>::type = true>
auto operator*(const Matrix<Scalar, _Cols, _Other_Cols>& other) const {
using MultResultType = Matrix<Scalar, _Rows, _Other_Cols>;
MultResultType M;
// [TODO] do matrix multipy, and return a matrix
return M;
}
// Specialized for vector dot product.
template <int _Other_Cols, typename std::enable_if<_Rows == 1 && _Other_Cols == 1, bool>::type = true>
Scalar operator*(const Matrix<Scalar, _Cols, _Other_Cols>& other) const {
Scalar result;
// [TODO] do vector multipy, and return a scalar.
return result;
}
}
上面的例子中,判断条件比较简单,利用模板特化也可以做。但是对于其他的应用,如果判断条件很复杂,用模板特化就没有SFINAE机制来的方便。
Conclusion
SFINAE(Substitution Failure Is Not An Error)是一个强大但经常被误解的C++模板机制,它既是模板元编程的福音,也是陷阱。其不是免死金牌,需要深入理解后才可以高效的使用。
实际开发中,要注意SFINAE的一些关键限制:
- 类模板实例化:SFINAE无法在类模板实例化过程中保护成员函数。当类模板被实例化时,所有成员函数都必须是有效的,即使它们从未被调用。
- 立即类型解析:在类实例化时确定的模板参数(如Rows和Cols)会立即被解析,而不是延迟到函数调用时。
- 语法错误 vs. 类型失败:SFINAE优雅地处理类型替换失败,但无法克服无效的C++语法或语言规则违反。
从C++17开始,C++提供了更优雅的解决方案,比如: if constexpr
, concept
。条件允许的情况下,推荐使用最新的一些模板meta-programming技巧。
Comments