为什么std :: set没有“包含”成员函数?


103

我使用率很高std::set<int>,通常只需要检查这样的集合是否包含数字即可。

我觉得写自然:

if (myset.contains(number))
   ...

但是由于缺少contains成员,我需要编写繁琐的代码:

if (myset.find(number) != myset.end())
  ..

还是不太明显:

if (myset.count(element) > 0) 
  ..

这个设计决定有理由吗?


7
大多数标准库都与迭代器一起使用,因此通常情况下,返回迭代器的函数就是您所期望的。虽然不难编写一个抽象函数。编译器很可能会内联它,因为它只能是一行或两行代码,并且您将获得相同的性能。
NathanOliver

3
count()方法的另一个(更基本的)问题是,它countains()要做的工作比要做的更多。
Leo Heinsaar

11
该设计决策背后的根本原因是,contains()返回a bool将会丢失有关元素在集合中的位置的有价值的信息find()保留并以迭代器的形式返回该信息,因此对于像STL这样的通用库来说是更好的选择。(尽管这并不是说a bool contains()并不是非常好用,甚至不是必要的。)
Leo Heinsaar

3
contains(set, element)使用集合的公共接口编写自由函数很容易。因此,该设备集的接口在功能上是完整的。添加便捷方法只会增加接口而不会启用任何其他功能,而这不是C ++的方式。
Toby Speight'3

3
这些天我们关门吗?这个问题如何以“主要观点为基础”?
Alien先生

Answers:


148

我认为这可能是因为他们试图使 std::setstd::multiset尽可能相似。(并且显然count对具有完全明智的意义std::multiset。)

我个人认为这是一个错误。

如果您假装这count只是拼写错误,那看起来还不错contains并将测试编写为:

if (myset.count(element)) 
   ...

不过还是很遗憾。


5
顺便提一下,它与maps和multimaps完全相同(虽然丑陋,但是比所有与的比较丑陋.end())。
Matteo Italia

8
或者,他们可能不需要额外的成员contains(),理由是这将是多余的,因为任何std::set<T> sT t的结果s.contains(t)都与的结果完全相同static_cast<bool>(s.count(t))。由于在条件表达式中使用该值会将其隐式转换为bool,因此他们可能觉得这样做count()足够好。
贾斯汀(Justin Time)-恢复莫妮卡

2
拼错?if (myset.ICanHaz(element)) ...:d
斯特凡纳·古里科

3
@MartinBonner忽略它的原因是否愚蠢并不重要。对话是否不是100%的最终依据也并不重要。在这里你的回答就是你如何认为它一个合理的猜测应该是。对话和来自某人的回答不仅涉及对话,而且提出提议的任务(即使他们没有参与)无疑也比这个猜测更接近真相,无论您如何看待它。最起码,您至少应该在答案中提及它,这将是一个很大的改进,并且是您应负责任的事情。
杰森C

2
@JasonC:您可以继续编辑底部的部分吗?我不太了解您要提出的观点,评论可能不是阐明这一点的最佳方法。谢谢!
Martin Bonner

44

为了能够编写if (s.contains())contains()必须像返回一样返回一个bool(或可转换为的类型bool,这是另一个故事)binary_search

设计决策这样做的根本原因是,返回a 将会丢失有关元素在集合中的位置的有价值的信息。保留并以迭代器的形式返回该信息,因此对于像STL这样的通用库是更好的选择。正如他经常解释的那样,这一直是Alex Stepanov的指导原则(例如,在此处)。contains()boolfind()

至于count()一般的方法,尽管通常这是一个好的解决方法,但问题在于 contains() 要做的工作比要做的要多

这并不是说a bool contains()并不是非常好用,甚至不是必需的。不久前,我们在ISO C ++标准-未来提案小组中就这个问题进行了长时间的讨论


5
有趣的是,在讨论结束时,人们几乎一致认为它是可取的,并且要求为此编写建议。
PJTraill'3

@PJTraill是的,我没有继续前进的原因是contains(),显然,它会与现有的容器和算法进行强烈的交互,这将受到概念和范围的强烈影响(预计在C ++ 17中会出现),以及我确信(作为讨论的结果,以及两次私人电子邮件交换的结果),首先等待他们是一个更好的主意。当然,在2015年,尚不清楚概念和范围都不会使其融入C ++ 17中(事实上,人们寄予了很高的希望)。不过,我不确定现在是否值得追求。
Leo Heinsaar

1
对于std::set(这是这个问题要问的问题),我看不出有多少count工作contains要做。的glibc实现count是(大致)return find(value) == end() ? 0 : 1;。除了有关三元运算符!= end()而不是返回(我希望优化器删除)的详细信息之外,我看不到还有更多工作要做。
马丁·邦纳

4
“ ... 返回一个布尔值的contains()将丢失有关该元素在集合中的位置的有价值的信息 ”-如果用户调用myset.contains()(如果存在),则将非常有力地表明该信息没有价值(给用户)。
基思·汤普森

1
为什么count()要做的工作比contains()要做的要多std::set?它是唯一的,因此count()可能return contains(x) ? 1 : 0;完全相同。
Timmmm '17

22

它缺少它,因为没有人添加它。没人添加它是因为std库中合并了STL的容器,这些容器被设计为最小化接口。(请注意,它std::string不是以相同的方式来自STL)。

如果您不介意一些奇怪的语法,则可以伪造它:

template<class K>
struct contains_t {
  K&& k;
  template<class C>
  friend bool operator->*( C&& c, contains_t&& ) {
    auto range = std::forward<C>(c).equal_range(std::forward<K>(k));
    return range.first != range.second;
    // faster than:
    // return std::forward<C>(c).count( std::forward<K>(k) ) != 0;
    // for multi-meows with lots of duplicates
  }
};
template<class K>
containts_t<K> contains( K&& k ) {
  return {std::forward<K>(k)};
}

用:

if (some_set->*contains(some_element)) {
}

基本上,您可以std使用此技术为大多数C ++ 类型编写扩展方法。

这样做更有意义:

if (some_set.count(some_element)) {
}

但是我对扩展方法感到很高兴。

真正可悲的是,containsmultimap或上编写高效文件可能会更快multiset,因为他们只需要查找一个元素,而count必须查找每个元素并计算它们

包含10亿个7的副本的多集(如果用尽了,您知道)可能会非常慢.count(7),但可能会非常快contains(7)

使用上述扩展方法,通过使用lower_bound,与end,然后与元素进行比较,我们可以针对这种情况更快地进行处理。但是,对于无序喵声和有序喵声执行此操作将需要花哨的SFINAE或特定于容器的重载。


2
10亿份7?我认为这里std::set不能包含重复项,因此std::set::count将始终返回01
nwp

5
@nwp std::multiset::count
milleniumbug

2
@nwp我缺少backticks“ set”一词,是因为我没有std::set具体提及。为了让您感觉更好,我将添加多点
Yakk-Adam Nevraumont

3
我似乎错过了关于“喵”应该被提及的笑话。
user2357112支持Monica's

2
@ user2357112 meow是“集合或地图”的占位符。询问STL为什么。
Yakk-Adam Nevraumont

12

您正在调查特殊情况,而没有看到更大的图景。如文档 所述,std::set满足AssociativeContainer概念的要求。对于该概念,使用方法没有任何意义contains,因为它对于std::multiset和几乎没有用std::multimap,但count对所有方法都适用。虽然方法contains可以被添加为一个别名countstd::setstd::map并且其散列版本(如length用于size()std::string),但看起来像库创建者没有看到它真正的需要。


8
请注意,这string是一个庞然大物:它存在于STL之前,在这里它具有length所有基于索引的方法,然后被“容器化”以适合STL模型...由于向后兼容的原因而没有删除现有方法。参见GotW#84:Monoliths Unstrung => string确实违反了“最小数量的成员函数”设计原则。
Matthieu M.

5
但是问题就变成了“为什么有这样一个AssociativeContainer概念值得吗?” -我不确定这是事后看来。
Martin Bonner

24
询问多集,多图或地图是否包含某些内容对我来说很有意义。实际上,contains在集合/地图上的工作量是相等的,但可以比在多集合/多地图上更快count
Yakk-Adam Nevraumont

5
AssociativeContainer不需要类不是有一个contains方法。
user2357112支持Monica's

6
@Slava就像是在说,size()并且empty()是重复的,但是很多容器都兼有。
巴里(Barry)

10

尽管我不知道为什么std::set没有,contains但是count只有一次返回01,您可以编写一个模板化的contains帮助函数,如下所示:

template<class Container, class T>
auto contains(const Container& v, const T& x)
-> decltype(v.find(x) != v.end())
{
    return v.find(x) != v.end();
}

并像这样使用它:

    if (contains(myset, element)) ...

3
-1,因为与该contains方法确实存在这一事实直接矛盾,因此它只是以一种愚蠢的方式命名。
Matteo Italia

4
咳嗽 std::string 咳嗽
bolov

6
@bolov:你的意思是?std.::string不属于STL!它是标准库的一部分,并且具有追溯性的模板...
MFH,2017年

3
@MatteoItalia count可能会变慢,因为find如果与共享代码,则它实际上需要做两秒才能得到范围的开始和结束multiset
Mark Ransom'3

2
OP已经知道它是多余的,但显然希望代码被显式读取contains。我认为这没有错。@MarkRansom是SFINAE的小功能,它可以防止此模板绑定到不应绑定的内容。
rustyx

7

真正的原因对set我来说是个谜,但是对此相同设计的一种可能解释map可能是防止人们偶然编写低效率的代码:

if (myMap.contains("Meaning of universe"))
{
    myMap["Meaning of universe"] = 42;
}

这将导致两个 map查找。

相反,您被迫获得一个迭代器。这给您一个心理提示,您应该重用迭代器:

auto position = myMap.find("Meaning of universe");
if (position != myMap.cend())
{
    position->second = 42;
}

仅消耗一次map查找。

当我们意识到set并且map由相同的肉制成时,我们也可以将这一原理应用于set。也就是说,如果我们只想对set存在于中的项目进行操作set,则此设计可以防止我们这样编写代码:

struct Dog
{
    std::string name;
    void bark();
}

operator <(Dog left, Dog right)
{
    return left.name < right.name;
}

std::set<Dog> dogs;
...
if (dogs.contain("Husky"))
{
    dogs.find("Husky")->bark();
}

当然,所有这些仅仅是推测。


1
是的,但不适用于整数集。
Jabberwocky

7
除了人们写得if (myMap.count("Meaning of universe"))还好,所以...?
巴里(Barry)

@MichaelWalz糟糕,您是对的。我修改了答案,以包括示例。但是,对于一组整数的推理对我来说还是个谜。
Martin Drozdik '17

2
这是不对的。他们可以很方便地与你写的代码效率低下containscount
Martin Bonner

2

从c ++ 20开始,

bool contains( const Key& key ) const

可用。


0

那binary_search呢?

 set <int> set1;
 set1.insert(10);
 set1.insert(40);
 set1.insert(30);
 if(std::binary_search(set1.begin(),set1.end(),30))
     bool found=true;

它不适用于std::unordered_set,但std::set可以。
Jabberwocky

正常,binary_search仅适用于二叉树。
Massimiliano Di Cavio

0

contains()必须返回布尔值。使用C ++ 20编译器,我得到以下代码输出:

#include<iostream>
#include<map>
using namespace std;

int main()
{
    multimap<char,int>mulmap;
    mulmap.insert(make_pair('a', 1)); //multiple similar key
    mulmap.insert(make_pair('a', 2)); //multiple similar key
    mulmap.insert(make_pair('a', 3)); //multiple similar key
    mulmap.insert(make_pair('b', 3));
    mulmap.insert({'a',4});
    mulmap.insert(pair<char,int>('a', 4));
    
    cout<<mulmap.contains('c')<<endl;  //Output:0 as it doesn't exist
    cout<<mulmap.contains('b')<<endl;  //Output:1 as it exist
}

-1

另一个原因是,它会给程序员带来错误的印象,即std :: set是数学集合理论意义上的集合。如果他们实现了这一点,那么还会有许多其他问题:如果std :: set包含一个值的contains(),为什么它不包含另一个值呢?union(),intersection()和其他集合操作和谓词在哪里?

答案是,当然,某些设置操作已经在(std :: set_union()等)中实现为函数,而其他一些操作已与contains()一样简单地实现。函数和函数对象在数学抽象上比对象成员更好地工作,并且它们不限于特定的容器类型。

如果需要实现完整的数学集功能,那么他不仅可以选择底层容器,还可以选择实现细节,例如,他的theory_union()函数可用于不可变对象,更适合于函数式编程,还是会修改其操作数并节省内存?是从一开始就将其实现为函数对象,还是最好实现为C函数,并在需要时使用std :: function <>?

到目前为止,std :: set只是一个容器,非常适合数学意义上set的实现,但它与std :: vector相比,与理论向量几乎没有理论上的距离。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.