Шаблон C для покрытия постоянного и неконстантного метода

#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 as b in test() .

5. Итак, в чем проблема с тем, что посетитель бесплатной функции не упоминает Aggregate ? STL выполняет довольно много из этого … просто называя аргумент as ForwardIterator и ожидая, что пользовательский код знает, что делать. В качестве альтернативы вы можете использовать SFINAE для определения того, является ли аргумент на самом деле a Aggregate или нет.

Ответ №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);
}