0Pricing
C++ Academy · 课时

SFINAE

有条件地启用函数

SFINAE 是 CoddyKit 上的免费 C++ Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C++ Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C++ Academy 课程共包含 4 节课。

SFINAE 是什么意思

SFINAE 是“替换失败不是错误”("Substitution Failure Is Not An Error")的缩写。当编译器替换模板参数后结果不符合语法时,该候选项会被直接移除,而不会引发严重错误。

  • 它可以实现有条件的重载。
  • 它是编译期选择的基础。
#include <iostream>
#include <type_traits>

template <typename T>
typename std::enable_if<std::is_integral<T>::value, bool>::type
isOdd(T n) { return n % 2 != 0; }

int main() {
    std::cout << std::boolalpha << isOdd(3) << '\n';
    return 0;
}

替换失败的实际效果

如果替换某个类型会使函数签名无效,编译器会悄然跳过该函数,继续检查其他候选项。

#include <iostream>

template <typename T>
auto sizeOfValue(T t) -> decltype(t.size()) { return t.size(); }

int main() {
    std::string s = "hello";
    std::cout << sizeOfValue(s) << '\n';
    return 0;
}

两个重载,一个胜出

提供两个重载,使得对于给定类型恰好只有一个能够成功替换。SFINAE 会丢弃无效的那个。

#include <iostream>
#include <type_traits>

template <typename T>
typename std::enable_if<std::is_integral<T>::value, const char*>::type
name() { return "integral"; }

template <typename T>
typename std::enable_if<std::is_floating_point<T>::value, const char*>::type
name() { return "floating"; }

int main() {
    std::cout << name<int>() << ' ' << name<double>() << '\n';
    return 0;
}

decltype 与表达式 SFINAE

在返回类型中对表达式使用 decltype,可以让函数仅在该表达式对该类型有效时存在。

#include <iostream>

template <typename T>
auto tryDouble(T x) -> decltype(x + x) { return x + x; }

int main() {
    std::cout << tryDouble(21) << '\n';
    std::cout << tryDouble(2.5) << '\n';
    return 0;
}

检测成员

一种经典的 SFINAE 技巧是检查使用某个成员的表达式能否通过编译,从而检测类型是否具有该成员。

#include <iostream>
#include <type_traits>
#include <vector>

template <typename T>
auto hasSize(int) -> decltype(std::declval<T>().size(), std::true_type{});

template <typename T>
std::false_type hasSize(...);

int main() {
    std::cout << std::boolalpha;
    std::cout << decltype(hasSize<std::vector<int>>(0))::value << '\n';
    std::cout << decltype(hasSize<int>(0))::value << '\n';
    return 0;
}

... 回退

接受 ...(可变参数)的重载优先级最低。当受 SFINAE 约束的重载失败时,它充当兜底选项。

#include <iostream>

template <typename T>
auto pick(int) -> decltype(T{}.value, void()) { std::cout << "has value\n"; }

template <typename T>
void pick(...) { std::cout << "no value\n"; }

struct WithValue { int value = 0; };

int main() {
    pick<WithValue>(0);
    pick<int>(0);
    return 0;
}

void_t

C++17 的 std::void_t 简化了检测过程:它会将所有格式正确的类型映射为 void,因此只有被检查的表达式有效时,特化才会触发。

#include <iostream>
#include <type_traits>
#include <vector>

template <typename T, typename = void>
struct HasSize : std::false_type {};

template <typename T>
struct HasSize<T, std::void_t<decltype(std::declval<T>().size())>> : std::true_type {};

int main() {
    std::cout << std::boolalpha;
    std::cout << HasSize<std::vector<int>>::value << '\n';
    std::cout << HasSize<int>::value << '\n';
    return 0;
}

为什么不直接报错

没有 SFINAE 时,无效的替换会成为严重的编译错误,并且无法恢复。SFINAE 会将它转变为安静的“不是候选项”。

#include <iostream>
#include <type_traits>

template <typename T>
typename std::enable_if<std::is_pointer<T>::value, bool>::type
isNull(T p) { return p == nullptr; }

int main() {
    int x = 0;
    std::cout << std::boolalpha << isNull(&x) << '\n';
    return 0;
}

标签分派替代方案

SFINAE 可能难以阅读。标签分派则使用小型标签类型和重载决议来选择 implementation。

#include <iostream>
#include <type_traits>

template <typename T>
void impl(T v, std::true_type) { std::cout << "integral: " << v << '\n'; }

template <typename T>
void impl(T v, std::false_type) { std::cout << "other: " << v << '\n'; }

template <typename T>
void handle(T v) { impl(v, std::is_integral<T>{}); }

int main() {
    handle(5);
    handle(2.5);
    return 0;
}

SFINAE 与 constexpr if

在 C++17 中,if constexpr 通常可以替代函数内部的 SFINAE 分支,因为未使用的分支会在编译期被丢弃。

#include <iostream>
#include <type_traits>

template <typename T>
void describe(T v) {
    if constexpr (std::is_integral<T>::value) {
        std::cout << "integral: " << v << '\n';
    } else {
        std::cout << "other: " << v << '\n';
    }
}

int main() {
    describe(7);
    describe(3.14);
    return 0;
}

整合运用

当您必须在重载层级根据类型属性从多个独立的函数模板中进行选择时,SFINAE 尤其有用。

#include <iostream>
#include <type_traits>

template <typename T>
typename std::enable_if<std::is_signed<T>::value, T>::type
absValue(T v) { return v < 0 ? -v : v; }

int main() {
    std::cout << absValue(-9) << '\n';
    return 0;
}

快速检查

测试您对 SFINAE 的理解。

回顾

您学习了 SFINAE:

  • 无效的替换会移除候选项,而不是产生错误
  • 与 enable_if、decltype 和 void_t 搭配使用,可以有条件地启用函数
  • ... 重载充当回退选项
  • 现代替代方案包括标签分派和 if constexpr

接下来,您将详细学习经典的 enable_if 模式。

常见问题解答

「SFINAE」课时是免费的吗?

是的 — 「SFINAE」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C++ Academy 课程的其余内容,请升级到 CoddyKit PRO。 C++ Academy 课程共包含 4 节课。

「SFINAE」这节课中我会学到什么?

有条件地启用函数 你通过在浏览器中直接运行的动手代码来练习 C++ Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 C++ Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 C++ Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「SFINAE」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 C++ Academy 课中编写并运行代码吗?

能。每节 C++ Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 完全特化
  2. 部分特化
  3. SFINAE
  4. enable_if 模式
← 返回 C++ Academy