We propose the introduction of a propagate_const wrapper class that propagates const-ness to pointer-like member variables.
The behaviour of const member functions on objects with pointer-like data members is seen to be surprising by many experienced C++ developers. A const member function can call non-const functions on pointer-like data members and will do so by default without use of const_cast.
Example:
struct A { void bar() const { std::cout << "bar (const)" << std::endl; } void bar() { std::cout << "bar (non-const)" << std::endl; } }; struct B { B() : m_ptrA(std::make_unique<A>()) {} void foo() const { std::cout << "foo (const)" << std::endl; m_ptrA->bar(); } void foo() { std::cout << "foo (non-const)" << std::endl; m_ptrA->bar(); } std::unique_ptr<A> m_ptrA; }; int main() { B b; b.foo(); const B const_b; const_b.foo(); }
Running this program gives the following output:
foo (non-const) bar (non-const) foo (const) bar (non-const)
The behaviour above can be amended by re-writing void B::foo() const using const_cast to explicitly call the const member function of A. Such a change is unnatural and not common practice. We propose the introduction of a wrapper class which can be used on pointer-like member data to ensure propagation of const-ness.
The class propagate_const is designed to function as closely as possible to a traditional pointer or smart-pointer. Pointer-like member objects can be wrapped in a propagate_const object to ensure propagation of const-ness.
A const-propagating B would be written as
struct B { B(); // unchanged void foo() const; // unchanged void foo(); // unchanged std::propagate_const<std::unique_ptr<A>> m_ptrA; };
With an amended B, running the program from the earlier example will give the following output:
foo (non-const) bar (non-const) foo (const) bar (const)
The pimpl (pointer-to-implementation) idiom pushes implementation details of a class into a separate object, a pointer to which is stored in the original class [2].
class C { void foo() const; void foo(); std::unique_ptr<CImpl> m_pimpl; }; void C::foo() const { m_pimpl->foo(); } void C::foo() { m_pimpl->foo(); }
When using the pimpl idiom the compiler will not catch changes to member variables within const member functions. Member variables are kept in a separate object and the compiler only checks that the address of this object is unchanged. By introducing the pimpl idiom into a class to decouple interface and implementation, the author may have inadventantly lost compiler checks on const-correctness.
When the pimpl object is wrapped in propagate_const, const member functions will only be able to call const functions on the pimpl object and will be unable to modify (non-mutable) member variables of the pimpl object without explicit const_casts: const-correctness is restored. The class above would be modified as follows:
class C { void foo() const; // unchanged void foo(); // unchanged std::propagate_const<std::unique_ptr<CImpl>> m_pimpl; };
Herb Sutter introduced the appealing notion that const implies thread-safe [3]. Without propagate_const, changes outside a class with pointer-like members can render the const methods of that class non-thread-safe. This means that maintaining the rule const=>thread-safe requires a global review of the code base.
With only the const version of foo() the code below is thread-safe. Introduction of a non-const (and non-thread-safe) foo() into D renders E non-thread-safe.
struct D { int foo() const { /* thread-safe */ } int foo() { /* non-thread-safe */ } }; struct E { E(D& pD) : m_pD{&pD} {} void operator() () const { m_pD->foo(); } D* m_pD; }; int main() { D d; const E e1(d); const E e2(d); std::thread t1(e1); std::thread t2(e2); t1.join(); t2.join(); }
One solution to the above is to forbid pointer-like member variables in classes if const=>thread-safe. This is undesirably restrictive. If instead all pointer-like member variables are decorated with propagate_const then the compiler will catch violations of const-ness that could render code non-thread-safe.
struct E { E(D& pD); // unchanged void operator() () const; // unchanged std::propagate_const<D*> m_pD; };
Introduction of propagate_const cannot automatically guarantee thread-safety but can allow const=>thread-safe to be locally verified during code review.
This proposal is a pure library extension. It does not require changes to any standard classes, functions or headers.
Given absolute freedom we would propose changing the const keyword to propagate const-ness. That would be impractical, however, as it would break existing code and change behaviour in potentially undesirable ways. A second approach would be the introduction of a new keyword to modify const, for instance, deep const, which enforces const-propagation. Although this change would maintain backward-compatibility, it would require enhancements to the C++ compiler.
We suggest that the standard library supply a class that wraps member data where const-propagating behaviour is required. The propagate_const wrapper can be used much like the const keyword and will cause compilation failure wherever const-ness is violated. const-propagation can be introduced into existing code by decorating pointer-like members of a class with propagate_const.
The change required to introduce const-propagation to a class is simple and local enough to be enforced during code review and taught to C++ developers in the same way as smart-pointers are taught to ensure exception safety.
It is intended that propagate_const contain no member data besides the wrapped pointer. Inlining of function calls by the compiler will ensure that using propagate_const incurs no run-time cost.
Inheritance from the wrapped pointer-like object (where it is a class type) was considered but ruled out. The purpose of this wrapper is to help the author ensure const-propagation; if propagate_const<T> were to inherit from T, then it would allow potentially non-const member functions of T to be called in a const context.
A propagate_const<T> should be constructable and assignable from a U or a propagate_const<U> where U is any type that T can be constructed or assigned from. There should be no additional cost of construction for a propagate_const<T> beyond that for construction of a T. The wrapped T should not be value-initialized as this would incur a cost for raw pointers. If value-initialization is desirable then it can be accomplished with another wrapper class like boost::value_initialized [4].
operator* and operator-> are defined to preserve const-propagation. When a const propagate_const<T> is used only const member functions of T can be used without explicit casts.
The get function returns the address of the object pointed to by the wrapped pointer. get is intended to be used to ensure const-propagation is preserved when using interfaces which require raw C-style pointers
When T is a raw pointer operator value* exists and allows implicit conversion to a raw pointer. This avoids using get to access the raw pointer in contexts where it was unnecesary before addition of the propagate_const wrapper.
Free-standing equality, inequality and comparison operators are provided so that a propagate_const<T> can be used in any equality, inequality or comparison where a T could be used. const-propagation should not alter the result of any equality, inequality or comparison operation.
Neither the member swap function nor the free-standing swap function should add or remove const-ness but should not unduly restrict the types with which propagate_const<T> can be swapped. If T and U can be swapped then const-propagating T and U can be swapped.
get_underlying is a free-standing function which allows the underlying pointer to be accessed. The use of this function allows const-propagation to be dropped and is therefore discouraged. The function is named such that it will be easy to find in code review.
The hash struct is specialized so that inclusion of propagate_const does not alter the result of hash evaluation.
The header <propagate_const> defines a template class propagate_const that imposes deep-const behaviour on pointer types.
propagate_const is a wrapper around a pointer type T which treats the wrapped pointer as a const pointer to const when the wrapper is accessed as a const object.
When T is a raw pointer then all member functions of propagate_const are noexcept. When T is a class type imitating a pointer then member functions of propagate_const may be conditionally noexcept.
namespace std { template <class T> class propagate_const { public: typedef decltype(*declval<T&>()) element_type; typedef element_type* pointer_type; // Constructors constexpr propagate_const(); template <class U> propagate_const(U&& u); template <class U> propagate_const(const propagate_const<U>& pu); template <class U> propagate_const(propagate_const<U>&& pu); // Destructor ~propagate_const() = default; // Assignment template <class U> propagate_const operator=(U&& u); template <class U> propagate_const operator=(const propagate_const<U>& pu); template <class U> propagate_const operator=(propagate_const<U>&& pu); // Observers explicit operator bool() const; const element_type* operator->() const; operator const element_type*() const; const element_type& operator*() const; const element_type* get() const; // Accessors element_type* operator->(); operator element_type*(); element_type& operator*(); element_type* get(); // Modifiers void swap(propagate_const<T>& pt); private: T t_; //exposition only }; // Relational operators template <class T, class U> bool operator==(const propagate_const<T>& pt, const propagate_const<U>& pu); template <class T, class U> bool operator!=(const propagate_const<T>& pt, const propagate_const<U>& pu); template <class T, class U> bool operator<(const propagate_const<T>& pt, const propagate_const<U>& pu); template <class T, class U> bool operator>(const propagate_const<T>& pt, const propagate_const<U>& pu); template <class T, class U> bool operator<=(const propagate_const<T>& pt, const propagate_const<U>& pu); template <class T, class U> bool operator>=(const propagate_const<T>& pt, const propagate_const<U>& pu); template <class T, class U> bool operator==(const propagate_const<T>& pt, const U& u); template <class T, class U> bool operator!=(const propagate_const<T>& pt, const U& u); template <class T, class U> bool operator<(const propagate_const<T>& pt, const U& u); template <class T, class U> bool operator>(const propagate_const<T>& pt, const U& u); template <class T, class U> bool operator<=(const propagate_const<T>& pt, const U& u); template <class T, class U> bool operator>=(const propagate_const<T>& pt, const U& u); template <class T, class U> bool operator==(const T& t, const propagate_const<U>& pu); template <class T, class U> bool operator!=(const T& t, const propagate_const<U>& pu); template <class T, class U> bool operator<(const T& t, const propagate_const<U>& pu); template <class T, class U> bool operator>(const T& t, const propagate_const<U>& pu); template <class T, class U> bool operator<=(const T& t, const propagate_const<U>& pu); template <class T, class U> bool operator>=(const T& t, const propagate_const<U>& pu); // Specialized algorithms template <class T> void swap (propagate_const<T>& pt, propagate_const<T>& pt2); // Underlying pointer access template <class T> const T& get_underlying(const propagate_const<T>& pt); template <class T> T& get_underlying(propagate_const<T>& pt); // Hash support template <class T> struct hash<propagate_const<T>>; }
1 | Effects: Constructs a propagate_const<T> that wraps a default-initialized T. |
2 | Remarks: This constructor shall not participate in overload resolution unless U is implicitly convertible to T. |
3 | Effects: Constructs a propagate_const<T> that wraps a T. If U is an rvalue reference, then the wrapped T is move constructed from u, otherwise the wrapped T is copy constructed from u. |
4 | Post-conditions: If u is a raw pointer, then get() yields u, otherwise get() yields the value that u.get() yielded before construction. |
5 | Remarks: This constructor shall not participate in overload resolution unless U is implicitly convertible to T. |
6 | Effects: Constructs a propagate_const<T> that wraps a T constructed from the pointer type wrapped by pu. |
7 | Post-conditions: get() yields the value that pu.get() yielded before construction. |
8 | Remarks: This constructor shall not participate in overload resolution unless U is implicitly convertible to T. |
9 | Effects: Constructs a propagate_const<T> that wraps an rvalue constructed T from the pointer type wrapped by pu. |
10 | Post-conditions: get() yields the value that pu.get() yielded before construction. |
1 | Effects: Destroys the wrapped object. |
1 | Remarks: This function shall not participate in overload resolution unless U is implicitly convertible to T. |
2 | Effects: The wrapped pointer type is assigned to forward<U>(u). |
3 | Returns: *this. |
4 | Remarks: This function shall not participate in overload resolution unless U is implicitly convertible to T. |
5 | Effects: The wrapped pointer type is assigned to the wrapped pointer in pu. |
6 | Returns: *this. |
7 | Remarks: This function shall not participate in overload resolution unless U is implicitly convertible to T. |
8 | Effects: The wrapped pointer type is move-assigned to the wrapped pointer in pu. |
9 | Returns: *this. |
1 | Effects: Return get() != nullptr. |
2 | Requires: get() != nullptr. |
3 | Returns: get(). |
4 | Returns: get(). |
5 | Remarks: This function shall participate in overload resolution only if T is a raw pointer or has an implicit conversion to element_type*. |
6 | Requires: get() != nullptr. |
7 | Returns: *get(). |
8 | Remarks: This function shall not participate in overload resolution if element_type is void. |
9 | Returns: t_ if T is a raw pointer, otherwise t_.get(). |
1 | Requires: get() != nullptr. |
2 | Returns: get(). |
3 | Returns: get(). |
4 | Remarks: This function shall participate in overload resolution only if T is a raw pointer or has an implicit conversion to element_type*. |
5 | Requires: get() != nullptr. |
6 | Returns: *get(). |
7 | Remarks: This function shall not participate in overload resolution if element_type is void. |
8 | Returns: t_ if T is a raw pointer, otherwise t_.get(). |
1 | Effects: Swaps the wrapped pointer type of *this with the wrapped pointer type of pt. |
1 | Returns: pt.get() == pu.get(). |
2 | Returns: pt.get() != pu.get(). |
3 | Returns: pt.get() < pu.get(). |
4 | Returns: pt.get() > pu.get(). |
5 | Returns: pt.get() <= pu.get(). |
6 | Returns: pt.get() >= pu.get(). |
7 | Returns: pt.get() == u. |
8 | Returns: pt.get() != u. |
9 | Returns: pt.get() < u. |
10 | Returns: pt.get() > u. |
11 | Returns: pt.get() <= u. |
12 | Returns: pt.get() >= u. |
13 | Returns: t == pu.get(). |
14 | Returns: t != pu.get(). |
15 | Returns: t < pu.get(). |
16 | Returns: t > pu.get(). |
17 | Returns: t <= pu.get(). |
18 | Returns: t >= pu.get(). |
1 | Effects: Calls pt1.swap(pt2). |
Access to the underlying pointer type is through free functions rather than member functions. These functions are intended to resemble cast operations to encourage caution when using them.
1 | Returns: a reference to the underlying pointer type. |
2 | Returns: a reference to the underlying pointer type. |
The template specialization shall meet the requirements of class template hash (20.9.12).
For an object p of type propagate_const<T>, hash
Requires: The specialization hash<T> shall be well-formed and well-defined, and shall meet the requirements of class template hash.
Thanks to Walter Brown, Kevin Channon, Nick Maclaren, Roger Orr, Ville Voutilainen, Jonathan Wakely, David Ward and others for helpful discussion.
This paper revises N4057
N4057 revises N3973