"passing const as this discards qualifiers": Ten C++ const Errors and Their Fixes

Key takeaways

Ten common const-related C++ compile errors with the exact GCC wording, including "passing const as this argument discards qualifiers" and "cannot bind non-const lvalue reference", why each one fires, and how to fix the design rather than cast the const away.

Why const errors feel so arbitrary

const in C++ is a promise that the type system checks. Once something is const, every operation on it must also promise not to modify it, and the compiler follows that promise through references, pointers, member functions and iterators. Most const errors are not really about the line the compiler points at. They are about some function further away that did not make the promise it could have made.

That is the key to fixing them. The tempting fix is to remove const from the caller, or to reach for const_cast. The right fix is almost always to add const to the thing that should have had it: a parameter, a member function, a return type.

All messages below are from GCC 10 with -std=c++17. Clang and MSVC say the same things in different words; MSVC’s typical equivalents are C2662 (“cannot convert ‘this’ pointer from ‘const X’ to ‘X &’”) and C2664 for argument conversions.

Error 1: cannot bind non-const lvalue reference to an rvalue

void modify(std::string& s) { s += " world"; }

int main() {
    modify("Hello");
}
error: cannot bind non-const lvalue reference of type 'std::string&' to an rvalue of type 'std::string'
note:   initializing argument 1 of 'void modify(std::string&)'

"Hello" is not a std::string. The compiler would have to create a temporary string to call the function, and C++ forbids binding a temporary to a non-const T&: whatever modify wrote into it would disappear at the end of the statement, which is almost always a bug. It is allowed to bind a temporary to const T&, and the temporary lives until the call returns.

Fix it by deciding what the function actually does. If it only reads, take const std::string& (or std::string_view). If it really modifies its argument, the caller must pass a named variable. If it wants its own copy to change, take std::string by value.

Error 2: passing ‘const X’ as ‘this’ argument discards qualifiers

This is the most common const error in class code.

struct Meter {
    int value;
    int get() { return value; }          // not marked const
};

int read(const Meter& m) { return m.get(); }
error: passing 'const Meter' as 'this' argument discards qualifiers [-fpermissive]
note:   in call to 'int Meter::get()'

Inside a non-const member function, this has type Meter*. Inside a const one it is const Meter*. Calling get() on a const Meter& would require converting const Meter* to Meter*, which discards the const qualifier. The note line tells you exactly which member function lacks the promise.

int get() const { return value; }

Every member function that does not modify the object should be const from the start. Retrofitting it is painful because making one function const may require making the functions it calls const too.

Error 3: the same error from a standard container

int lookup(const std::map<std::string, int>& m) {
    return m["a"];
}
error: passing 'const std::map<std::__cxx11::basic_string<char>, int>' as 'this' argument discards qualifiers [-fpermissive]
note:   in call to '... std::map<...>::operator[](...) [with _Key = ...]'

It is error 2 in disguise, buried under a long template signature. std::map::operator[] inserts a default value when the key is missing, so it cannot be const. Use m.at("a") (throws std::out_of_range if absent) or m.find("a") and check against m.end(). The same applies to std::unordered_map.

Error 4: assignment of read-only variable

const int x = 1;
x = 2;
error: assignment of read-only variable 'x'

The obvious case. The same message appears for a const pointer itself:

int a = 1, b = 2;
int* const p = &a;
p = &b;        // error: assignment of read-only variable 'p'

Read declarations right to left: int* const p is “p is a const pointer to int”. The pointer is fixed; *p = 5; is fine. const int* p is “p is a pointer to const int”: you can repoint it but not write through it.

Error 5: assignment of member in read-only object

struct Counter {
    int hits;
    void touch() const { hits++; }
};
error: assignment of member 'Counter::hits' in read-only object

The function was marked const but modifies a member. Either the function should not be const, or the member is genuinely not part of the object’s observable state, in which case it may be mutable. Good candidates for mutable are caches, lazily computed values and mutexes used to lock inside const accessors. A hit counter that callers can read back is not a good candidate; it is real state.

Error 6: invalid conversion from ‘const int*’ to ‘int*’

const int x = 1;
int* p = &x;
error: invalid conversion from 'const int*' to 'int*' [-fpermissive]

Allowing this would let you write to x through p. The fix is const int* p = &x;. A related and more surprising version involves double pointers:

void h(char** argv);
const char* names[2];
h(names);   // error: invalid conversion from 'const char**' to 'char**'

That one is expected once you see it. The surprise is that the opposite direction, char** to const char**, is rejected too, even though char* to const char* is fine. Allowing it would let you store a const char* into a slot that the original char** still sees as writable. If a function only reads an array of strings, its parameter should be const char* const*, which accepts both.

Error 7: binding reference of type ‘int&’ to ‘const int’ discards qualifiers (range-for)

void reset(const std::vector<int>& v) {
    for (int& x : v) x = 0;
}
error: binding reference of type 'int&' to 'const int' discards qualifiers

Iterating a const container yields const elements. Use const int& (or const auto&) to read, or take the vector by non-const reference if the point of the function is to modify it. Writing for (auto& x : v) avoids spelling the type: auto& deduces const int& here, and then the error moves to the x = 0 line, which is where it belongs.

Error 8: the same message from a const member returning a reference

struct Box {
    int v;
    int& ref() const { return v; }
};
error: binding reference of type 'int&' to 'const int' discards qualifiers

Inside a const member function, v is const int. Returning a non-const reference to it would give callers a back door to modify a const object. The standard library pattern is a const/non-const pair:

int&       ref()       { return v; }
const int& ref() const { return v; }

Error 9: iterator conversion from const_iterator

void f(const std::vector<int>& v) {
    std::vector<int>::iterator it = v.begin();
}
error: conversion from '__normal_iterator<const int*,[...]>' to non-scalar type '__normal_iterator<int*,[...]>' requested

On a const container, begin() returns a const_iterator, whose internal pointer is const int*. The message leaks the library’s implementation type, but const int* versus int* inside the brackets gives it away. Use auto it = v.begin(); or std::vector<int>::const_iterator.

Error 10: ‘override’ does not override because const differs

struct Base    { virtual void draw() const; };
struct Derived : Base { void draw() override; };
error: 'void Derived::draw()' marked 'override', but does not override

const is part of a member function’s signature. draw() and draw() const are different functions, so Derived::draw does not override anything. Without override this compiles and silently creates a new, unrelated function; calls through Base& keep calling Base::draw. That silent version is the real danger, and it is the best argument for writing override on every overriding function.

What about const_cast?

const_cast<T&>(x) removes const, and the compiler will accept it. Whether it is safe depends on the original object: if it was created non-const and only viewed through a const reference, writing through the cast is fine. If it was defined const, writing through the cast is undefined behavior, and the compiler may have placed it in read-only memory or folded its value into the code.

The realistic legitimate case is calling an old C API declared as void log(char* msg) that never actually writes to msg. For anything in your own code, fix the signature.

Where this goes wrong in practice

The pattern I keep running into is const being added late. A class is written without const member functions, and months later someone writes a function taking const Widget&. Every call inside it fails with error 2. The quick fix is to change the parameter to Widget&, which then forces the callers to drop const too, and the non-constness spreads outward. Adding const to the accessors at the source is a smaller change, even though it looks bigger at first.

The other one is mutable used as a way to silence error 5. A const getTotal() that updates a mutable running total looks harmless, until two threads call it on the same object. Callers reasonably assume that const member functions on a shared object can be called concurrently without locks, since the standard library makes that guarantee for its own types. A mutable member that is written without synchronization breaks that assumption, and the result is a data race in code that appears read-only.

FAQ

Q. Why does m[key] fail to compile on a const std::map&?

A. operator[] may insert a new element, so it is a non-const member function. Use at(key) or find(key).

Q. Is const on a by-value parameter useful?

A. In a declaration, void f(const int x); is identical to void f(int x);: top-level const on a parameter is not part of the function type. In the definition it prevents the function body from modifying its own copy, which some codebases like and others consider noise. It never affects callers.