如何使用 SFINAE 来防止模板函数变窄?

How can I use SFINAE to prevent narrowing in a template function?

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

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 来处理相反的情况。

我找到了这个 post with an incomplete example。但这还不够,因为他指出它不允许 uint8_t 到 uint64_t 转换。

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

通过 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
 }

有一些关于 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

您可以使用 {} 构造函数并让它为您检测 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;
}

也就是说,对于涉及尾随 return 类型的 sfinae 表达式,您可以使用通常的形式。
如果您的 Vector class 模板没有默认构造函数,您仍然可以使用 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 的实际类型对其进行测试 .
只要包含的值是可访问的(它是有效的或者操作员是您的 class 的朋友)这应该不是问题。


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

[...] or from double to float, except where the source is a constant expression and the actual value after conversion is within the range of values that can be represented (even if it cannot be represented exactly) [...]

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


另一个有效的方法是使用 std::common_type.
反正已经有提出来了。不值得再次重复示例代码。