为什么 std::set 没有 "contains" 成员函数?

Why does std::set not have a "contains" member function?

我经常使用 std::set<int>,而且我经常只需要检查这样的集合是否包含数字。

我觉得写起来很自然:

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

但是因为缺少contains成员,所以需要写的比较繁琐:

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

或不那么明显:

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

这个设计决定有原因吗?

我认为这可能是因为他们试图让 std::setstd::multiset 尽可能相似。 (显然 countstd::multiset 具有完全合理的含义。)

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

如果你假装 count 只是 contains 的拼写错误并将测试写成:

,它看起来并没有那么糟糕
if (myset.count(element)) 
   ...

不过还是很遗憾。

虽然我不知道为什么 std::set 没有 containscount 只有 returns 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)) ...

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 查找。

当我们意识到setmap是同一个肉体时,我们也可以将这个原理应用到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();
}

当然这一切都只是猜测。

您正在调查特定案例,而没有看到更大的图景。正如 documentation std::set meets requirement of AssociativeContainer 概念中所述。对于这个概念,contains 方法没有任何意义,因为它对 std::multisetstd::multimap 几乎没有用,但 count 对它们都很好。虽然方法 contains 可以添加为 count 的别名,用于 std::setstd::map 及其散列版本(如 length 用于 size() std::string ), 但看起来库创建者并没有看到真正需要它。

缺少它,因为没有人添加它。没有人添加它,因为 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)) {
}

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

这样做更有意义:

if (some_set.count(some_element)) {
}

不过我被扩展方法方法逗乐了

真正可悲的是,在 multimapmultiset 上编写高效的 contains 可能会更快,因为他们只需要找到一个元素,而 count 必须找到它们中的每一个 并计算它们 .

一个包含 7 的 10 亿个副本的多重集(你知道,以防你 运行 出来)可能有一个非常慢的 .count(7),但可能有一个非常快的 contains(7)

使用上述扩展方法,我们可以通过使用 lower_bound、与 end 比较,然后与元素进行比较,使这种情况更快。然而,为无序的喵喵和有序的喵喵做这件事需要花哨的 SFINAE 或容器特定的重载。

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

设计决定背后的根本原因contains() returns bool 丢失有关元素在集合中位置的有价值信息 find() 以迭代器的形式保留和 returns 这些信息,因此对于像 STL 这样的通用库来说是更好的选择。这一直是 Alex Stepanov 的指导原则,正如他经常解释的那样(例如,here)。

至于一般的 count() 方法,虽然它通常是一个不错的解决方法,但它的问题是 它比 contains() 必须做

这并不是说 bool contains() 不是一个很好的选择,甚至不是必需的。不久前,我们在 ISO C++ 标准 - 未来提案组。

另一个原因是它会给程序员一种错误的印象,即 std::set 是数学集合论意义上的集合。如果他们实现了这一点,那么就会出现许多其他问题:如果 std::set 有一个值的 contains() ,为什么另一个集合没有它? union()、intersection() 等集合运算和谓词在哪里?

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

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

现在,std::set 只是一个容器,非常适合数学意义上的集合的实现,但它离理论集合的距离几乎和 std::vector 一样远是一个理论向量。

那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;

自 c++20 起,

bool contains( const Key& key ) const

可用。

contains() 必须 return 一个布尔值。使用 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
}

我想指出,正如 Andy 所提到的,自从 C++20 标准添加了映射或集合的 contains 成员函数:

bool contains( const Key& key ) const;  (since C++20)

现在我想集中回答有关性能与可读性的问题。 如果比较两个版本,就性能而言:

#include <unordered_map>
#include <string>
using hash_map = std::unordered_map<std::string,std::string>;
hash_map a;

std::string get_cpp20(hash_map& x,std::string str)
{
    if(x.contains(str))
        return x.at(str);
    else
        return "";
};

std::string get_cpp17(hash_map& x,std::string str)
{
    if(const auto it = x.find(str); it !=x.end())
        return it->second;
    else
        return "";
};

您会发现 cpp20 版本对 std::_Hash_find_last_result 进行了两次调用,而 cpp17 仅进行了一次调用。

现在我发现自己有很多嵌套的数据结构unordered_map。 所以你最终得到这样的结果:

using my_nested_map = std::unordered_map<std::string,std::unordered_map<std::string,std::unordered_map<int,std::string>>>;

std::string get_cpp20_nested(my_nested_map& x,std::string level1,std::string level2,int level3)
{
    if(x.contains(level1) &&
        x.at(level1).contains(level2) &&
        x.at(level1).at(level2).contains(level3))

        return x.at(level1).at(level2).at(level3);
    else
        return "";
};

std::string get_cpp17_nested(my_nested_map& x,std::string level1,std::string level2,int level3)
{
    if(const auto it_level1=x.find(level1); it_level1!=x.end())
        if(const auto it_level2=it_level1->second.find(level2);it_level2!=it_level1->second.end())
            if(const auto it_level3=it_level2->second.find(level3);it_level3!=it_level2->second.end())
                return it_level3->second;

    return "";
};

现在如果你有足够的条件in-between这些ifs,使用迭代器真的很痛苦,很容易出错和不清楚,我经常发现自己回头看map的定义来理解什么样的对象处于 1 级或 2 级,而对于 cpp20 版本,您会看到 at(level1).at(level2)... 并立即了解您正在处理的内容。 所以就代码 maintenance/review 而言,contains 是一个非常好的补充。