网站首页 > 博客文章 正文
什么是SFINAE?您在什么地方可以使用这种元编程技术?在现代C++中有更好的选择吗?那么C++ 20的Concept呢?
请往下阅读找出答案。
介绍
让我们从这个概念背后的一些基本想法开始:
简而言之:编译器可以拒绝对于给定类型“无法编译”的代码。
维基百科:
替换失败并非错误 (Substitution failure is not an error, SFINAE)是指C++语言在模板参数匹配失败时不认为这是一个编译错误。戴维·范德沃德最先引入SFINAE缩写描述相关编程技术。
我们在这里讨论的是一些与模板、模板替换规则和元编程相关的东西……这使得它可能成为一个可怕的领域!
一个简单的例子:
struct Bar {
typedef double internalType;
};
template <typename T>
typename T::internalType foo(const T& t) {
cout << "foo<T>\n";
return 0;
}
int main() {
foo(Bar());
foo(0); // << error!
}
我们有一个很好的模板函数,它返回T::internalType,我们用Bar和int参数类型调用它。
当然,代码不能编译。第一个调用 foo(Bar()) 是正确的构造,但第二个会生成以下错误(GCC):
no matching function for call to 'foo(int)' ... template argument deduction/substitution failed:
当我们进行更正并为int类型提供适当的函数时,非常简单:
int foo(int i) { cout << "foo(int)\n"; return 0; }
即可以编译运行。
为什么?
当我们为int类型添加重载函数时,编译器可以找到一个合适的匹配项并调用代码。但在编译过程中,编译器还会“查看”模版函数声明。这个函数对于int类型是无效的,那么为什么甚至没有报告警告(就像我们在没有提供第二个函数时得到的那样)?为了理解这一点,我们需要看看为函数调用构建重载解析集的过程。
重载解析
当编译器试图编译函数调用时(简化):
- 执行一个名字查找
- 对于函数模版,模版参数根据实际传递给函数的参数类型推导出来。所有模版参数(返回类型和参数类型)用推导出来的类型替换如果导致非法类型(比如int::internalType),这个函数就从重载解析集中排除(SFINAE)
- 最后,我们得到一批函数可用于这个调用。如果没有找到,那么编译失败。如果不止一个函数,那我们遇到了歧义。一般来说,参数最匹配的那个函数被调用。
在我们的示例中:typename T::internalType foo(const T& t) 与int不匹配,它被重载解析集拒绝。但最后,foo(int i) 是集合中唯一的选项,因此编译器没有问题。
在哪里使用
我想您对SFINAE的功能有了一个基本的了解,但是我们可以在哪里使用这种技术呢?一般的回答是:当我们想为特定类型选择适当的函数时。
一些例子:
- 当T有一个给定的方法时(比如toString()),调用某个函数
- 放置窄化或者错误类型转换
- 检测传递给构造函数的初始化列表中的对象数量
- 为不同的type traits特化函数,比如is_integral, is_array, is_class, is_pointer等等
好了,但是我们怎么能写出这样的SFINAE呢?有辅助吗?
让我们会一会std::enable_if。
什么是std::enable_if
SFINAE的首席用例可以在std::enable_if表达式中看到。
std::enable_if是一个工具集,从C++11起就在标准库中,其内部使用SFINAE。它可以从函数模版或者类模版中包含或者排除某个重载。
比如:
// C++11:
template <class T>
typename std::enable_if<std::is_arithmetic<T>::value, T>::type
foo(T t) {
std::cout << "foo<arithmetic T>\n";
return t;
}
这个函数可以用于所有的is_arithmetic类型(int, long, float…)。如果传递其他类型(比如MyClass),则实例化失败。换句话说,模版对于非算数类型的实例化从重载解析集中被排除。这个结构可用于模版参数、函数参数和返回值类型。
enable_if<condition, T>::type在condition为true的时候生成T,在false的时候替换失败。
enable_if可以和type traits一起使用,以根据trait提供最佳函数。
同时请注意,在C++14和17中有一个更好更紧凑的语法。由于_v和_t变量模版和模版别名的引入,不再需要使用enable_if和trait的::type和::value。
之前的代码变为:
// C++17:
template <class T>
typename std::enable_if_t<std::is_arithmetic_v<T>, T> // << shorter!
foo(T t) {
std::cout << "foo<arithmetic T>\n";
return t;
}
注意里面std::enable_if_t和std::is_arithmetic_v的使用。
表达式SFINAE
C++11有更复杂的SFINAE选项。
n2634: Solving the SFINAE problem for expressions
这篇文档明确了规范,它允许你在decltype和sizeof中使用表达式。
比如:
template <class T> auto f(T t1, T t2) -> decltype(t1 + t2);
在上面的例子中,表达式t1+t2会被检查。它可以用于两个int类型(其结果类型还是int),但是不能用于一个int和一个vector。
表达式检查对编译器增添了更多的复杂性。在重载解析一节中,我仅仅提到模版参数替换, 但是现在,编译器需要查看表达式并做完整的语义检查。
SFINAE的不好之处?
SFINAE和enable_if是引人注目的特性,但是它也很难弄对。简单的示例可能没问题,但在实际生活的场景中,您可能会遇到各种问题:
- 模版错误:您愿意读编译器产生的模版错误信息吗?特别是当你使用STL类型时。
- 可读性
- 嵌套模版通常在enable_if中不能使用
我们能做的更好吗?
SFINAE替代品
我们至少有三样:
- 标签分发tag dispatching
- 编译时if
- 还有……Concept
我们简短复习下。
标签分发tag dispatching
这个版本的选择哪个函数调用的可读性高的多。首先,我们定义一个核心函数,然后根据一些编译时条件决定调用实现函数的A或者B版本。
template <typename T>
int get_int_value_impl(T t, std::true_type) {
return static_cast<int>(t+0.5f);
}
template <typename T>
int get_int_value_impl(T t, std::false_type) {
return static_cast<int>(t);
}
template <typename T>
int get_int_value(T t) {
return get_int_value_impl(t, std::is_floating_point<T>{});
}
当你调用get_int_value时,编译器会检查std::is_floating_point的值,然后调用匹配的_impl函数。
编译时if – C++17
从C++17开始,我们有了一个新工具,内建于语言之中,其允许你在编译时检查条件,而且不需要你写复杂的模版代码。
我们可以通过一个片段来展示:
template <typename T>
int get_int_value(T t) {
if constexpr (std::is_floating_point_v<T>) {
return static_cast<int>(t+0.5f);
}
else {
return static_cast<int>(t);
}
}
你可以在下面这篇博客里读到更多:Simplify code with ‘if constexpr’ in C++17。
Concept – C++20
在每一个C++标准修订中,我们都可以得到更好的技术和工具来写模版。在C++20中,我们有了一个期待已久的功能,可以革命我们写模版的方式。
使用Concept,你可以对模版参数添加限制并得到更好的编译警告。
一个基本的例子:
// define a concept:
template <class T>
concept SignedIntegral = std::is_integral_v<T> && std::is_signed_v<T>;
// use:
template <SignedIntegral T>
void signedIntsOnly(T val) { }
在上面的代码中,我们首先创建一个Concept来描述有符号和整数的类型。请注意,我们可以使用现有的类型特征。接着,我们使用它来定义一个模板函数,它只支持与Concept匹配的类型。这里我们不使用typename T,但是我们可以使用一个Concept名称。
我们现在用一个示例来结合这些知识。
一个示例
最后,我想通过一些实际例子,看看如何利用SFINAE:
测试类:
template <typename T>
class HasToString {
private:
typedef char YesType[1];
typedef char NoType[2];
template <typename C> static YesType& test(decltype(&C::ToString));
template <typename C> static NoType& test(...);
public:
enum { value = sizeof(test<T>(0)) == sizeof(YesType) };
};
上面的模板类将用于测试某些给定类型T是否具有ToString()方法。这里…有什么,又在哪里使用了SFINAE概念?你能看见吗?
当我们想进行测试时,我们需要写下:
HasToString<T>::value
如果我们传递int会怎么样?它将类似于本文开头的第一个示例。编译器将尝试执行模板替换,但在这里会匹配失败:
template <typename C> static YesType& test( decltype(&C::ToString) ) ;
显然,没有int::ToString方法,因此第一个重载方法将从解析集中排除。但是,第二个方法将会通过 (NoType& test(...)),因为它可以在所有其他类型上调用。所以我们得到了SFINAE!一个方法被删除,只有第二个方法对此类型有效。
最后,最终的enum值被计算为:
enum { value = sizeof(test<T>(0)) == sizeof(YesType) };
由于选中的函数返回NoType,sizeof(NoType)与sizeof(YesType)不等,所以最终的值为0。
当我们测试如下类时又该如何?
class ClassWithToString {
public:
string ToString() { return "ClassWithToString object"; }
};
现在模版替换会生成两个候选:两个test方法都有效,但是前一个更好,所以它被选中。我们得到YesType,并且最终的 HasToString<ClassWithToString>::value返回1作为结果。
怎样使用这样的检查类:
理想情况下,if语句会很方便:
if (HasToString<decltype(obj)>::value)
return obj.ToString();
else
return "undefined";
我们可以使用if constexpr,但是为了示例的目的,让我们继续使用C++11/14方案。
我们可以使用enable_if,并创建两个函数:一个接受带有ToString方法的类对象,另一个接受所有其他类型。
template<typename T>
typename enable_if<HasToString<T>::value, string>::type
CallToString(T * t) {
return t->ToString();
}
string CallToString(...) {
return "undefined...";
}
同样,上面的代码里有SFINAE。如果你传递一个类型,HasToString<T>::value = false,则enable_if实例化失败。
上面的技术非常复杂,但是功能也很有限。比如,它没有限制函数的返回类型。
让我们看看现代C++有什么帮忙的。
现代C++来救
在文章最初版本的一个评论中,STL(stephantt.Lavavej)提到我在文章中提出的解决方案来自于旧的Cpp风格。那么新的现代风格是什么呢?
我们可以看到几个东西:
- decltype
- declval
- constexpr
- std::void_t
- 检测范式
decltype
decltype是一个返回表达式类型的强大工具。我们已经使用过:
template <typename C>
static YesType& test( decltype(&C::ToString) ) ;
它返回C::ToString成员方法的类型(如果这个类有这个方法的话)
declval
declval可以让你调用类型T的一个方法而不需要创建一个真正的对象。在我们的例子中,我们可以用它来得到成员方法的类型:
decltype(declval<T>().toString())
constexpr
constexpr提示编译器在编译时计算表达式(如果可能的话)。没有它的话,我们的checker方法可能只能在运行时进行评估。新方式建议为大多数方法添加constexpr。
void_t
- SO question: Using void_t to check if a class has a method with a specific signature
- SO question: How does void_t work
CppCon 2014: Walter E. Brown “Modern Template Metaprogramming: A Compendium, Part II” – YouTube
从29分钟开始看,特别是39分钟。
这一个不可思议的元编程模式。我不想多说什么,就看视频吧。
检测范式
- WG21 N4436, PDF – Proposing Standard Library Support for the C++ Detection Idiom, by Walter E. Brown
- std::is_detected
- wikibooks: C++ Member Detector
Walter E.Brown提出了一个完整的工具类,可以用来检查给定类的接口和其他属性。当然,大部分都是基于void_t的。
改进后的代码
// default template:
template< class , class = void >
struct has_toString : false_type { };
// specialized as has_member< T , void > or sfinae
template< class T>
struct has_toString<T , void_t<decltype(&T::toString)>> : std::is_same<std::string, decltype(declval<T>().toString())>
{ };
很漂亮,对吧:
它使用基于void_t的显式检测范式. 基本上,当类中没有T::toString()方法时, 出现SFINAE,我们最终得到通用缺省模版。但如果类中有这个方法时,特化版本被选择。如果我们不关心方法的返回类型,那么到这里就结束了。但在这里,我们通过继承std::is_same来检查返回类型是不是std::string,从而得到true_type或者false_type。
Concept来救
我们可以在C++ 20中做得更好。使用此功能,我们可以声明一个新的Concept来指定类的接口:
比如:
template <typename T>
concept HasToString = requires(T v)
{
{v.toString()} -> std::convertible_to<std::string>;
};
这就是全部。漂亮易读。
我们可以使用一些测试代码来实验:
#include <iostream>
#include <string>
#include <type_traits>
template <typename T>
concept HasToString = requires(const T v)
{
{v.toString()} -> std::convertible_to<std::string>;
};
struct Number {
int _num { 0 };
std::string toString() const { return std::to_string(_num); };
};
void PrintType(HasToString auto& t) {
std::cout << t.toString() << '\n';
}
int main() {
Number x { 42 };
PrintType(x);
}
如果您的类型不支持toString,那么您可能会得到如下编译器错误(GCC 10):
int x = 42;
PrintType(x);
error: use of function 'void PrintType(auto:11&) [with auto:11 = int]' with unsatisfied constraints
| PrintType(x);
| ^
note: declared here
| void PrintType(HasToString auto& t) {
| ^~~~~~~~~
In instantiation of 'void PrintType(auto:11&) [with auto:11 = int]':
required for the satisfaction of 'HasToString<auto:11>' [with auto:11 = int]
in requirements with 'const int v'
note: the required expression 'v.toString()' is invalid
8 | {v.toString()} -> std::convertible_to<std::string>;
| ~~~~~~~~~~^~
我们来到了一个全新的世界。从复杂的SFINAE代码,在C++14和C++17中得到改进,最终到C++20的简明的语法。
结论
在这篇文章中,我们介绍了SFINAE的理论和示例-一种允许您从重载解析集中拒绝代码的模板编程技术。在原始形式中,这可能有点复杂,但得益于现代C++,我们有许多工具可以帮助:例如,enable_if,std::declval以及一些其他的工具。更重要的是,如果你幸运的与最新的C++标准,你可以使用C++17的if constexpr和C++20的Concept。
猜你喜欢
- 2024-10-12 C++核心准则T.24:用标签类或特征区分只有语义不同的概念
- 2024-10-12 用苹果发布会方式打开C++20(苹果在哪开发布会)
- 2024-10-12 C++核心准则T.25:避免互补性约束(规矩是一种约束,一种准则)
- 2024-10-12 C++核心准则T.21:为概念定义一套完整的操作
- 2024-10-12 C++核心准则T.5:结合使用泛型和面向对象技术应该增强效果
- 2024-10-12 C++经典书籍(c++相关书籍)
- 2024-10-12 C++一行代码实现任意系统函数Hook
- 2024-10-12 C++核心准则T.11:只要可能就使用标准概念
- 2024-10-12 C++核心准则T.48:如果不能用概念,用enable_if
- 2024-10-12 C++核心准则T.13:简单、单类型参数概念使用缩略记法更好
你 发表评论:
欢迎- 最近发表
-
- 给3D Slicer添加Python第三方插件库
- Python自动化——pytest常用插件详解
- Pycharm下安装MicroPython Tools插件(ESP32开发板)
- IntelliJ IDEA 2025.1.3 发布(idea 2020)
- IDEA+Continue插件+DeepSeek:开发者效率飙升的「三体组合」!
- Cursor:提升Python开发效率的必备IDE及插件安装指南
- 日本旅行时想借厕所、买香烟怎么办?便利商店里能解决大问题!
- 11天!日本史上最长黄金周来了!旅游万金句总结!
- 北川景子&DAIGO缘定1.11 召开记者会宣布结婚
- PIKO‘PPAP’ 洗脑歌登上美国告示牌
- 标签列表
-
- ifneq (61)
- messagesource (56)
- aspose.pdf破解版 (56)
- promise.race (63)
- 2019cad序列号和密钥激活码 (62)
- window.performance (66)
- qt删除文件夹 (72)
- mysqlcaching_sha2_password (64)
- ubuntu升级gcc (58)
- nacos启动失败 (64)
- ssh-add (70)
- jwt漏洞 (58)
- macos14下载 (58)
- yarnnode (62)
- abstractqueuedsynchronizer (64)
- source~/.bashrc没有那个文件或目录 (65)
- springboot整合activiti工作流 (70)
- jmeter插件下载 (61)
- 抓包分析 (60)
- idea创建mavenweb项目 (65)
- vue回到顶部 (57)
- qcombobox样式表 (68)
- vue数组concat (56)
- tomcatundertow (58)
- pastemac (61)
本文暂时没有评论,来添加一个吧(●'◡'●)