Question

Const Member Functions and std::set Iterators in C++

cppmember-functionsconst-correctnessstdsetfunction-qualifier

Question

Why does the following C++ program produce an error when calling getId() and getName() through a std::set<StudentT>::iterator?

#include <iostream>
#include <set>
#include <string>

using namespace std;

class StudentT {
public:
    int id;
    string name;

    StudentT(int _id, string _name) : id(_id), name(_name) {}

    int getId() {
        return id;
    }

    string getName() {
        return name;
    }
};

inline bool operator<(StudentT s1, StudentT s2) {
    return s1.getId() < s2.getId();
}

int main() {
    set<StudentT> st;
    StudentT s1(0, "Tom");
    StudentT s2(1, "Tim");

    st.insert(s1);
    st.insert(s2);

    set<StudentT>::iterator itr;
    for (itr = st.begin(); itr != st.end(); ++itr) {
        cout << itr->getId() << " " << itr->getName() << endl;
    }
}

The compiler reports that passing a const StudentT as the this argument to getId() and getName() discards qualifiers. What is wrong, and how should the class be corrected?

Short Answer

You will learn what const means for member functions, why elements in a std::set are treated as read-only, and how to write accessors and comparison functions that work correctly with standard containers.

Concept

A member function is either allowed to modify its object or explicitly promises not to modify it.

A function such as this is not a const member function:

int getId() {
    return id;
}

Its implicit this pointer is effectively StudentT* const this, so it may modify the current object. It cannot be called on a const StudentT object.

To promise that an accessor does not modify the object, place const after the parameter list:

int getId() const {
    return id;
}

This makes the implicit this pointer effectively const StudentT* const this. The function can now be called for both ordinary and const objects.

Why std::set does this

A std::set stores unique elements in sorted order. Its ordering is determined by its comparison function, such as operator< in this example.

If code could modify an element while it was inside the set, especially the value used for sorting, the set could become incorrectly ordered. For that reason, dereferencing a set iterator gives access to a const element. Even a std::set<StudentT>::iterator behaves this way for its element access.

Therefore, this expression:

itr->getId()

attempts to call getId() on a const StudentT. The original getter does not have a const qualifier, so the compiler rejects the call.

Mental Model

Think of a const object as a library book behind a glass display case.

  • You may read its title and author.
  • You may not write in it or replace pages.
  • A member function marked const is a tool that only reads the book.
  • A member function without const might write in the book, so it is not allowed through the glass.

A std::set keeps its elements behind this glass because changing an element could change where it belongs in the sorted collection.

Take Quiz

Syntax and Examples

Add const after a member function's parameter list when it does not change the observable state of the object.

class StudentT {
private:
    int id;
    std::string name;

public:
    StudentT(int id, std::string name)
        : id(id), name(std::move(name)) {}

    int getId() const {
        return id;
    }

    const std::string& getName() const {
        return name;
    }
};

getId() returns a small int, so returning it by value is appropriate.

getName() returns a const std::string&, avoiding a copy of the string while preventing callers from changing the student's stored name through the returned reference.

A suitable comparison operator also accepts const references:

bool operator<(const StudentT& left, const StudentT& right) {
    return left.getId() < right.getId();
}

Passing by const reference avoids copying both objects and lets the operator compare const values.

Step by Step Execution

Consider this corrected loop:

std::set<StudentT> students;
students.insert(StudentT(2, "Ava"));

for (std::set<StudentT>::iterator it = students.begin();
     it != students.end();
     ++it) {
    std::cout << it->getId() << '\n';
}

Step by step:

  1. students.insert(...) stores a StudentT in sorted order.
  2. students.begin() returns an iterator pointing at the first stored element.
  3. it-> accesses the object referred to by the iterator.
  4. For a std::set, that referred-to object is treated as const StudentT.
  5. getId() const promises not to alter the object, so it may be called.
  6. The function returns id, and the program prints 2.
  7. ++it advances to the next element until it == students.end().

If getId() did not end in const, step 5 would fail at compile time.

Real World Use Cases

Const member functions appear throughout C++ programs:

  • Displaying API or database results: Read fields from records stored in a std::set, std::map, or other collection.
  • Sorting and searching: Comparison functions inspect objects without changing them.
  • Logging and debugging: A logger may receive const references and call read-only accessors.
  • Function parameters: Functions often accept const User&, const Config&, or const Order& to avoid copying and prevent accidental changes.
  • Read-only application state: UI rendering code commonly reads from models without modifying them.

Whenever an operation only observes an object, making it const communicates that intent and allows it to work in more places.

Take Quiz

Real Codebase Usage

Developers usually make fields private and expose const accessors. This protects class invariants and makes future changes safer.

class Student {
private:
    int id_;
    std::string name_;

public:
    Student(int id, std::string name)
        : id_(id), name_(std::move(name)) {}

    int id() const noexcept {
        return id_;
    }

    const std::string& name() const noexcept {
        return name_;
    }
};

For a std::set, projects commonly provide a dedicated comparator rather than a global operator<:

struct StudentById {
    bool operator()(const Student& left, const Student& right) const {
        return left.id() < right.id();
    }
};

std::set<Student, StudentById> students;

Notice two separate uses of const:

  • const Student&: the comparator will not modify either student.
  • operator()(...) const: calling the comparator will not modify the comparator object itself.

If an ordering field must change, do not modify an element in place inside a set. Remove the element, make the change, then insert it again. This preserves the set's ordering invariant.

Common Mistakes

Forgetting const on a getter

Broken:

class Student {
public:
    int getId() {
        return id;
    }
private:
    int id = 0;
};

Fix:

int getId() const {
    return id;
}

Putting const in the wrong location

This makes the return value const, not the member function:

const int getId() {
    return id;
}

For an int, const on the returned value is not useful. The needed syntax is:

int getId() const {
    return id;
}

Changing an object in a const member function

This is invalid because name is modified:

std::string getName() const {
    name += "!"; // Error
    return name;
}

A const member function should only read normal data members.

Passing set elements by value in comparisons

This works but makes unnecessary copies:

bool operator<(StudentT left, StudentT right) {
    return left.getId() < right.getId();
}

Prefer const references:

bool operator<(const StudentT& left, const StudentT& right) {
    return left.getId() < right.getId();
}

Modifying a set element through an iterator

Do not expect this to work:

// it->id = 10; // Not allowed for a std::set element

Changing a key could invalidate the set's sorted structure. Erase and reinsert a changed value instead.

Comparisons

SituationCan modify object?Can call getId() const?Can call getId()?
Ordinary StudentT objectYesYesYes
const StudentT objectNoYesNo
Element accessed through std::vector<StudentT>::iteratorUsually yesYesYes
Element accessed through std::set<StudentT>::iteratorNoYesNo

std::set versus std::vector

  • std::vector is a sequence. Its non-const iterator normally provides mutable access to elements.
  • std::set is an ordered associative container. Its elements determine their own position, so mutable iterator access would be unsafe.

const member function versus static member function

  • A const member function belongs to an object and promises not to modify that object.
  • A static member function has no this object at all and cannot directly access ordinary instance fields.
class Example {
public:
    int read() const;      // Reads one Example object
    static int count();    // Does not use a particular Example object
};
Take Quiz

Cheat Sheet

class Item {
public:
    int id() const {       // Callable on const and non-const objects
        return id_;
    }

private:
    int id_ = 0;
};
  • Put const after a member function's () to make it read-only.
  • A non-const member function cannot be called on a const object.
  • A const member function can be called on both const and non-const objects.
  • std::set elements are exposed as const because changing them could break ordering.
  • Use const T& for read-only parameters when copying T is unnecessary.
  • Compare set values with bool operator<(const T&, const T&) or a comparator object.
  • To change a value that affects set ordering: erase it, create the updated value, and insert it again.
  • Returning const std::string& can avoid a string copy; do not return a reference to a local variable.

FAQ

Why are std::set elements const?

A set must remain sorted according to its comparator. Allowing in-place changes could make an element belong somewhere else while it remains in its old position.

Does std::set<StudentT>::iterator return a const object?

Yes, element access through a set iterator behaves as access to a const value. This is true even if the iterator type is named iterator rather than const_iterator.

Can a const member function return a value?

Yes. const describes whether the function may modify its object, not whether it may return a value.

Can I overload a member function with and without const?

Yes. C++ treats them as different overloads.

std::string& name() { return name_; }
const std::string& name() const { return name_; }

The first is selected for mutable objects; the second is selected for const objects.

Should every getter be const?

Normally, yes. A getter that only reads state should be const so it works with const objects and standard-library code.

Why should the comparison operator use const references?

const StudentT& avoids copying objects and ensures comparison does not modify either value.

Can I modify data in a const member function with mutable?

mutable permits specific members to change in a const function, usually for caches or synchronization details. Do not use it merely to bypass const-correctness for ordinary object state.

Take Quiz

Mini Project

Description

Build a small student directory using std::set. The directory keeps students sorted by ID and prints them safely through const-qualified accessor functions. It demonstrates why a set's stored values should be treated as read-only.

Goal

Store unique students ordered by ID, then display every student's ID and name without const-related compiler errors.

Requirements

Create a Student class with private ID and name fields.

Add const-qualified accessor methods for both fields.

Store several students in a std::set ordered by ID.

Print every student by iterating over the set.

Attempt no in-place modification of an element after insertion.

Take Quiz

Keep learning

Related questions

View more