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.