c++ 禁用通用代码的要求

367 阅读5分钟

禁用通用代码的要求

假设我们想为add()函数模板提供一个特殊的实现

在处理浮点值时,应该发生一些不同的或额外的事情。一种简单的方法可能是为double重载函数模板

template<typename Coll, typename T>

void add(Coll& coll, const T& val)

//对于泛型值类型
{

coll.push_back(val); 

}

template<typename Coll>

void add(Coll& coll,double val)

// 对于触发点值类型

{

..//special stuff for flouting-point values coll.push_back(val);

}

正如预期的那样,当传递double作为第二个参数时,将调用第二个函数;否则,将使用泛型参数

std::vector<int> iVec; add(iVec,42);

//OK: culls add ()表示 T 为 int

std::vector<double> dVec;

add(dVec,0.7);

// OK:为double调用第二个add()

当传递double时,两个函数重载都匹配。第二个重载是首选的,因为它与第二个参数完全匹配。

然而,如果我们传递一个浮点数,我们有以下效果:

float f=0.7;

add(dVec,f);

// OOPS:剔除 Ist add() for T 是浮点数

原因在于有时重载解析的微妙细节。同样,两个函数都可以被调用。重载解析有一些通用规则,例如:

  • 没有类型转换比有类型转换更好。

  • 没有模板参数比模板参数更好。

但是,在这里,重载解析必须在类型转换和使用模板形参之间做出决定。不幸的是,首选带有template参数的版本

修复过载解决方案

修复错误的重载分辨率非常简单。

我们应该只要求要插入的值具有浮点类型,而不是用特定类型声明第二个形参。

为此,我们可以使用新的标准概念 std::float point来约束函数模板

template<typename Coll, typename T>

requires std::floating_point<T>


void add(Coll& col1, const T& val)

{

...

//special stuff for flouting-point values

coll.push_back(val);

}

因为我们使用了一个适用于单个模板参数的概念,所以我们也可以使用简写标记

template<typename Coll, std::floating_point T> 

void add(Coll& coll, const T& val)

{

....//special stuff for flouting-point values 

coll.push_back(val);

}

对于浮点值,我们现在有两个可以调用的函数模板,一个没有,一个有特定的要求

template<typename Co11,typename T> 

void add(Coll& coll, const T& val)

// for generic value types 
{

co11.push_back(val); 
}

template<typename Coll, std::floating_point T>

void add(Coll& coll, const T& val)

//for flouting-point value types

{

//special stuff for flouting-point values

coll.push_back(val);

}

这已经足够了,因为重载解析还更喜欢具有约束的重载或专用化,而不是具有较少或没有约束的重载或专用化:

std::vector<int> iVec; add(iVec,42);

//OK:culls add()用于泛型值类型

std::vector<double> dVec;

add(dVec,0.7);

//OK:culls add()用于浮点类型

不同签名的限制

如果两个重载或专门化有约束,重载解析能够决定哪个更好是很重要的。

为此,重载函数必须具有相同的签名。如果签名不同,则不首选约束更强的重载。

例如,如果我们声明浮点值的重载以按值获取参数,则传递浮点值将变得模糊

template<typename Coll,typename T>

void add(Co11& col1,const T& val) //注意: 通过常量引用传递

{

coll.push_back(val); 

}

template<typename Coll, std::floating_point T> 

void add(Col1& coll,T val)

//注意:按值传递

{

...  

coll.push_back(val);

}

std::vector<double> dVec; 

add(dVec,0.7);

//错误:两个模板都匹配且没有首选项

后一种声明不再是前一种声明的特例。我们只有两个不同的函数模板,它们都可以被调用。

如果真的想要使用不同的签名,则必须限制第一个函数模板不能用于浮点值。

缩小范围的限制

这里还有另一个有趣的问题:两个函数模板都允许传递double类型,将其添加到int类型的集合中

std::vector<int> iVec; add(dVec,1.9);

//OOPS:add 1

原因是我们有从double到int的隐式类型转换(由于与编程语言C兼容)。

这种可能会丢失部分值的隐式转换称为缩窄。

这意味着上面的代码在插入1.9之前编译并将值转换为1。

如果不想支持缩窄,有多个选项:

  • 一个选项是通过要求传递的值类型与集合的元素类型匹配来完全禁用类型转换
requires std::same_as<typename Coll::value_type,T>

但是,这也会禁用有用且安全的类型转换

出于这个原因,最好定义一个概念,该概念产生一个类型是否可以在不缩小的情况下转换为另一个类型,这在一个简短而棘手的需求中是可能的

template<typename From, typename To>

concept ConvertsWithoutNarrowing=

std::convertible_to<From,To> &&

requires (From&& x)

{

{ std::type_identity_t<To[]>{std::forward<From>(x)}}

->std::same_as<To[1]>;

};

然后我们可以用这个概念来表述一个相应的约束条件

template<typename Coll,typename T>

requires ConvertsWithoutNarrowing<T, typename Col1::value_type>

void add(Coll& coll, const T& val)

{

...

}

运用约束

在不需要std::convertible to概念的情况下定义窄化转换的概念可能就足够了,因为其余部分隐式地检查了这一点

template<typename From, typename To>

concept ConvertsWithoutNarrowing = requires (From&& x) {

{ std::type_identity_t<To[]>{std: :forward<From>(x)} } →> std: :same_as<To[1]>; };

然而,如果convertswithoutnarrow概念也检查std::convertible to概念,则有一个重要的好处。

在这种情况下,编译器可以检测到convertswithoutnarrow比std::convertible to更有约束。

术语是convertswithoutnarrow包含std::convertible to。

这允许程序员执行以下操作

template<typename F, typename T>

  


requires std::convertible_to<F,T>

  


void foo(F,T)

{

std::cout << "may be narrowing\n";

}

template<typename F, typename T>

requires ConvertsWithoutNarrowing<F, T>

void foo(F,T)

{

std::cout <<"without narrowing\n";

}

如果没有指定convertswithoutnarrow包含std::convertible to,编译器在调用foo()时,在没有窄化的情况下,两个形参相互转换时,会引发歧义错误。


开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 N 天,点击查看活动详情