Предположим, у меня есть вектор указателей на объекты класса Widget
, которые являются тяжелыми объектами без владельца. Чаще всего моему коду нужно перебирать этот вектор, но ему нужен только доступ к const Widget
.
Можно ли написать класс WidgetSet так, чтобы const WidgetSet
хранил const Widget
? Т.е. итерация по этому классу дает const std::shared_ptr<const Widget>&
объектов.
Я попробовал приведенный ниже код, но столкнулся с различными проблемами, отмеченными в комментариях:
#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.
}
}
Мне также нужен этот класс для взаимодействия с существующими функциями, которые принимают std::shared_ptr<const Widget>
и std::shared_ptr<Widget>
, которые не зависят от experimental::propagate_const
.
@TedLyngmo Я знаю об этом варианте, но предпочел бы не делать этого. Видите ли, большая часть нашей логики связана с отдельными виджетами, а WidgetSet — это скорее «дополнительная опция». Если указатель на отдельный виджет начнет зависеть от деталей реализации WidgetSet, это запутает всех участников проекта. (В этой проблемной области распространение const для итератора WidgetSet является деталью реализации).
Я знаю, что обычно это не рекомендуется, но рассматривали ли вы возможность использования вектора Widget* вместоshared_ptr? Это облегчит чтение вашего кода (без propagate_const) и позволит достичь желаемого. Я также собирался предложить продолжать использовать Shared_ptr, но создать итератор, который при разыменовании возвращает Widget* вместо Shared_ptr, но это, вероятно, слишком большая работа.
@MickaëlC.Guimarães, когда вы откладываете std::vector<Widget *>::const_iterator
, вы получаете Widget * const
, а не const Widget *
. У него та же проблема с распространением констант, что и у std::shared_ptr<Widget>
@MickaëlC.Guimarães У меня не может быть виджета*, потому что виджеты используются совместно с кодом Python через nanobind, и у них нет явного владельца. Мы также не хотим утечек памяти.
Можно ли написать класс
WidgetSet
так, чтобыconst WidgetSet
хранилconst Widget
? Т.е. итерация по этому классу даетconst std::shared_ptr<const Widget>&
объектов.
Нет. const std::shared_ptr<const Widget>&
— это не объектный тип, а ссылочный тип. Вы можете создать std::shared_ptr<const Widget>
из std::shared_ptr<Widget>
, но вы его нигде не сохраните, поэтому возврат ссылки на этот общий ptr небезопасен, он будет висеть в конце полного выражения.
Несколько подозрительно писать функции, которые принимают std::shared_ptr
по константной ссылке. Либо они будут участвовать в совместном владении, поэтому вам следует передавать значения shared_ptr
, либо они наблюдают за указанным значением, поэтому вам следует передавать ссылки или указатели (если сохраненные указатели могут быть нулевыми).
Вы можете написать что-то, что разворачивает 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 с вариациями для const Widget *
и std::shared_ptr<const Widget>
И это решает мои проблемы! Спасибо @Caleth! Я многому научился у тебя сегодня!
Что касается «Я бы не хотел, чтобы клиенты использовали типы, содержащие
std::experimental::propagate_const
в названии» — как насчет определенияusing value_type = std::experimental::propagate_const<std::shared_ptr<Widget>>;
и разрешения пользователям использовать это?const WidgetSet::value_type& w
выглядит не так уж и плохо, не так ли?