Two Paradigms of Getters and Setters in C++: Identity-Oriented vs Value-Oriented

Dec 05, 2025 · Programming · 14 views · 7.8

Keywords: C++ | getter | setter | identity-oriented | value-oriented | const correctness

Abstract: This article explores two main implementation paradigms for getters and setters in C++: identity-oriented (returning references) and value-oriented (returning copies). Through analysis of real-world examples from the standard library, it explains the design philosophy, applicable scenarios, and performance considerations of both approaches, providing complete code examples. The article also discusses const correctness, move semantics optimization, and alternative type encapsulation strategies to traditional getters/setters, helping developers choose the most appropriate implementation based on specific requirements.

Introduction

The implementation of getters and setters in C++ often sparks discussions about "Java style" versus "C++ style." Based on standard library practices, this article presents two core paradigms: identity-oriented and value-oriented. These paradigms are not about right or wrong but correspond to different design requirements.

Identity-Oriented Paradigm

The identity-oriented paradigm implements property access by returning references to member variables. This approach emphasizes the importance of object identity in system interactions, allowing both the caller and callee to observe each other's modifications.

class Foo {
    X x_;
public:
    X& x() { return x_; }
    const X& x() const { return x_; }
};

This implementation appears to only provide "get" functionality, but since it returns a modifiable reference, it effectively offers "set" capability as well:

Foo f;
f.x() = X{...};  // Direct modification through reference

When type X is assignable, this pattern is particularly suitable for scenarios requiring consistent object identity.

Value-Oriented Paradigm

The value-oriented paradigm implements property access by returning copies and accepting copies, emphasizing value independence rather than object identity:

class Foo {
    X x_;
public:
    X x() const { return x_; }
    void x(X x) { x_ = std::move(x); }
};

This implementation ensures that modifications by the caller and callee do not affect each other, suitable for scenarios where only the value matters, not the specific object instance.

Const Correctness

Regardless of the paradigm chosen, const correctness is a core principle in C++ getter/setter design. Methods that do not modify object state must be declared as const member functions:

const Foo f;
X val = f.x();  // Correct: const object calling const method

Non-const methods cannot be called on const objects, ensuring type system safety.

Performance Optimization and Move Semantics

In C++11 and later, performance can be optimized by overloading reference qualifiers:

class Foo {
    X x_;
public:
    auto x() const& -> const X& { return x_; }
    auto x() & -> X& { return x_; }
    auto x() && -> X&& { return std::move(x_); }
};

This implementation allows moving members from rvalue objects, avoiding unnecessary copies, particularly suitable for resource management classes.

Alternative Approach: Type Encapsulation

In some scenarios, getters/setters may not be the best choice. By defining types with intrinsic constraints, validation logic can be encapsulated within the type itself:

template<class T>
class checked {
    T value;
    std::function<T(const T&)> check;
public:
    template<class checker>
    checked(checker c) : check(c), value(c(T())) {}
    
    checked& operator=(const T& in) {
        value = check(in);
        return *this;
    }
    
    operator T() const { return value; }
};

This approach allows members to be directly public because all constraints are internalized in the type definition.

Practical Application Examples

Consider element access in a vector class, which is essentially a pair of getter/setter:

class Vector {
    std::vector<int> data_;
public:
    // Identity-oriented implementation
    int& element(std::size_t index) {
        return data_.at(index);
    }
    
    const int& element(std::size_t index) const {
        return data_.at(index);
    }
    
    // Value-oriented implementation
    int get_element(std::size_t index) const {
        return data_.at(index);
    }
    
    void set_element(std::size_t index, int value) {
        data_.at(index) = value;
    }
};

The choice between implementations depends on whether direct modification of vector elements is needed (identity-oriented) or only the element value matters (value-oriented).

Conclusion

Designing getters/setters in C++ requires comprehensive consideration of multiple factors: the distinction between object identity and value, const correctness, performance optimization, and the expressiveness of the type system. The identity-oriented paradigm suits scenarios requiring consistent object references, while the value-oriented paradigm suits scenarios emphasizing value independence. In practical development, the appropriate paradigm should be selected based on specific requirements, and consideration should be given to whether traditional getter/setter patterns can be replaced by type encapsulation.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.