const and mut: C++ and Rust

C++ const correctness is a habit: mark the methods that do not change the object, pass references as const, and the compiler checks the rest. Rust flips the default, because a method on a shared reference cannot change the object at all. The interesting case is the getter that counts its own calls, which one language allows and the other stops.

The task in C++

The counter has a getter that increments before returning, and a const reader that only reads. Calling the getter twice changes the object, and the reader sees the new value afterwards. Nothing in the program says that total() mutates, which is exactly the trap: a name that looks like a read hides a write.

The const method cannot do the same, because the compiler checks it. The program therefore has one method that reads and one that writes, with nothing in the signature of the first to warn the reader.

#include <iostream> class Counter { public: explicit Counter(int start) : value_(start) {} int total() { return ++value_; } int value() const { return value_; } private: int value_; }; void print(const Counter& counter) { std::cout << "const view: " << counter.value() << '\n'; } int main() { Counter counter(10); print(counter); std::cout << "total: " << counter.total() << '\n'; std::cout << "total: " << counter.total() << '\n'; print(counter); }

g++ -std=c++23 -Wall -Wextra const_mut.cpp -o const_mut
./const_mut

const view: 10
total: 11
total: 12
const view: 12

The output climbs from ten to twelve because each call to total writes. A caller that prints the value in a loop changes the object on every iteration, which is the kind of action at a distance const correctness was meant to prevent.

The same task in Rust

The Rust version makes the write honest: total takes &mut self, and the caller needs a mutable binding. The reader takes &self and cannot write, because the language has no mutable keyword to opt out for one field.

The output matches the C++ run line for line. The difference is that the signature now says which method changes the object, and a caller holding a shared reference simply cannot call the writer.

struct Counter { value: i32, } impl Counter { fn new(value: i32) -> Self { Self { value } } fn total(&mut self) -> i32 { self.value += 1; self.value } } fn print(counter: &Counter) { println!("const view: {}", counter.value); } fn main() { let mut counter = Counter::new(10); print(&counter); println!("total: {}", counter.total()); println!("total: {}", counter.total()); print(&counter); }

rustc --edition 2024 const_mut.rs
./const_mut

const view: 10
total: 11
total: 12
const view: 12

A method that really must mutate through a shared reference uses Cell or RefCell, which the lesson on const and mutability builds. The cell makes the mutation visible in the field type, which is better than hiding it in a method.

What differs and why

Who owns the value: the caller in both cases, and the method borrows it. In C++ the borrow is a reference whose constness is part of the type, and a non-const method may be called on a non-const object. In Rust the borrow itself is either shared or mutable, and the method signature must match it.

What the compiler rejects: C++ rejects a write in a const method and accepts a write in any other. Rust rejects a write behind &self with E0594, whatever the method is called, so a getter cannot quietly become a writer.

What happens at run time: the same increments and the same printed numbers. The difference is in what a reader of the signature can conclude, and in the fact that the Rust caller had to declare the binding mutable to call the writer at all.

The habit that breaks

The habit is the getter with a side effect. In C++ it compiles as long as the method is not marked const, and the call site cannot tell it apart from a read. In Rust the same method is rejected, and the compiler tells you to take &mut self, which changes every call site and makes the write visible.

struct Counter { value: i32, } impl Counter { fn total(&self) -> i32 { self.value += 1; self.value } } fn main() { let mut counter = Counter { value: 10 }; println!("{}", counter.total()); }

rustc --edition 2024 getter.rs

error[E0594]: cannot assign to `self.value`, which is behind a `&` reference
--> getter.rs:7:9
|
7 | self.value += 1;
| ^^^^^^^^^^^^^^^ `self` is a `&` reference, so it cannot be written to
|
help: consider changing this to be a mutable reference
|
6 | fn total(&mut self) -> i32 {
| +++

warning: variable does not need to be mutable
--> getter.rs:13:9
|
13 | let mut counter = Counter { value: 10 };
| ----^^^^^^^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default

error: aborting due to 1 previous error; 1 warning emitted

For more information about this error, try `rustc --explain E0594`.

The C++ lesson for this ground is const and const correctness, and the Rust lesson is const, static and interior mutability.

The C++ program was built with g++ 15.2.0 and the Rust program with rustc 1.96.1, and both were run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev

Banner image