SFINAE区分有符号和无符号类型

8

我有转换不同算术类型为半精度浮点类型(仅在最低级别上使用uint16_t)的函数,我还有针对整数和浮点源类型的不同函数,使用SFINAE和std::enable_if

template<typename T>
uint16_t to_half(typename std::enable_if<
                 std::is_floating_point<T>::value,T>::type value)
{
    //float to half conversion
}

template<typename T>
uint16_t to_half(typename std::enable_if<
                 std::is_integral<T>::value,T>::type value)
{
    //int to half conversion
}

通过显式实例化,这些函数从通用模板构造函数内部调用:

template<typename T>
half::half(T rhs)
    : data_(detail::conversion::to_half<T>(rhs))
{
}

这段代码可以编译并且运行良好。现在我尝试区分有符号整数和无符号整数,通过用两个函数替换第二个函数:

template<typename T>
uint16_t to_half(typename std::enable_if<std::is_integral<T>::value &&
                 std::is_signed<T>::value,T>::type value)
{
    //signed to half conversion
}

template<typename T>
uint16_t to_half(typename std::enable_if<std::is_integral<T>::value &&
                 std::is_unsigned<T>::value,T>::type value)
{
    //unsigned to half conversion
}

但是,一旦我尝试编译这个,VS2010就会给出以下错误: 错误 C2995:"uint16_t math::detail::conversion::to_half(std::enable_if::value && std::tr1::is_signed<_Ty>::value,T>::type)": 函数模板已经定义。 所以它似乎不能区分两个模板,但显然在浮点版本中与整数版本并存没有问题。 但由于我不是那么擅长模板这方面的魔法,我可能会错过一些明显的东西(或者它实际上应该工作,只是VS2010的错误)。那么为什么无法正常工作,如何在尽可能少的编程开销和纯标准功能的限制下使其正常工作(如果有可能)?

1
不清楚 is_signed/is_unsigned 是否互斥(你好,char?)。尝试让第二个版本改为 !std::is_signed<T>::value - Kerrek SB
你能否尝试在其中一个成员中使用 std::is_signed<T>::value,在另一个成员中使用 !std::is_signed<T>::value 吗?这只是为了确保没有某种类型的 is_signedis_unsigned 设置不一致。 - Dietmar Kühl
@KerrekSB 和 Dietmar Hah,搞定了!难以相信它是如此简单。如果有人将其作为答案添加,我会接受它。 - Christian Rau
@Kerrek char既不是有符号整数类型也不是无符号整数类型。但是我记得is_signedis_unsigned会处理这个问题:只有其中一个会报告chartrue - Johannes Schaub - litb
@JohannesSchaub-litb: 你说得对,char 是没问题的。不过,枚举和指针始终是错误的,我猜是因为它们不是算术类型。 - Kerrek SB
3个回答

8

个人而言,我会尽量避免在此处使用SFINAE,因为您可以通过重载来实现相同的功能:

template<typename T>
uint16_t to_half_impl(T val, std::true_type, std::true_type)
{
    // is_integral + is_signed implementation
}

template<typename T>
uint16_t to_half_impl(T val, std::true_type, std::false_type)
{
    // is_integral + is_unsigned implementation
}

template<typename T>
uint16_t to_half_impl(T val, std::false_type, std::true_type)
{
    // is_floating_point implementation
}

template<typename T>
typename std::enable_if<std::is_arithmetic<T>::value, uint16_t>::type to_half(T val)
{
    return to_half_impl(val, std::is_integral<T>(), std::is_signed<T>());
}

+1 不错的替代方案。但是顺便说一下,我认为浮点版本的最后一个参数应该是 std::true_type,因为浮点数总是有符号的(至少通常实现是这样的,对于非IEEE的转换代码无论如何都不会起作用)。 - Christian Rau
@Christian:你完全正确;我基于MSDN文档中的is_signed逻辑,但这些文档实际上是错误的(惊喜吧)。已修复。 - ildjarn

3

如果这没有起作用,那么您的编译器存在错误。

涉及模板参数的两个表达式如果包含在两个函数定义中,这些定义将满足一个定义规则,则它们被视为等效的...

这里最重要的规则是(省略“...”的细节):您的两个模板不满足 ODR,因为它们的记号序列不同。

如果两个函数模板在相同的作用域中声明,具有相同的名称,具有完全相同的模板参数列表,并且返回类型和参数列表使用上述比较涉及模板参数的表达式的规则是等效的,则它们是等效的。

因此,您的两个模板定义了不同的模板并且不冲突。现在,您可以检查您的模板是否“功能等效”。如果对于任何可能的模板参数集,您的 enable_if 表达式始终生成相同的值,那么它们就是等效的。但由于对于 is_unsignedis_signed 来说这不是真的,所以这也不是真的。如果是这样的话,您的代码将是非法的,但不需要诊断(这实际上意味着“未定义行为”)。


!is_signed(或者相反)代替is_unsigned可以解决问题,所以我猜想(尽管我对语言规范的深层次内容并不熟悉,更不用说模板了),有一种类型同时被视为带符号和无符号。或者是因为这些模板的默认版本对于非算术类型都会评估为 false ?但再考虑到也有 is_integral 来消除歧义(对于整数类型应该是互斥的,不是吗?)。 - Christian Rau
@Christian 我不知道他们在干什么,但那明显是错误的行为。尝试使用!!is_unsigned代替!is_signed。我甚至不会惊讶地看到它“工作”了 :) - Johannes Schaub - litb
哈哈,这也行。现在我觉得有点荒谬了。你可能是对的,编译器在这里出了问题。 - Christian Rau
如果两个包含模板参数的表达式在两个函数定义中被使用时,这两个函数定义满足“一次定义规则”,那么这两个表达式被认为是等价的。你需要在你的帖子中添加一个例子来进一步解释吗? - Prasoon Saurav
你的答案只有一半是正确的。编译器拒绝代码是正确的(请参见下面的我的答案),但它给出了错误的原因。 - Walter
显示剩余2条评论

1
常见的习惯用法是在返回类型上使用SFINAE,而不是参数类型。否则,模板类型T可能无法推导出来。
// from C++14
template<bool C, typename T> using enable_if_t = typename std::enable_if<C,T>::type;

template<typename T>
enable_if_t<std::is_integral<T>::value &&  std::is_signed<T>::value, uint16_t>
to_half(T value)
{
    //signed to half conversion
}

template<typename T>
enable_if_t<std::is_integral<T>::value && !std::is_signed<T>::value,  int16_t>
to_half(T value)
{
    //unsigned to half conversion
}

下面这个语句中的类型 T
auto y=to_half(x);    // T is deduced from argument, no need for <T> 

即使是显而易见的,也可以推导出来,但对于您的原始代码来说不是这样!事实上,通过clang运行您的to_half()实现时会得到以下结果。
test.cc:24:11: error: no matching function for call to 'to_half'
  auto x= to_half(4);
          ^~~~~~~
test.cc:7:10: note: candidate template ignored: couldn't infer template argument 'T'
uint16_t to_half(typename std::enable_if<std::is_integral<T>::value &&
         ^
test.cc:15:10: note: candidate template ignored: couldn't infer template argument 'T'
uint16_t to_half(typename std::enable_if<std::is_integral<T>::value &&
         ^

当然,如果你明确提供模板参数(就像你所做的那样),这个问题就不会出现。所以你的代码并没有错(但编译器错了),但是如果你传递模板参数类型,SFINAE有什么意义呢?

这个是否会有与参数类型中的SFINAE一样的歧义问题?有什么规则使得这个可以工作而参数版本不行吗?否则它似乎并没有解决问题。 - Christian Rau
在参数类型上不使用SFINAE有一个非常好的理由,详见编辑后的答案。 - Walter

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接