Когда я разрабатываю общий класс, я часто сталкиваюсь со следующими вариантами проектирования:
template<class T>
class ClassWithSetter {
public:
T x() const; // getter/accessor for x
void set_x(const T& x);
...
};
// vs
template<class T>
class ClassWithProxy {
struct Proxy {
Proxy(ClassWithProxy& c /*, (more args) */);
Proxy& operator=(const T& x); // allow conversion from T
operator T() const; // allow conversion to T
// we disallow taking the address of the reference/proxy (see reasons below)
T* operator&() = delete;
T* operator&() const = delete;
// more operators to delegate to T?
private:
ClassWithProxy& c_;
};
public:
T x() const; // getter
Proxy x(); // this is a generalization of: T& x();
// no setter, since x() returns a reference through which x can be changed
...
};
Заметки:
T
вместо const T&
в x()
а также operator T()
потому что ссылка на x
может быть недоступен из класса, если x
хранится только косвенным образом (например, предположим, T = std::set<int>
но x_
типа T
хранится как std::vector<int>
)x
является не позволилМне интересно, каковы были бы некоторые сценарии, в которых один предпочел бы один подход по сравнению с другим, особенно? с точки зрения:
?
Вы можете предположить, что компилятор достаточно умен, чтобы применять NRVO и полностью встроить все методы.
Текущие личные наблюдения:
(Эта часть не имеет отношения к ответу на вопрос; она просто служит мотивацией и иллюстрирует, что иногда один подход лучше другого.)
Один конкретный сценарий, в котором подход сеттера проблематичен, заключается в следующем. Предположим, вы реализуете контейнерный класс со следующей семантикой:
MyContainer<T>&
(изменяемый, чтение-запись) — позволяет изменять как контейнер, так и его данныеMyContainer<const T>&
(изменяемый, только для чтения) — позволяет изменять контейнер, но не его данныеconst MyContainer<T>
(неизменяемый, чтение-запись) — позволяет изменять данные, но не контейнерconst MyContainer<const T>
(неизменяемый, только для чтения) — без изменений в контейнере / данныхгде под «модификацией контейнера» я подразумеваю такие операции, как добавление / удаление элементов. Если я реализую это наивно с подходом сеттера:
template<class T>
class MyContainer {
public:
void set(const T& value, size_t index) const { // allow on const MyContainer&
v_[index] = value; // ooops,
// what if the container is read-only (i.e., MyContainer<const T>)?
}
void add(const T& value); // disallow on const MyContainer&
...
private:
mutable std::vector<T> v_;
};
Проблема может быть смягчена путем введения большого количества стандартного кода, который опирается на SFINAE (например, путем получения из специализированного помощника по шаблонам, который реализует обе версии set()
). Тем не менее, большая проблема заключается в том, что это тормозит общий интерфейс, как нам нужно либо:
С другой стороны, в то время как подход на основе прокси работает аккуратно:
template<class T>
class MyContainer {
typedef T& Proxy;
public:
Proxy get(const T& value, size_t index) const { // allow on const MyContainer&
return v_[index]; // here we don't even need a const_cast, thanks to overloading
}
...
};
и общий интерфейс и семантика не нарушены.
Одна проблема, которую я вижу с прокси-подходом, заключается в поддержке Proxy::operator&()
потому что не может быть никакого объекта типа T
сохранено / ссылка доступна (см. примечания выше). Например, рассмотрим:
T* ptr = &x();
который не может быть поддержан, если x_
фактически где-то хранится (либо в самом классе, либо доступно через (цепочку) методов, вызываемых для переменных-членов), например:
template<class T>
T& ClassWithProxy::Proxy::operator&() {
return &c_.get_ref_to_x();
}
Означает ли это, что ссылки на прокси-объекты действительно лучше, когда T&
доступно (т.е. x_
хранится в явном виде), поскольку позволяет:
(В этом случае дилемма между void set_x(const T& value)
а также T& x()
.)
Изменить: я изменил опечатки в констант сеттеров / аксессоров
Как и большинство дилемм дизайна, я думаю, что это зависит от ситуации. В целом, я бы предпочел шаблон получения и установки, так как его проще кодировать (нет необходимости в прокси-классе для каждого поля), проще для понимания другим человеком (глядя на ваш код) и более явным при определенных обстоятельствах. Однако существуют ситуации, когда прокси-классы могут упростить взаимодействие с пользователем и скрыть детали реализации. Несколько примеров:
Если ваш контейнер представляет собой некий ассоциативный массив, вы можете перегрузить оператор [] для получения и установки значения для определенного ключа. Однако, если ключ не был определен, вам может потребоваться специальная операция для его добавления. Здесь прокси-класс, вероятно, был бы наиболее удобным решением, так как он может обрабатывать присваивание различными способами по мере необходимости. Однако это может ввести пользователей в заблуждение: если эта конкретная структура данных имеет разное время для добавления и настройки, использование прокси-сервера затрудняет это, в то время как использование метода set и put может прояснить отдельное время, используемое каждой операцией.
Что если контейнер выполняет какое-то сжатие на T и сохраняет сжатую форму? Хотя вы можете использовать прокси-сервер, который выполняет сжатие / декомпрессию, когда это необходимо, он будет скрывать затраты, связанные с де / повторным сжатием, от пользователя, и они могут использовать его, как если бы это было простое назначение без сложных вычислений. Создавая методы получения / установки с соответствующими именами, можно сделать более очевидным, что они требуют значительных вычислительных усилий.
Методы получения и установки также кажутся более расширяемыми. Создать метод получения и установки для нового поля легко, а создание прокси-сервера, который перенаправляет операции для каждого свойства, было бы подвержено ошибкам. Что если вам позже понадобится расширить свой контейнерный класс? С помощью методов получения и установки просто сделайте их виртуальными и переопределите их в подклассе. Для прокси вам может понадобиться создать новую структуру прокси в каждом подклассе. Чтобы избежать нарушения инкапсуляции, вы, вероятно, должны заставить свою прокси-структуру использовать прокси-структуру суперкласса для выполнения некоторой работы, что может привести к путанице. С получателями / установщиками просто вызовите супер получателя / установщика.
В целом, геттеры и сеттеры легче программировать, понимать и изменять, и они могут сделать видимыми затраты, связанные с операцией. Так что в большинстве ситуаций я бы предпочел их.
Я думаю, что ваш интерфейс ClassWithProxy смешивает оболочки / прокси и контейнеры. Для контейнеров обычно используются такие аксессуары, как
T& x();
const T& x() const;
так же, как это делают стандартные контейнеры, например std::vector::at()
, Но обычно доступ к членам по ссылке нарушает инкапсуляцию. Для контейнеров это удобство и часть дизайна.
Но вы отметили, что ссылка на T
не всегда доступен, поэтому это уменьшит параметры вашего интерфейса ClassWithSetter, который должен быть обертка за T
иметь дело с тем, как вы храните ваш тип (в то время как контейнеры работают с тем, как вы храните объекты) Я бы изменил название, чтобы было ясно, что оно может быть не таким эффективным, как обычное получение / установка.
T load() const;
void save(const T&);
или что-то более в контексте. Теперь это должно быть очевидно, модифицируя T
используя прокси, снова нарушает инкапсуляцию.
Кстати, нет причин не использовать упаковку внутри контейнера.
Я думаю, что, возможно, часть проблемы с вашим set
реализация заключается в том, что ваша идея о том, как const MyContainer<T>&
поведение не совместимо с поведением стандартных контейнеров и, следовательно, может запутать будущих разработчиков кода. Обычный тип контейнера для «константный контейнер, изменяемые элементы» const MyContainer<T*>&
где вы добавляете уровень косвенности, чтобы четко указать свое намерение пользователям.
Вот как работают стандартные контейнеры, и если вы используете этот механизм, вам не нужно, чтобы нижележащий контейнер был изменяемым, set
функция быть const
,
Все сказанное я немного предпочитаю set
/get
подход, потому что если конкретный атрибут нуждается только в get
вам не нужно писать set
совсем.
Однако я предпочитаю не писать какой-либо прямой доступ к членам (например, получить / установить или прокси), а вместо этого предоставлять осмысленно названный интерфейс, через который клиенты могут получить доступ к функциональности класса. В тривиальном примере, чтобы показать мое значение, а не set_foo(1); set_bar(2); generate_report();
предпочитаю прямой интерфейс как generate_report(1, 2);
и избегайте прямого манипулирования атрибутами класса.