Skip to content

Correctness bugs: set::min()/max() ignore comparator; non-const set::operator[] does not compile #28

@jkalias

Description

@jkalias

Summary

Two correctness defects in include/set.h. Both are currently masked by the test suite (one via a compiler-conditional expectation, one because the buggy overload is never instantiated) rather than fixed.


1. set::min() / max() ignore the set's comparator (and are O(n))

Location: include/set.h:607-635

[[nodiscard]] fcpp::optional_t<TKey> min() const
{
    const auto& it = std::min_element(begin(), end());
    if (it != end()) {
        return *it;
    }
    return fcpp::optional_t<TKey>();
}

std::min_element / std::max_element compare elements with operator< on TKey, not with the set's TCompare.

Consequences

  • Wrong result for any set instantiated with a custom comparator. The set is stored ordered by TCompare, but min()/max() re-rank by operator<, so they can return the wrong element.
  • Fails to compile for key types that provide TCompare but not operator<.
  • O(n) where it should be O(1): a std::set is already ordered, so the minimum is *begin() and the maximum is *std::prev(end()).

Evidence — the test papers over this instead of fixing it (tests/set_test.cc:289):

TEST(SetTest, MinCustomType)
{
    const set<person, person_comparator> persons({
        person(15, "Jake"), person(18, "Jannet"),
        person(25, "Kate"), person(62, "Bob")
    });
    const auto minimum = persons.min();
#if defined(__clang__)
    EXPECT_EQ(person(18, "Jannet"), minimum.value());
#else
    EXPECT_EQ(person(62, "Bob"), minimum.value());   // different answer per compiler
#endif
}

The fact that the expected value differs between clang and gcc is a direct symptom of the bug: the answer depends on operator< and iteration order rather than the set's actual ordering.

Suggested fix

[[nodiscard]] fcpp::optional_t<TKey> min() const
{
    if (m_set.empty()) {
        return fcpp::optional_t<TKey>();
    }
    return *begin();              // ordered by TCompare, O(1)
}

[[nodiscard]] fcpp::optional_t<TKey> max() const
{
    if (m_set.empty()) {
        return fcpp::optional_t<TKey>();
    }
    return *std::prev(end());     // O(1)
}

This is deterministic, respects the custom comparator, is O(1), and allows the #if defined(__clang__) branch in SetTest.MinCustomType (and the analogous max test) to be removed.


2. Non-const set::operator[] does not compile

Location: include/set.h:1102-1116

TKey operator[](size_t index)
{
    assert_smaller_size(index);
#ifdef CPP17_AVAILABLE
    auto it = std::advance(begin(), index);   // std::advance returns void
    return *it;
#else
    ...
#endif
}

std::advance returns void, so auto it = std::advance(...) is ill-formed. The const overload (set.h:1121-1136) does it correctly:

auto it = begin();
std::advance(it, index);
return *it;

Why it's currently hidden: every call site in the tests uses operator[] on a const set& (e.g. test_contents(const set<int>& set) at tests/set_test.cc:37), so the non-const overload is never instantiated. Any use such as:

fcpp::set<int> s({1, 2, 3});
auto x = s[0];   // selects the non-const overload -> compile error

would fail to build under C++17.

Suggested fix: mirror the const overload:

TKey operator[](size_t index)
{
    assert_smaller_size(index);
    auto it = begin();
    std::advance(it, index);   // works for both C++11 and C++17 paths
    return *it;
}

A regression test that subscripts a non-const set would prevent this from regressing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions