C++ Complete Guide to const | Practical Use of "Const Correctness"

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

DeclarationPointer ModifiableValue ModifiableRead 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* ptrSame 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

ScenarioExampleReason
CachingStore computed resultsLogically const, physically modifiable
StatisticsCount access frequencyObservational behavior
Synchronizationmutable mutexLocks are not logical state
Lazy InitializationInitialize on first accessRead 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:

  1. Make const a habit in new code
  2. Add const to member functions that don’t modify state
  3. Use const references for function parameters
  4. Mark variables const when they won’t change
  5. 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 등으로 검색하시면 이 글이 도움이 됩니다.