为什么 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::set
和 std::multiset
尽可能相似。 (显然 count
对 std::multiset
具有完全合理的含义。)
我个人认为这是一个错误。
如果你假装 count
只是 contains
的拼写错误并将测试写成:
,它看起来并没有那么糟糕
if (myset.count(element))
...
不过还是很遗憾。
虽然我不知道为什么 std::set
没有 contains
而 count
只有 returns 0
或 1
,
你可以像这样写一个模板化的 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
查找。
当我们意识到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();
}
当然这一切都只是猜测。
您正在调查特定案例,而没有看到更大的图景。正如 documentation std::set
meets requirement of AssociativeContainer 概念中所述。对于这个概念,contains
方法没有任何意义,因为它对 std::multiset
和 std::multimap
几乎没有用,但 count
对它们都很好。虽然方法 contains
可以添加为 count
的别名,用于 std::set
、std::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)) {
}
不过我被扩展方法方法逗乐了
真正可悲的是,在 multimap
或 multiset
上编写高效的 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
是一个非常好的补充。
我经常使用 std::set<int>
,而且我经常只需要检查这样的集合是否包含数字。
我觉得写起来很自然:
if (myset.contains(number))
...
但是因为缺少contains
成员,所以需要写的比较繁琐:
if (myset.find(number) != myset.end())
..
或不那么明显:
if (myset.count(element) > 0)
..
这个设计决定有原因吗?
我认为这可能是因为他们试图让 std::set
和 std::multiset
尽可能相似。 (显然 count
对 std::multiset
具有完全合理的含义。)
我个人认为这是一个错误。
如果你假装 count
只是 contains
的拼写错误并将测试写成:
if (myset.count(element))
...
不过还是很遗憾。
虽然我不知道为什么 std::set
没有 contains
而 count
只有 returns 0
或 1
,
你可以像这样写一个模板化的 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
查找。
当我们意识到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();
}
当然这一切都只是猜测。
您正在调查特定案例,而没有看到更大的图景。正如 documentation std::set
meets requirement of AssociativeContainer 概念中所述。对于这个概念,contains
方法没有任何意义,因为它对 std::multiset
和 std::multimap
几乎没有用,但 count
对它们都很好。虽然方法 contains
可以添加为 count
的别名,用于 std::set
、std::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)) {
}
不过我被扩展方法方法逗乐了
真正可悲的是,在 multimap
或 multiset
上编写高效的 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
是一个非常好的补充。