C++ Complete Guide to const | Practical Use of "Const Correctness"
이 글의 핵심
const correctness: top-level vs member const, const methods, overloading on const&, and logical const with mutable.
What is const Correctness?
Const correctness is a design principle in C++ that ensures objects that shouldn’t be modified are marked as const. This provides compile-time guarantees that prevent accidental modifications and makes code intent explicit.
Why is it needed?:
- Bug Prevention: Compiler catches accidental modifications
- Intent Clarity: Makes code self-documenting
- Interface Safety: Prevents misuse of APIs
- Optimization: Enables compiler optimizations
- Thread Safety: Helps identify shared state
// ❌ Without const: Intent unclear, bugs possible
void process(vector<int>& data) {
data.push_back(42); // Was this intentional?
}
// ✅ With const: Intent clear, modifications prevented
void process(const vector<int>& data) {
// data.push_back(42); // Compile error!
// Can only read data
}
const expresses “immutability” and is recommended even during code reviews. Combined with mutable, it allows for exceptions like caching or locks. When paired with function basics or smart pointers, you can create read-only interfaces.
const Variables
const int x = 10;
// x = 20; // Error: const variables cannot be modified
const int y; // Error: const requires initialization at declaration
Why use const variables?:
// Configuration values
const int MAX_CONNECTIONS = 100;
const double PI = 3.14159;
// Prevents accidental modification
// MAX_CONNECTIONS = 200; // Compile error!
// Compiler can optimize
const int BUFFER_SIZE = 1024;
char buffer[BUFFER_SIZE]; // Size known at compile time
const Functions
class Point {
private:
int x, y;
public:
Point(int x, int y) : x(x), y(y) {}
// const member function (does not modify object state)
int getX() const { return x; }
int getY() const { return y; }
// non-const member function
void setX(int newX) { x = newX; }
};
int main() {
const Point p(10, 20);
cout << p.getX() << endl; // OK
// p.setX(30); // Error: const objects cannot call non-const functions
}
const Pointers
const Pointer Patterns
| Declaration | Pointer Modifiable | Value Modifiable | Read As |
|---|---|---|---|
const int* ptr | ✅ | ❌ | “Pointer to const int” |
int* const ptr | ❌ | ✅ | “Const pointer to int” |
const int* const ptr | ❌ | ❌ | “Const pointer to const int” |
int const* ptr | ✅ | ❌ | Same as const int* |
int x = 10;
int y = 20;
// 1. Pointer points to a const value
const int* ptr1 = &x;
// *ptr1 = 20; // Error
ptr1 = &y; // OK
// 2. Pointer itself is const
int* const ptr2 = &x;
*ptr2 = 20; // OK
// ptr2 = &y; // Error
// 3. Both pointer and value are const
const int* const ptr3 = &x;
// *ptr3 = 20; // Error
// ptr3 = &y; // Error
Tip: Use const as the reference point—everything to the right is const.
Visualizing const Pointers
graph TD
A[const int* ptr] --> B[Pointer modifiable]
A --> C[*ptr not modifiable]
D[int* const ptr] --> E[Pointer not modifiable]
D --> F[*ptr modifiable]
G[const int* const ptr] --> H[Pointer not modifiable]
G --> I[*ptr not modifiable]
const References
void process(const vector<int>& v) {
// Only reads v (no copying)
for (int x : v) {
cout << x << " ";
}
// v.push_back(10); // Error
}
int main() {
vector<int> data = {1, 2, 3};
process(data); // Pass without copying
}
mutable
Scenarios for Using mutable
| Scenario | Example | Reason |
|---|---|---|
| Caching | Store computed results | Logically const, physically modifiable |
| Statistics | Count access frequency | Observational behavior |
| Synchronization | mutable mutex | Locks are not logical state |
| Lazy Initialization | Initialize on first access | Read operation with internal modification |
class Cache {
private:
mutable int accessCount; // Modifiable even in const functions
int value;
public:
Cache(int v) : value(v), accessCount(0) {}
int getValue() const {
accessCount++; // OK: mutable
return value;
}
int getAccessCount() const {
return accessCount;
}
};
int main() {
const Cache cache(42);
cout << cache.getValue() << endl; // Increases accessCount
cout << cache.getAccessCount() << endl; // 1
}
Practical Examples
Example 1: Const Correctness
class String {
private:
char* data;
size_t length;
public:
String(const char* str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
~String() {
delete[] data;
}
// const version
const char* c_str() const {
return data;
}
// non-const version
char* c_str() {
return data;
}
size_t size() const {
return length;
}
};
void print(const String& s) {
cout << s.c_str() << endl; // Calls const version
}
Example 2: Const and Iterators
#include <vector>
using namespace std;
void process(const vector<int>& v) {
// Use const_iterator
for (vector<int>::const_iterator it = v.begin();
it != v.end(); ++it) {
cout << *it << " ";
// *it = 10; // Error
}
// Or use auto
for (auto it = v.cbegin(); it != v.cend(); ++it) {
cout << *it << " ";
}
}
Example 3: Const and Thread Safety
#include <mutex>
class ThreadSafeCounter {
private:
mutable mutex mtx; // Allows locking in const functions
int count;
public:
ThreadSafeCounter() : count(0) {}
void increment() {
lock_guard<mutex> lock(mtx);
count++;
}
int getCount() const {
lock_guard<mutex> lock(mtx); // OK: mutable
return count;
}
};
Common Issues
Issue 1: Modifying Members in Const Functions
// ❌ Error
class Bad {
private:
int x;
public:
void func() const {
x = 10; // Error!
}
};
// ✅ Use mutable
class Good {
private:
mutable int x;
public:
void func() const {
x = 10; // OK
}
};
Issue 2: Const Overloading
class Container {
private:
int data[10];
public:
// const version
const int& operator[](int index) const {
return data[index];
}
// non-const version
int& operator[](int index) {
return data[index];
}
};
int main() {
Container c;
c[0] = 10; // Calls non-const version
const Container cc;
int x = cc[0]; // Calls const version
// cc[0] = 10; // Error
}
Issue 3: const_cast
void legacyFunction(char* str) {
// Legacy function (no const)
}
void modernFunction(const char* str) {
// Remove const with const_cast (dangerous!)
legacyFunction(const_cast<char*>(str));
}
// ✅ Better approach: Wrapper function
void safeWrapper(const char* str) {
char* temp = new char[strlen(str) + 1];
strcpy(temp, str);
legacyFunction(temp);
delete[] temp;
}
Const Correctness Checklist
1. Function Parameters
// ❌ Unnecessary copying
void process(vector<int> v) { }
// ✅ Use const reference
void process(const vector<int>& v) { }
2. Member Functions
class MyClass {
public:
// ❌ Missing const
int getValue() { return value; }
// ✅ Add const
int getValue() const { return value; }
};
3. Return Types
class String {
public:
// ❌ Exposes internal data
char* getData() { return data; }
// ✅ Return const
const char* getData() const { return data; }
};
Compiler Optimizations
// const helps with compiler optimizations
void compute(const int* data, int size) {
// Compiler knows data won't change, enabling optimizations
for (int i = 0; i < size; i++) {
// ...
}
}
Production Patterns
Pattern 1: Builder Pattern with const
class HttpRequest {
std::string url_;
std::string method_;
std::map<std::string, std::string> headers_;
public:
HttpRequest& setUrl(const std::string& url) {
url_ = url;
return *this;
}
HttpRequest& setMethod(const std::string& method) {
method_ = method;
return *this;
}
HttpRequest& addHeader(const std::string& key, const std::string& value) {
headers_[key] = value;
return *this;
}
// const getter methods
const std::string& getUrl() const { return url_; }
const std::string& getMethod() const { return method_; }
const std::map<std::string, std::string>& getHeaders() const {
return headers_;
}
};
// Usage
HttpRequest request;
request.setUrl("https://api.example.com")
.setMethod("POST")
.addHeader("Content-Type", "application/json");
// Read-only access
const HttpRequest& constRequest = request;
std::cout << constRequest.getUrl() << '\n';
Pattern 2: Lazy Initialization with mutable
class ExpensiveResource {
mutable std::unique_ptr<Data> cache_;
mutable bool initialized_ = false;
void initialize() const {
if (!initialized_) {
cache_ = std::make_unique<Data>(/* expensive computation */);
initialized_ = true;
}
}
public:
const Data& getData() const {
initialize(); // OK: mutable members
return *cache_;
}
};
Pattern 3: Read-Only View
template<typename T>
class ReadOnlyView {
const std::vector<T>& data_;
public:
explicit ReadOnlyView(const std::vector<T>& data) : data_(data) {}
// Read-only operations
size_t size() const { return data_.size(); }
const T& operator[](size_t index) const { return data_[index]; }
// Iterator support
auto begin() const { return data_.cbegin(); }
auto end() const { return data_.cend(); }
// No modification allowed
// void push_back(const T& value) = delete;
};
// Usage
std::vector<int> data = {1, 2, 3, 4, 5};
ReadOnlyView<int> view(data);
for (int value : view) {
std::cout << value << ' ';
}
// view.push_back(6); // Not available
FAQ
Q1: Why use const?
A:
- Clarifies intent: Makes it explicit that a value won’t change
- Prevents bugs: Compiler catches accidental modifications
- Enables optimizations: Compiler can make assumptions about const data
- Improves interface safety: Prevents misuse of APIs
- Thread safety: Helps identify shared state
// Without const: Unclear if modification is intentional
void process(std::string& data) {
data = "modified"; // Bug or feature?
}
// With const: Intent is clear
void process(const std::string& data) {
// Can only read data
}
Q2: Where should const be used?
A:
- Immutable variables: Configuration values, constants
- Member functions: Functions that don’t modify object state
- Function parameters: To avoid copying and prevent modification
- Return types: To prevent modification of returned data
class MyClass {
int value_;
public:
// const member function
int getValue() const { return value_; }
// const parameter
void process(const std::vector<int>& data) const {
// Read-only access
}
// const return type
const std::string& getName() const { return name_; }
};
Q3: When should I use mutable?
A:
- Caching: Store computed results in const functions
- Logging: Track access in const functions
- Reference counting: Modify counters in const functions
- Mutexes: Lock in const functions for thread safety
class Cache {
mutable int accessCount_ = 0;
mutable std::mutex mutex_;
public:
int getValue() const {
std::lock_guard<std::mutex> lock(mutex_); // OK: mutable
accessCount_++; // OK: mutable
return value_;
}
};
Q4: Is const_cast safe?
A: It is risky. Modifying an originally const object leads to undefined behavior. Use only for integrating legacy code.
// ❌ Dangerous: Undefined behavior
const int x = 10;
int* ptr = const_cast<int*>(&x);
*ptr = 20; // Undefined behavior!
// ✅ Safe: Original object was not const
int y = 10;
const int* constPtr = &y;
int* ptr2 = const_cast<int*>(constPtr);
*ptr2 = 20; // OK: y was not originally const
Better approach: Avoid const_cast by designing APIs correctly.
Q5: Does const affect performance?
A: Const itself has no runtime overhead. It often helps the compiler optimize your code by:
- Allowing values to be stored in read-only memory
- Enabling better register allocation
- Allowing more aggressive inlining
// Compiler can optimize const data
const int SIZE = 1000;
for (int i = 0; i < SIZE; ++i) {
// Compiler knows SIZE won't change
}
Q6: How do I start with const correctness?
A:
- Make const a habit in new code
- Add const to member functions that don’t modify state
- Use const references for function parameters
- Mark variables const when they won’t change
- Improve gradually in existing code
// Step 1: Start with new code
class NewClass {
public:
int getValue() const { return value_; } // const member
void process(const std::string& data) { // const parameter
const int result = compute(data); // const variable
}
};
Q7: What’s the difference between const T* and T* const?
A:
const T*: Pointer to const data (can’t modify data, can change pointer)T* const: Const pointer to data (can modify data, can’t change pointer)
int x = 10, y = 20;
// Pointer to const int
const int* ptr1 = &x;
// *ptr1 = 20; // Error: can't modify data
ptr1 = &y; // OK: can change pointer
// Const pointer to int
int* const ptr2 = &x;
*ptr2 = 20; // OK: can modify data
// ptr2 = &y; // Error: can't change pointer
Tip: Read from right to left: int* const = “const pointer to int”
Q8: Can I overload functions based on const?
A: Yes, you can provide both const and non-const versions:
class Container {
int data_[10];
public:
// const version: returns const reference
const int& operator[](int index) const {
return data_[index];
}
// non-const version: returns non-const reference
int& operator[](int index) {
return data_[index];
}
};
Container c;
c[0] = 10; // Calls non-const version
const Container cc;
int x = cc[0]; // Calls const version
// cc[0] = 10; // Error: returns const reference
Related Posts: mutable, smart pointers, function basics, code review checklist.
One-line Summary: Const correctness ensures objects that shouldn’t be modified are marked as const, providing compile-time safety and making code intent explicit.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ mutable Keyword | “mutable 키워드” 가이드
- C++ 스마트 포인터 | unique_ptr/shared_ptr “메모리 안전” 가이드
- C++ 함수 | “처음 배우는” 함수 만들기 완벽 가이드 [예제 10개]
- C++ 코드 리뷰 | “체크리스트” 20가지 [실무 필수]
이 글에서 다루는 키워드 (관련 검색어)
C++, const, const-correctness, constants, code-quality 등으로 검색하시면 이 글이 도움이 됩니다.