我们可能想让我们的add()函数模板更加灵活:
- 可能还想支持只提供insert()而不是push back()来插入新元素的集合。
- 可能想要支持传递一个集合(容器或范围)来插入多个值。
你可以说这些是不同的函数,它们应该有不同的名称,如果这样做有效的话,使用不同的名称通常会更好。
然而,c++标准库是一个很好的例子,说明了如果不同的API得到协调,将会带来哪些好处。
例如,您可以使用相同的泛型代码
调用不同的成员函数
要在函数实现中使用push back()和insert()之间切换,有几个选项。有两个问题需要解决:
- 必须检测是否支持push back()或insert()。
- 必须确保只编译受支持的调用。
因为切换发生在函数内部,所以我们可以使用c++ 17引入的if constexpr特性。我们甚至可以直接使用require表达式作为它的条件
if constexpr (requires { coll.push_back(val); }) {
coll.push_back(val);
}
else {
coll.insert(val);
}
另一种选择是引入一个名称来支持push back()和insert()。
以定义一个bool类型的变量模板(就像类型trait一样)
template<typename T>
inline constexpr bool SupportsPushBack = requires(T coll) {
col1.push_back(std::declval<typename T::value_type>());
};
我们可以定义一个概念
template<typename T>
concept SupportsPushBack = requires(T coll) {
co11.push_back(std::declval<typename T::value_type>());
};
在这两种情况下,我们都只使用一个模板参数,尝试插入容器的元素类型的对象。在这里,您可以看到概念和需求的定义并不会创建代码。
这是一个未求值的上下文,我们可以使用std::dec1val<>()来假设我们将有一个这种类型的对象,无论我们将coll声明为值还是非const引用都无关紧要。
不过,让我们看看这里在技术上是可行的。
我们可以在if constexpr条件中使用任何形式的命名需求
if constexpr (SupportsPushBack<col1>)
{
coll.push_back(val);
}
else{
coll.insert(val);
}
另一种选择是重载两个实现
template<typename Coll, typename T>
requires SupportsPushBack<Coll>
void add(Coll& coll, const T& val) {
coll.push_back(val);
}
template<typename Coll, typename T>
void add(Coll& coll, const T& val)
{
coll.insert(val);
}
使用require子句,这既适用于概念,也适用于名为boo1的对象。
注意,我们在这里不需要命名需求SupportsInsert,因为附加需求的add()更特殊,因此重载解析更倾向于它。
如果我们将命名需求定义为一个概念,我们甚至可以将其作为一个受约束的模板参数使用
template<SupportsPushBack Coll,typenameT>
void add(Coll& coll,const T& val)
{
coll.push_back(val);
}
如果一个已命名的需求被定义为一个变量模板,这是不可能的。
在 C + + 20之前,我们必须使用复杂的声明来“ SFINAE”去掉不适合的模板。
插入单个和多个值
为了提供一个重载来处理作为一个集合传递的多个值,我们可以简单地为它们添加 con- straints。标准概念 std::ranges::input_range 可用于此:
template<PushBackContainer Coll, std::ranges::input_range T>
void add(Coll& col1, const T& val)
{
coll.insert(coll.end(),val.begin(),val.end());
}
template<InsertContainer Coll, std::ranges::input_range T>
void add(Coll& coll, const T& val)
{
coll.insert(val.begin(),val.end());
}
同样,只要重载有这一附加约束,这些函数将是首选的。
std::ranges::input range概念是用来处理范围的,它是可以用begin()和end()迭代的集合。但是,range不需要begin()和end()作为成员函数。
因此,处理范围的代码应该使用范围库提供的帮助程序std::ranges::begin()和std::ranges::end()
template<PushBackContainer Coll, std::ranges::input_range T>
void add(Coll& col1,const T& val)
{
coll.insert(coll.end(),std::ranges::begin(val), std::ranges::end(val));
}
template<InsertContainer Coll, std::ranges::input_range T>
void add(Coll& coll, const T& val)
{
coll.insert(std::ranges::begin(val), std::ranges::end(val));
}
这些帮助程序实际上是函数对象,避免了 ADL 问题。
处理多重约束
通过将所有有用的概念和需求组合在一起,我们可以将它们都放在不同位置的一个函数中。
template<PushBackContainer Coll, std::ranges::input_range T>
requires ConvertsWithoutNarrowing<std::ranges::range_value_t<T>,
typename Coll::value_type>
void add(Coll& coll,const T& val)
{
coll.insert(coll.end(),
std::ranges::begin(val), std::ranges::end(val));
}
为了禁用缩小范围的转换,我们使用std::ranges::range value t将范围的元素类型传递给convertswithoutnarrow。
Std::ranges::range value t是另一个range实用程序,用于在迭代范围时获取它们的元素类型。我们也可以在require子句中把它们组合在一起
template<typename Coll,typename T>
requires PushBackContainer<Coll> &&
std::ranges::input_range<T> &&
ConvertsWithoutNarrowing<std::ranges::range_value_t<T>,
typename Coll::value_type>
void add(Coll& col1, const T& val)
{
coll.insert(coll.end(),
std::ranges::begin(val), std::ranges::end(val));
}
声明函数模板的两种方法是等效的。
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 N 天,点击查看活动详情”