Suppose I have vector of pointers to objects of class Widget
, which are heavy objects with no owner. More often than not, my code needs to iterate over this vector, but needs only access to const Widget
.
Is it possible to write the WidgetSet class such that const WidgetSet
stores const Widget
s? I.e. the iteration over this class yields const std::shared_ptr<const Widget>&
objects.
I tried the code below, but came to various issues, highlighted in the comments:
#include <experimental/propagate_const>
#include <memory>
#include <vector>
struct Widget {
void const_method() const {}
void modify() {}
};
class WidgetSet {
std::vector<std::experimental::propagate_const<std::shared_ptr<Widget>>> _widgets;
public:
[[nodiscard]] auto begin() const { return _widgets.cbegin(); }
[[nodiscard]] auto end() const { return _widgets.cend(); }
[[nodiscard]] auto begin() { return _widgets.begin(); }
[[nodiscard]] auto end() { return _widgets.end(); }
};
void delegate_modify(const std::shared_ptr<Widget>& w) {
w->modify();
}
void exposed_delegate_modify(std::experimental::propagate_const<std::shared_ptr<Widget>>& w) {
//I need to prevent this from compiling and I would not like clients to use types have std::experimental::propagate_const in its name.
w->modify();
}
void delegate_test(const std::shared_ptr<const Widget>& w) {
w->const_method();
// w->modify(); // error, as expected.
}
void test(const WidgetSet cws, WidgetSet ws) {
for(const auto& w: cws) {
w->const_method(); // OK
// w->modify(); // error, as expected.
delegate_modify(get_underlying(w)); //surprise! this compiles, but shouldn't: w is const!
// exposed_delegate_modify(w); // error, as expected.
// delegate_test(w); // error: No matching function for call.
}
}
I also need this class to interoperate with existing functions that accept std::shared_ptr<const Widget>
and std::shared_ptr<Widget>
which do not depend on the experimental::propagate_const
.
Is it possible to write the
WidgetSet
class such thatconst WidgetSet
storesconst Widget
s? I.e. the iteration over this class yieldsconst std::shared_ptr<const Widget>&
objects.
No. const std::shared_ptr<const Widget>&
is not an object type, it is a reference type. You can construct a std::shared_ptr<const Widget>
from a std::shared_ptr<Widget>
, but you don't store it anywhere, so returning a reference to that shared ptr isn't safe, it will dangle at the end of the full expression.
It is somewhat suspicious to be writing functions that take std::shared_ptr
by const reference. Either they are going to participate in the shared ownership, so you should pass shared_ptr
values, or they are observing the pointed-to value, so you should pass references, or pointers (if the stored pointers can be null).
You can write something that unwraps the propogate_const
:
class WidgetSet {
struct get_mutable {
Widget& operator()(std::experimental::propagate_const<std::shared_ptr<Widget>>& ptr) const { return *ptr; }
};
struct get_const {
const Widget& operator()(const std::experimental::propagate_const<std::shared_ptr<Widget>>& ptr) const { return *ptr; }
};
std::vector<std::experimental::propagate_const<std::shared_ptr<Widget>>> _widgets;
decltype(_widgets | std::views::transform(get_mutable{})) _mutable = _widgets | std::views::transform(get_mutable{});
decltype(_widgets | std::views::transform(get_const{})) _const = _widgets | std::views::transform(get_const{});
public:
[[nodiscard]] auto begin() const { return _const.cbegin(); }
[[nodiscard]] auto end() const { return _const.cend(); }
[[nodiscard]] auto begin() { return _mutable.begin(); }
[[nodiscard]] auto end() { return _mutable.end(); }
};
Godbolt link with variations for const Widget *
and std::shared_ptr<const Widget>