c++ 如何使add()函数更加灵活

374 阅读4分钟

我们可能想让我们的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 天,点击查看活动详情