15.5. The set class

A set refers to any data structure in which every member of the set is unique. The integers define a set, because every number is unique. The values {3, 1, 4, 1, 5, 9} do not define a proper set, because the value 1 is repeated.

In C++, a std::set keeps its elements ordered according to its comparison object. By default, that comparison uses operator<. Like std::vector, a set is a generic class and declarations must include the object type stored in the class:

#include <iostream>
#include <set>

std::set<int> sample_set() {
  return {2, 7, 1, 8, 4, 5, 9};
}

The following example initializes a set with values and prints the set. Without running the code first, what do you think will be stored in x after initialization?

Answer

The two defining characteristics of a set are:

  • A set is sorted.

  • A set may contain only unique values.

Defining a set with repeated values is not an error. Equivalent values after the first are ignored.

When initialized, x will contain: 1 2 4 5 7 8 9

 1#include <iostream>
 2#include <set>
 3
 4int main() {
 5  std::set<int> x {2, 7, 1, 8, 2, 8, 1, 8, 2, 8, 4, 5, 9};
 6  for (const auto value : x) {
 7    std::cout << value << ' ';
 8  }
 9  std::cout << '\n';
10}

Like the sequence containers, each element in a set can be visited one at a time using a range-for loop. The preceding complete example uses that loop to display the sorted values.

Because set does not provide operator[], an index-based loop is not the appropriate way to visit its elements. Use a range-for loop or an iterator instead.

Sets can contain any key type for which the set's comparison object defines a strict weak ordering. The default comparison object uses operator<, but a caller can provide a different comparator instead. The key type does not have to overload operator< when a custom comparator is provided.

For example, this complete program stores the values in descending order:

 1struct descending {
 2  bool operator()(int lhs, int rhs) const {
 3    return lhs > rhs;
 4  }
 5};
 6
 7int main() {
 8  std::set<int, descending> x {2, 7, 1, 8, 4, 5, 9};
 9  for (const auto value : x) {
10    std::cout << value << ' ';
11  }
12  std::cout << '\n';
13}

The keys in a std::set are treated as constant while they are in the container. Changing a key through an iterator could violate the ordering invariant, so dereferencing a set iterator produces a const reference.

For an ordered set, search, insertion, and erasure take \(O(\log N)\) time. The container uses \(O(N)\) storage. Unordered containers have average constant-time lookup, insertion, and erasure, but their worst-case time is \(O(N)\).

Use set::insert to add a new element to a set. The function returns a pair containing an iterator to the equivalent element and a Boolean that is true only when a new element was inserted.

 1#include <iostream>
 2#include <set>
 3
 4int main() {
 5  auto x = std::set<int> {2, 7, 1, 8, 4, 5, 9};
 6  auto inserted = x.insert(6);
 7  auto duplicate = x.insert(8);
 8
 9  std::cout << std::boolalpha
10            << "inserted 6: " << inserted.second << '\n'
11            << "value at returned position: " << *inserted.first << '\n'
12            << "inserted duplicate 8: " << duplicate.second << '\n';
13}

Because a set is not an indexed container, looking up a value is a search. The set::find function returns an iterator to the element with a specific key, or end() when the key is absent:

 1#include <iostream>
 2#include <set>
 3
 4int main() {
 5  auto x = std::set<int> {2, 7, 1, 8, 4, 5, 9};
 6  const auto it = x.find(8);
 7  if (it != x.end()) {
 8    std::cout << "found: " << *it << '\n';
 9  }
10}

C++20 Feature

C++20 added set::contains for membership checks. It returns a Boolean and avoids creating an iterator when the position is not needed.

1#include <iostream>
2#include <set>
3
4int main() {
5  const auto x = std::set<int> {2, 7, 1, 8, 4, 5, 9};
6  std::cout << std::boolalpha
7            << x.contains(8) << ' '
8            << x.contains(3) << '\n';
9}

The set::erase function removes an element from a set. When given an iterator, it removes the element at that position and does not invalidate iterators to other elements:

 1#include <iostream>
 2#include <set>
 3
 4int main() {
 5  auto x = std::set<int> {2, 7, 1, 8, 4, 5, 9};
 6  const auto it = x.find(8);
 7  if (it != x.end()) {
 8    x.erase(it);
 9  }
10
11  std::cout << std::boolalpha
12            << (x.find(8) == x.end()) << '\n';
13}

C++20 Feature

C++20 also provides std::erase_if for removing every element that satisfies a predicate. This is useful when the value to remove is described by a condition rather than a single key.

 1#include <iostream>
 2#include <set>
 3
 4int main() {
 5  auto x = std::set<int> {2, 7, 1, 8, 4, 5, 9};
 6  std::erase_if(x, [](int value) { return value % 2 == 0; });
 7
 8  for (const auto value : x) {
 9    std::cout << value << ' ';
10  }
11  std::cout << '\n';
12}

15.5.1. Variations on std::set

The standard library provides related ordered and unordered containers:

multiset

A set in which duplicate keys are allowed.

unordered_set

A container of unique objects organized by a hash function, not by sorted order. Added in C++11.

A key type needs a hash function and an equality predicate. The standard library provides these for many built-in and library types. For a user type, provide a std::hash<Key> specialization or a custom hash object, and provide equality through operator== or a custom equality object.

unordered_multiset

An unordered_set in which duplicate keys are allowed.