#c #templates #visitor-pattern
#c #шаблоны #посетитель-шаблон
Вопрос:
У меня проблема с дублированием идентичного кода для const
версий и не const
версий. Я могу проиллюстрировать проблему с помощью некоторого кода. Вот два примера посетителей, один из которых изменяет посещенные объекты, а другой — нет.
struct VisitorRead
{
template <class T>
void operator()(T amp;t) { std::cin >> t; }
};
struct VisitorWrite
{
template <class T>
void operator()(const T amp;t) { std::cout << t << "n"; }
};
Теперь вот агрегированный объект — у него всего два элемента данных, но мой фактический код намного сложнее:
struct Aggregate
{
int i;
double d;
template <class Visitor>
void operator()(Visitor amp;v)
{
v(i);
v(d);
}
template <class Visitor>
void operator()(Visitor amp;v) const
{
v(i);
v(d);
}
};
И функция для демонстрации вышеизложенного:
static void test()
{
Aggregate a;
a(VisitorRead());
const Aggregate b(a);
b(VisitorWrite());
}
Теперь проблема здесь заключается в дублировании версий Aggregate::operator()
for const
и non- const
versions.
Можно ли как-то избежать дублирования этого кода?
У меня есть одно решение, которое заключается в следующем:
template <class Visitor, class Struct>
void visit(Visitor amp;v, Struct amp;s)
{
v(s.i);
v(s.i);
}
static void test2()
{
Aggregate a;
visit(VisitorRead(), a);
const Aggregate b(a);
visit(VisitorWrite(), b);
}
Это означает, что ни Aggregate::operator()
то, ни другое не требуется, и дублирования нет. Но меня не устраивает тот факт, что visit()
он является общим без упоминания типа Aggregate
.
Есть ли лучший способ?
Комментарии:
1. Я думаю, что у вас может быть проблема дизайна больше, чем проблема реализации. Наличие
const
метода или получениеconst
переменной отправляет очень сильное сообщение: «этот метод не вносит изменений!» Тем не менее, наличие версии, которая меняет ситуацию, просто означает, что она будет вызвана на своем месте. Это приведет только к путанице и, в конечном итоге, к ошибкам.2. Если бы вы могли видеть реальный код, вы бы увидели, что дизайн имеет смысл. Реальный агрегат содержит десятки элементов POD, И функция visit работает с ними заданным способом (условные выражения и т. Д.) — Одинаково, Независимо от того, читаем мы или записываем объект.
3. Разве вы не можете удалить не
const
версиюAggregate::operator ()
?4. @iammilind я мог бы, но тогда я не мог использовать
const Aggregate
asb
intest()
.5. Итак, в чем проблема с тем, что посетитель бесплатной функции не упоминает
Aggregate
? STL выполняет довольно много из этого … просто называя аргумент asForwardIterator
и ожидая, что пользовательский код знает, что делать. В качестве альтернативы вы можете использовать SFINAE для определения того, является ли аргумент на самом деле aAggregate
или нет.
Ответ №1:
Я склонен любить простые решения, поэтому я бы выбрал подход со свободной функцией, возможно, добавив SFINAE, чтобы отключить функцию для типов, отличных от Aggregate
:
template <typename Visitor, typename T>
typename std::enable_if< std::is_same<Aggregate,
typename std::remove_const<T>::type
>::value
>::type
visit( Visitor amp; v, T amp; s ) { // T can only be Aggregate or Aggregate const
v(s.i);
v(s.d);
}
Где enable_if
, is_same
и remove_const
на самом деле просты в реализации, если у вас нет компилятора с поддержкой C 0x (или вы можете позаимствовать их у boost type_traits)
РЕДАКТИРОВАТЬ: при написании подхода SFINAE я понял, что существует немало проблем при предоставлении простого шаблонного (без SFINAE) решения в OP, которые включают в себя тот факт, что если вам нужно предоставить более одного доступных для посещения типов, разные шаблоны будут сталкиваться (т. Е. Они будут одинаково хорошо совпадатькак и другие). Предоставляя SFINAE, вы фактически предоставляете visit
функцию только для типов, которые выполняют условие, преобразуя странные SFINAE в эквивалент:
// pseudocode, [] to mark *optional*
template <typename Visitor>
void visit( Visitor amp; v, Aggregate [const] amp; s ) {
v( s.i );
v( s.d );
}
Комментарии:
1. Спасибо за это. Я смог использовать ключевое слово C 20
requires
с тем же эффектом вместоstd::enable_if
.
Ответ №2:
struct Aggregate
{
int i;
double d;
template <class Visitor>
void operator()(Visitor amp;v)
{
visit(this, v);
}
template <class Visitor>
void operator()(Visitor amp;v) const
{
visit(this, v);
}
private:
template<typename ThisType, typename Visitor>
static void visit(ThisType *self, Visitor amp;v) {
v(self->i);
v(self->d);
}
};
Хорошо, итак, все еще есть некоторый шаблон, но нет дублирования кода, который зависит от фактических членов агрегата. И в отличие от const_cast
подхода, поддерживаемого (например) Скоттом Мейерсом, чтобы избежать дублирования в геттерах, компилятор обеспечит постоянную корректность обеих общедоступных функций.
Комментарии:
1. Аккуратно, но
ThisType
все равно может быть что угодно, хотя в частной функции-члене это менее неприятно. Также есть много шаблонов, которые, я думаю, я мог бы избежать использования CRTP, если у меня много подобных структурAggregate
.2.@paperjam: Я полагаю, вы могли бы добавить статические утверждения, которые
ThisType
являются либоAggregate*
илиAggregate const*
. Но вместо этого я бы просто задокументировал, что первый аргументvisit
должен бытьthis
, и оставил его вызывающему, чтобы он не был идиотом. Или документ, которыйvisit
является вспомогательной функцией дляoperator()
, и не должен вызываться из другого места, даже из других членовAggregate
. CRTP может уменьшить шаблонность. Не уверен, поскольку родительский класс не может вызывать закрытую функциюvisit
, и мы не обязательно хотим, чтобы она была общедоступной.3. На самом деле, кого волнует, что
ThisType
это может быть что угодно? Очевидно, что это может быть только то, что имеет те же элементы,Aggregate
что и , поскольку телоvisit
будет использовать эти элементы. Итак, в наихудшем реалистичном случае это производный классAggregate
, и мы не можем посетить какие-либо дополнительные элементы, которые добавляет производный класс. Но это ожидается, когда вы пытаетесь выполнить этот посетительский материал с иерархиями классов, подклассы должны обязательно скрывать / переопределять все. Если кто-то найдет способvisit
общего использования в каком-либо другом классе с теми же именами членов, тогда удачи им 😉4. @SteveJessop Под «обобщенно» вы, вероятно, имеете в виду базу, которая не знает о ее происхождении. Как упоминалось ранее, альтернативой может быть CRTP. У вас может не быть выбора в этом вопросе, обычно, если экземпляры необходимо хранить в каком-либо разнородном контейнере. Любая непрерывная цепочка применений, удовлетворяющая LSP ( en.wikipedia.org/wiki/Liskov_substitution_principle ) может завершить цикл до исходного вывода. Основное использование предназначено
D: B<D>
для конкретного и окончательногоD
в отношенииB
(но Лисков может помочь вам гораздо дальше.)5. Счастливое совпадение — eli.thegreenplace.net/2011/05/17 /… решает эту самую проблему с помощью CRTP и mixins, допуская некоторую абстракцию без ущерба для производного поведения. Контейнер всегда может хранить миксины по признакам типа, чтобы восстановить некоторые ограничения типа при реализации, например, потерять
iterator
, но сохранитьconst_iterator
, когда он хранится вместеconst_iterable
с s.
Ответ №3:
Поскольку ваши конечные реализации не всегда идентичны, я не думаю, что существует реальное решение вашей предполагаемой «проблемы».
Давайте подумаем об этом. Мы должны учитывать ситуации, когда Aggregate
значение является либо const, либо non-const . Конечно, мы не должны ослаблять это (например, предоставляя только неконстантную версию).
Теперь постоянная версия оператора может вызывать только посетителей, которые принимают свой аргумент по const-ref (или по значению), в то время как непостоянная версия может вызывать любого посетителя.
Вы можете подумать, что можете заменить одну из двух реализаций другой. Для этого вы всегда должны реализовывать версию const в терминах неконстантной версии, а не наоборот. Гипотетически:
void operator()(Visitor amp; v) { /* #1, real work */ }
void operator()(Visitor amp; v) const
{
const_cast<Aggregate *>(this)->operator()(v); // #2, delegate
}
Но для того, чтобы это имело смысл, строка # 2 требует, чтобы операция логически не изменялась. Это возможно, например, в типичном операторе доступа к члену, где вы предоставляете либо постоянную, либо непостоянную ссылку на некоторый элемент. Но в вашей ситуации вы не можете гарантировать, что operator()(v)
вызов не мутирует *this
!
Таким образом, ваши две функции действительно довольно разные, хотя они выглядят формально похожими. Вы не можете выразить одно в терминах другого.
Может быть, вы можете увидеть это по-другому: ваши две функции на самом деле не совпадают. В псевдокоде они:
void operator()(Visitor amp; v) {
v( (Aggregate *)->i );
v( (Aggregate *)->d );
}
void operator()(Visitor amp; v) const {
v( (const Aggregate *)->i );
v( (const Aggregate *)->d );
}
На самом деле, если подумать об этом, возможно, если вы захотите немного изменить подпись, что-то можно сделать:
template <bool C = false>
void visit(Visitor amp; v)
{
typedef typename std::conditional<C, const Aggregate *, Aggregate *>::type this_p;
v(const_cast<this_p>(this)->i);
v(const_cast<this_p>(this)->d);
}
void operator()(Visitor amp; v) { visit<>(v); }
void operator()(Visitor amp; v) const { const_cast<Aggregate *>(this)->visit<true>()(v); }
Комментарии:
1. Имеет смысл, спасибо @Kerrek. Возможно, можно улучшить предложенное мной решение, приведя тип
Aggregate
вvisit
функцию. Что-то похожее на это: codeguru.com/forum/showthread.php?t=487097 ?2. @paperjam: я добавил довольно сумасшедший взлом в конце, проверьте это 🙂 Большое начальное препятствие заключается в том, что в подписи нет ничего, что напрямую позволяло бы нам различать эти два (т. Е.
v
Всегда неконстантно); поэтому я ввел параметр шаблона для создания двух отдельных статических путей кода.3. Мне нравится ваше второе решение, но использование a
bool
кажется немного неуклюжим.4. @paperjam: В качестве альтернативы, вы могли бы использовать
std::is_same<decltype(this), const Aggregate *>::value
.5. Я думаю, что это имеет смысл в определенных контекстах. Различие действительно, но точно так же, как вы хотели бы идеальной пересылки для инвариантности lvalue / rvalue, инвариантность CV кажется столь же полезной. В качестве примера можно было бы ожидать
g=CV(f[i])=CV(f)[i]
(operator[]
не имеет собственных побочных эффектов, CV распространяется, и осуществляется доступ к тому же элементу.) Любые неожиданности, не присущиеg
, были бы лучшими причинами не перегружатьoperator[]
.
Ответ №4:
Обычно в таких случаях, возможно, лучше использовать методы, которые имеют смысл. Например, load()
и save()
. Они говорят что-то конкретное об операции, которая должна выполняться через посетителя. Обычно предоставляется как const, так и неконстантная версия (в любом случае, для таких вещей, как средства доступа), так что это только кажется дублированием, но может избавить вас от некоторой головной боли при отладке позже. Если вам действительно нужен обходной путь (который я бы не советовал), это объявить метод const
и все члены mutable
.
Ответ №5:
Добавьте признак посетителя, чтобы указать, модифицируется он или нет (постоянное или неконстантное использование). Это используется итераторами STL.
Комментарии:
1. Вероятно, будет полезна некоторая дополнительная информация.
Ответ №6:
Вы могли бы использовать const_cast и изменить сигнатуру метода VisitorRead, чтобы он также принимал const Tamp; в качестве параметра, но я думаю, что это уродливое решение.
Комментарии:
1. Как бы вы
const_cast<nonconst T *>(amp; (T) x)
поступили, еслиT
уже является const? (Ключевое слово «noncost» используется в качестве примера). Другими словами: как удалить константуT
?
Ответ №7:
Другое решение — потребовать Visitor
, чтобы у класса была метафункция, которая добавляет const
, когда она применяется:
template <class Visitor>
static void visit(Visitor amp;v, typename Visitor::ApplyConst<Aggregate>::Type amp;a)
{
v(a.i);
v(a.d);
}