【问题标题】:How can I use SFINAE to prevent narrowing in a template function?如何使用 SFINAE 来防止模板函数变窄?
【发布时间】:2017-06-07 20:31:06
【问题描述】:

我正在尝试为 Vector 类实现基本算术运算,并希望支持混合基础类型,同时防止发生缩小。

template <typename T1,typename T2>
Vector<T1> operator+( Vector<T1> lhs, const Vector<T2>& rhs, std::enable_if< ! is_narrowing_conversion<T2,T1>::value >::type* = nullptr )
{ return lhs += rhs; }

我想实现 is_narrowing_conversion 以便仅在类型不缩小时才允许转换。以下是一些示例:

Vector<double> a = Vector<double>() + Vector<float>(); //OK
Vector<float> a = Vector<float> + Vector<double>; //Fails to find the operator+ function

最后,我想编写第二个模板 operator+ 函数,该函数将通过返回一个 Vector 来处理相反的情况。

我发现这篇文章with an incomplete example。但这还不够,因为他指出它不允许 uint8_t 到 uint64_t 转换。

我还发现了Daniel Krügler's paper on fixing is_constructible。特别是在这篇论文中,他提到了使用列表初始化,它已经具有窄化语义,但我不确定如何将他提到的内容转换为可用于 SFINAE 模板推导的适当特征。

【问题讨论】:

  • 我想我应该注意,我已经更新了您链接到上面的帖子作为“不完整示例”,其版本现在应该适用于 uint8_t --> uint64_t 等

标签: c++ c++11 sfinae narrowing


【解决方案1】:

通过std::common_type 使用 SFINAE 怎么样?

下面是一个简化的例子(不是Vector,而是简单的值;不是operator+(),而是sum()函数),但我希望你能明白我的意思

#include <iostream>
#include <type_traits>


template <typename T1, typename T2>
T1 sum (T1 t1,
        T2 const & t2,
        typename std::enable_if<std::is_same<
              T1,
              typename std::common_type<T1, T2>::type
           >::value>::type * = nullptr)
 { return t1 += t2; }

int main()
 {
   float  a      = 1.1f;
   double b      = 2.2;
   long double c = 3.3;

   std::cout << sum(b, a) << std::endl;
   std::cout << sum(c, a) << std::endl;
   std::cout << sum(c, b) << std::endl;
   // std::cout << sum(a, b) << std::endl; compilation error
   // std::cout << sum(a, c) << std::endl; compilation error
   // std::cout << sum(b, c) << std::endl; compilation error
 }

【讨论】:

  • 谢谢,这正是我想要的。我能够实现这个函数和反函数(使用std::is_same&lt;T2, typename std::common_type&lt;T1, T2&gt;::type&gt;),并让模板推导选择正确的实现来使用。需要注意的一件事是,调用sum(b, b) 的两个求和函数最终会产生模棱两可的重载决议。因为 T1 和 T2 是相同的类型,所以两个模板化函数都通过了重载决议。这是一个可以接受的限制,因为我可以轻松地在类型相同的情况下使用模板特化,并针对每种情况进行优化。
  • 另外,我可以通过 ! std::is_same&lt;T1, typename std::common_type&lt;T1, T2&gt;::type&gt; 来实现第二个函数,这样可以避免模棱两可的重载决议,而无需添加第三个模板特化。
【解决方案2】:

Vector 有一些假设,但你应该明白:

template <typename T1,typename T2>
Vector<typename std::common_type<T1, T2>::type> 
    operator+(Vector<T1> const& lhs, Vector<T2> const& rhs)
{ 
    std::size_t const n = std::min(lhs.size(), rhs.size());
    Vector<typename std::common_type<T1, T2>::type> res(n);
    for(std::size_t i{}; i < n; ++i) res[i] = a[i] + b[i];
    return res; 
}

Vector<double> a = Vector<double>() + Vector<float>(); // OK
Vector<double> b = Vector<float>() + Vector<double>(); // OK

【讨论】:

  • 感谢您的回复。我喜欢这个解决方案将我计划编写的所有不同的 operator+ 功能组合到一个实现中,从而使软件易于维护。但是,@max66 使用 SFINAE 回答了我原来的问题,所以我接受了他的回答。
  • 没问题。我只是认为让a + b 工作而b + a 不编译会令人惊讶。我的解决方案表现得更“自然”/预期 imo。
  • 我同意,我更愿意使用这个解决方案来进行模板推演。但是,我正在使用的库以一种晦涩的方式设计,Vector 派生自相同类型的模板类:template &lt;typename T&gt; class Policytemplate &lt;typename T, class B&gt; class Vector : public B 调用如下:Vector&lt;float,Policy&lt;float&gt; &gt; 我无法使用std::common_type 在一次返回中解析类型和基类类型。这就是阻止我使用您的解决方案的原因。但这是与我提出的问题不同的问题。
【解决方案3】:

您可以使用{} 构造函数,让它为您检测narrowing conversions
你可以这样做:

template<typename T>
struct Vector {
    T value;
};

template <typename T1, typename T2>
auto operator+(Vector<T1>, const Vector<T2>& rhs)
-> decltype(T1{rhs.value}, Vector<T1>{})
{ return {}; }

int main() {
    auto a = Vector<double>{} + Vector<float>{};
    //auto b = Vector<float>{} + Vector<double>{};
    (void)a;
}

也就是说,对于涉及尾随返回类型的 sfinae 表达式,您可以使用通常的形式。
如果您的 Vector 类模板没有默认构造函数,您仍然可以使用 std::declval 解决它:

-> decltype(T1{rhs.value}, std::declval<Vector<T1>>())

请注意,由于[dcl.init.list]/7.2 和后面的其他项目符号,您不能简单地在上面的代码中执行此操作:

-> decltype(T1{T2{}}, Vector<T1>{})

否则,在您的具体示例中,以下内容将是有效的:

auto b = Vector<float>{} + Vector<double>{};

因此,您必须使用与 rhs 一起提供的实际值,并且(让我说)测试它 使用用于专门化 lhs 的实际类型。
只要包含的值是可访问的(它是有效的或者操作员是您班级的朋友),这应该不是问题。


附带说明,您无法通过仅使用 T1T2 类型正确定义在您的情况下正常工作的 is_narrowing_conversion
例如,考虑[dcl.init.list]/7.2(强调我的)和您提出的测试代码:

[...] 或者从double到float,除非源是常量表达式并且转换后的实际值在可以表示的值范围内(即使不能准确表示) [...]

因为您没有用于测试的实际值,您所能做的就是尝试像T1{T2{}} 这样的东西。无论如何,这是行不通的。换句话说,由于上面提到的项目符号,float{double{}} 将被正确接受。
类似的情况也适用于少数其他类型。


另一种有效的方法是使用std::common_type
无论如何,已经有一个pretty good answer 提出了它。不值得再重复一遍示例代码。

【讨论】:

  • 我实现了{} 构造函数方法,发现它正确地阻止了我的operator+ 发生变窄。随后我使用 'decltype(T2{lhs.value}, Vector{})` 添加了反向实现,并意识到重载决议是不明确的。本质上,这两个函数仅在返回类型上有所不同,这并不等于不同的函数签名。对我来说,最好按照所有三个答案的建议,使用 std::common_type 在单个函数中实现我的 operator+,而不是尝试使用两个单独的模板函数来实现。
  • @MikeC。实现反函数没有多大意义。您只需在调用点切换参数即可获得相同的结果。
  • 这对于加法运算符是正确的,但对于除法或减法等其他运算符则不是这样,对于具有这两种功能的非交换运算符来说是理想的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-09-16
  • 2019-01-15
  • 1970-01-01
相关资源
最近更新 更多