如何使SFINAE光滑可靠

你好 我们正在与您分享一篇有趣的文章,该文章的翻译是专门为“ C ++ Developer”课程的学生准备的。





今天我们有dámBalázs的来宾帖子。 Adam是匈牙利Verizon智能社区的一名软件工程师,并为嵌入式系统开发视频分析。 他的爱好之一是优化编译时间,因此他立即同意就该主题写客座文章。 您可以在LinkedIn上在线找到Adam。

一系列有关如何使SFINAE变得优雅的文章中 ,我们看到了如何使我们的SFINAE模板简洁而富有表现力

看看它的原始形式:

template<typename T> class MyClass { public: void MyClass(T const& x){} template<typename T_ = T> void f(T&& x, typename std::enable_if<!std::is_reference<T_>::value, std::nullptr_t>::type = nullptr){} }; 


并将其与这种更具表现力的形式进行比较:

 template<typename T> using IsNotReference = std::enable_if_t<!std::is_reference_v<T>>; template<typename T> class MyClass { public: void f(T const& x){} template<typename T_ = T, typename = IsNotReference <T_>> void f(T&& x){} }; 

我们可以合理地相信,已经可以放宽并开始在生产中使用它。 我们可以在大多数情况下使用它,但是-在我们谈论接口时-我们的代码应该安全可靠。 是这样吗 让我们尝试破解它!

缺陷1:可以绕过SFINAE


通常,根据条件,使用SFINAE禁用部分代码。 如果我们出于某种原因(例如,用户定义的算术类,针对特定设备的优化,出于培训目的等)需要实现用户定义的函数abs,这将非常有用:

 template< typename T > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); } int main() { int a{ std::numeric_limits< int >::max() }; std::cout << "a: " << a << " myAbs( a ): " << myAbs( a ) << std::endl; } 

该程序显示以下内容,看起来很正常:

 a: 2147483647 myAbs( a ): 2147483647 

但是我们可以使用无符号参数T来调用abs函数,结果将是灾难性的:

 nt main() { unsigned int a{ std::numeric_limits< unsigned int >::max() }; std::cout << "a: " << a << " myAbs( a ): " << myAbs( a ) << std::endl; } 

实际上,现在程序显示:

a: 4294967295 myAbs( a ): 1

我们的函数并非旨在与无符号参数一起使用,因此必须使用SFINAE限制可能的T集:

 template< typename T > using IsSigned = std::enable_if_t< std::is_signed_v< T > >; template< typename T, typename = IsSigned< T > > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); } 

该代码按预期工作:使用无符号类型的myAbs调用会导致编译时错误:

candidate template ignored: requirement 'std::is_signed_v< unsigned int>' was not satisfied [with T = unsigned int]

入侵SFINAE状态


那么此功能有什么问题呢? 要回答这个问题,我们需要检查myAbs如何实现SFINAE。

 template< typename T, typename = IsSigned<T> > T myAbs( T val ); 

myAbs是具有两种类型的输入模板参数的功能模板。 第一个是函数参数的实际类型,第二个是默认的IsSigned < T > (否则std::enable_if_t < std::is_signed_v < T > >std::enable_if < std::is_signed_v < T>, void>::type ,即void或替换失败)。

我们如何称呼myAbs ? 有3种方法:

 int a{ myAbs( -5 ) }; int b{ myAbs< int >( -5 ) }; int c{ myAbs< int, void >( -5 ) }; 

第一个和第二个调用很简单,但第三个调用很有趣: void模板的参数是什么?

第二个模板参数是匿名的,具有默认类型,但是它仍然是模板参数,因此您可以显式指定它。 这有问题吗? 在这种情况下,这确实是一个巨大的问题。 我们可以使用第三种形式来解决SFINAE检查:

 unsigned int d{ myAbs< unsigned int, void >( 5u ) }; unsigned int e{ myAbs< unsigned int, void >( std::numeric_limits< unsigned int >::max() ) }; 

这段代码可以很好地编译,但会导致灾难性的结果,请避免使用SFINAE:

 a: 4294967295 myAbs( a ): 1 

我们将解决这个问题-但首先:还有其他缺点吗? 好吧...

缺陷2:我们无法执行特定的实现


SFINAE的另一个常见用途是为特定的编译时条件提供特定的实现。 如果我们不想完全禁止使用myAbs值调用myAbs并为这些情况提供简单的实现该怎么办? 我们可以在C ++ 17中使用if constexpr(我们将在后面讨论),或者可以:

  template< typename T > using IsSigned = std::enable_if_t< std::is_signed_v< T > >; template< typename T > using IsUnsigned = std::enable_if_t< std::is_unsigned_v< T > >; template< typename T, typename = IsSigned< T > > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); } template< typename T, typename = IsUnsigned< T > > T myAbs( T val ) { return val; } 

那是什么

 error: template parameter redefines default argument template< typename T, typename = IsUnsigned< T > > note: previous default template argument defined here template< typename T, typename = IsSigned< T > > 

哦,C ++标准(C ++ 17;§17.1.16)规定如下

“不应在同一范围内通过两个不同的声明将默认参数提供给模板参数。”

糟糕,这正是我们所做的...

为什么不使用常规的?


我们可以只在运行时使用if:

 template< typename T > T myAbs( T val ) { if( std::is_signed_v< T > ) { return ( ( val <= -1 ) ? -val : val ); } else { return val; } } 

编译器将优化条件,因为创建模板后if (std::is_signed_v < T>)变为if (true)if (false) 。 是的,使用我们当前的myAbs实施myAbs这将起作用。 但是总的来说,这施加了很大的限制: ifelse必须对每个T有效T 如果我们稍微改变实现方式该怎么办:

 template< typename T > T myAbs( T val ) { if( std::is_signed_v< T > ) { return std::abs( val ); } else { return val; } } int main() { unsigned int a{ myAbs( 5u ) }; } 

我们的代码将立即崩溃:

 error: call of overloaded 'abs(unsigned int&)' is ambiguous 

SFINAE消除了此限制:我们可以编写仅对T的子集有效的代码(在myAb中,它仅对无符号类型有效或仅对有符号类型有效)。

解决方案:SFINAE的另一种形式


我们该如何克服这些缺点? 对于第一个问题,无论用户如何调用我们的功能,我们都必须强制执行SFINAE检查。 当前,当编译器不需要第二个模板参数的默认类型时,可以绕过我们的测试。

如果我们使用SFINAE代码声明模板参数类型而不提供默认类型怎么办? 让我们尝试:

 template< typename T > using IsSigned = std::enable_if_t< std::is_signed_v< T >, bool >; template< typename T, IsSigned< T > = true > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); } int main() { //int a{ myAbs( 5u ) }; int b{ myAbs< int >( 5u ) }; //int c{ myAbs< unsigned int, true >( 5u ) }; } 

在有效情况下,我们需要IsSigned为void以外的其他类型,因为我们想为此类型提供默认值。 void类型没有值,因此我们应该使用其他类型:bool,int,enum,nullptr_t等。通常我使用bool-在这种情况下,表达式看起来很有意义:

 template< typename T, IsSigned< T > = true > 

有效! 对于myAbs (5u)编译器会像以前一样引发错误:

 candidate template ignored: requirement 'std::is_signed_v<unsigned int>' was not satisfied [with T = unsigned int 

第二个调用myAbs < int> (5u)仍然有效,我们明确告诉编译器类型T ,因此将5u转换为int

最后,我们不再myAbs在手指周围跟踪myAbsmyAbs < unsigned int, true> (5u)引发错误。 不管是否在调用中提供默认值,无论如何都将评估SFINAE表达式的一部分,因为编译器需要匿名模板值的参数类型。

我们可以继续进行下一个问题-请稍候! 我认为我们不再覆盖同一模板参数的默认参数,原始情况是什么?

 template< typename T, typename = IsUnsigned< T > > T myAbs( T val ); template< typename T, typename = IsSigned< T > > T myAbs( T val ); 

但是现在有了当前代码:

 template< typename T, IsUnsigned< T > = true > T myAbs( T val ); template< typename T, IsSigned< T > = true > T myAbs( T val ); 

它看起来与先前的代码非常相似,因此我们可能会认为这也不起作用,但实际上此代码没有相同的问题。 什么是IsUnsigned < T> ? 布尔或查找失败。 什么是IsSigned < T> ? 同样,但如果其中一个是Bool,则另一个是查找失败。

这意味着我们不会覆盖默认参数,因为只有一个带有模板参数bool的函数,另一个是失败的替换,所以它不存在。

句法糖


UPD 由于发现其中的错误,该段落已被作者删除。

旧版本的C ++


上面所有这些都与C ++ 11兼容,唯一的区别是标准版本之间的限制定义的详细程度:

 //C++11 template< typename T > using IsSigned = typename std::enable_if< std::is_signed< T >::value, bool >::type; //C++14 - std::enable_if_t template< typename T > using IsSigned = std::enable_if_t< std::is_signed< T >::value, bool >; //C++17 - std::is_signed_v template< typename T > using IsSigned = std::enable_if_t< std::is_signed_v< T >, bool >; 

但是模板保持不变:

 template< typename T, IsSigned< T > = true > 

在较旧的C ++ 98中,没有模板别名,此外,功能模板不能具有类型或默认值。 我们可以将SFINAE代码插入结果类型或仅插入函数参数列表。 建议使用第二个选项,因为构造函数没有结果类型。 我们能做的最好的事情是这样的:

 template< typename T > T myAbs( T val, typename my_enable_if< my_is_signed< T >::value, bool >::type = true ) { return( ( val <= -1 ) ? -val : val ); } 

仅供比较-C ++的现代版本:

 template< typename T, IsSigned< T > = true > T myAbs( T val ) { return( ( val <= -1 ) ? -val : val ); } 

C ++ 98版本很丑陋,引入了无意义的参数,但是它可以工作-如果绝对必要,可以使用它。 是的:应该实现my_enable_ifmy_is_signedstd :: enable_if std :: is_signed在C ++ 11中是新的)。

当前状态


C ++ 17引入了if constexpr ,这是一种基于编译时的条件丢弃代码的方法。 if和else语句在语法上都必须正确,但是条件将在编译时进行评估。

 template< typename T > T myAbs( T val ) { if constexpr( std::is_signed_v< T > ) { return( ( val <= -1 ) ? -val : val ); } else { if constexpr( std::is_unsigned_v< T > ) { return val; } /*else { static_assert( false, "T must be signed or unsigned arithmetic type." ); }*/ } } 

正如我们所看到的,我们的abs功能变得更加紧凑和易于阅读。 但是,不合格类型的处理并不简单。 static_assert无条件static_assert使得该语句的一致性很差,无论该标准是否被丢弃,该语句都被标准禁止。

幸运的是,存在漏洞:在模板对象中,如果条件与值无关,则不会创建删除的运算符。 太好了!

因此,我们的代码唯一的问题是,它在模板定义期间崩溃。 如果我们可以将对static_assert的评估推迟到创建模板之前,那么问题将得到解决:当且仅当我们所有条件均为false时,才会创建该问题。 但是,如何在创建模板之前将static_assert推迟? 使它的条件取决于类型!

 template< typename > inline constexpr bool dependent_false_v{ false }; template< typename T > T myAbs( T val ) { if constexpr( std::is_signed_v< T > ) { return( ( val <= -1 ) ? -val : val ); } else { if constexpr( std::is_unsigned_v< T > ) { return val; } else { static_assert( dependent_false_v< T >, "Unsupported type" ); } } } 

关于未来


我们已经很接近了,但是我们需要等待一会儿,直到C ++ 20提出最终解决方案:概念! 这将完全改变模板(和SFINAE)的使用方式。

简而言之:概念可用于限制模板参数接受的参数集。 对于abs函数,我们可以使用以下概念:

 template< typename T > concept bool Arithmetic() { return std::is_arithmetic_v< T >; } 

以及我们如何使用概念? 有三种方法:

 //   template< typename T > requires Arithmetic< T >() T myAbs( T val ); //   template< Arithmetic T > T myAbs( T val ); //  Arithmetic myAbs( Arithmetic val ); 

请注意,第三种形式仍然声明了模板函数! 这是C ++ 20中myAb的完整实现:

 template< typename T > concept bool Arithmetic() { return std::is_arithmetic_v< T >; } Arithmetic myAbs( Arithmetic val ) { if constexpr( std::is_signed_v< decltype( val ) > ) { return( ( val <= -1 ) ? -val : val ); } else { return val; } } int main() { unsigned int a{ myAbs( 5u ) }; int b{ myAbs< int >( 5u ) }; //std::string c{ myAbs( "d" ) }; } 

注释掉的呼叫产生以下错误:

 error: cannot call function 'auto myAbs(auto:1) [with auto:1 = const char*]' constraints not satisfied within 'template<class T> concept bool Arithmetic() [with T = const char*]' concept bool Arithmetic(){ ^~~~~~~~~~ 'std::is_arithmetic_v' evaluated to false 

我敦促每个人在生产代码中大胆使用这些方法;编译时间比运行时便宜。 祝您生日快乐!

Source: https://habr.com/ru/post/zh-CN466915/


All Articles