Шаблон С++ для поддержки метода const и non-const

У меня проблема с дублированием идентичного кода для версий const и non- const. Я могу проиллюстрировать проблему с некоторым кодом. Вот два примера посетителей: один изменяет посещенные объекты, а другой - нет.

struct VisitorRead 
{
    template <class T>
    void operator()(T &t) { std::cin >> t; }
};

struct VisitorWrite 
{
    template <class T> 
    void operator()(const T &t) { std::cout << t << "\n"; }
};

Теперь вот агрегатный объект - в нем всего два элемента данных, но мой код гораздо сложнее:

struct Aggregate
{
    int i;
    double d;

    template <class Visitor>
    void operator()(Visitor &v)
    {
        v(i);
        v(d);
    }
    template <class Visitor>
    void operator()(Visitor &v) const
    {
        v(i);
        v(d);
    }
};

И функция для демонстрации вышеупомянутого:

static void test()
{
    Aggregate a;
    a(VisitorRead());
    const Aggregate b(a);
    b(VisitorWrite());
}

Теперь проблема заключается в дублировании Aggregate::operator() для версий const и non- const.

Можно ли как-то избежать дублирования этого кода?

У меня есть одно решение, которое заключается в следующем:

template <class Visitor, class Struct>
void visit(Visitor &v, Struct &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

Мне нравятся простые решения, поэтому я бы пошел на подход с бесплатной функцией, возможно, добавив 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 & v, T & s ) {  // T can only be Aggregate or Aggregate const
    v(s.i);
    v(s.d);   
}

Где enable_if, is_same и remove_const на самом деле просты в реализации, если у вас нет компилятора с поддержкой С++ 0x (или вы можете одолжить их у boost_traits)

EDIT. При написании подхода SFINAE я понял, что существует немало проблем в предоставлении простого шаблонного (без SFINAE) решения в OP, которое включает в себя тот факт, что если вам нужно предоставить более чем один доступный тип, разные шаблоны будут сталкиваться (т.е. они будут столь же хорошими, как и другие). Предоставляя SFINAE, вы фактически предоставляете функцию visit только для типов, которые удовлетворяют условию, преобразуя странный SFINAE в эквивалент:

// pseudocode, [] to mark *optional*
template <typename Visitor>
void visit( Visitor & v, Aggregate [const] & s ) {
   v( s.i );
   v( s.d );
}

Ответ 2

struct Aggregate
{
    int i;
    double d;

    template <class Visitor>
    void operator()(Visitor &v)
    {
        visit(this, v);
    }
    template <class Visitor>
    void operator()(Visitor &v) const
    {
        visit(this, v);
    }
  private:
    template<typename ThisType, typename Visitor>
    static void visit(ThisType *self, Visitor &v) {
        v(self->i);
        v(self->d);
    }
};

ОК, так что еще есть шаблон, но не дублирование кода, которое зависит от фактических членов агрегата. И в отличие от подхода const_cast, защищенного (например,) Скоттом Мейерсом, чтобы избежать дублирования в геттерах, компилятор обеспечит const-правильность обеих публичных функций.

Ответ 3

Поскольку ваши конечные реализации не всегда идентичны, я не думаю, что существует реальное решение для вашей воспринимаемой "проблемы".

Подумайте об этом. Мы должны обслуживать ситуации, когда Aggregate является либо const, либо не const. Конечно, мы не должны расслабляться (например, предоставляя только неконстантную версию).

Теперь const-версия оператора может вызывать только посетителей, которые принимают свой аргумент const-ref (или по значению), в то время как непостоянная версия может вызывать любого посетителя.

Вы можете подумать, что вы можете заменить одну из двух реализаций другой. Для этого вы всегда будете реализовывать версию const в терминах неконстантной, а не наоборот. Гипотетически:

void operator()(Visitor & v) { /* #1, real work */ }

void operator()(Visitor & v) const
{
  const_cast<Aggregate *>(this)->operator()(v);  // #2, delegate
}

Но для того, чтобы это имело смысл, строка # 2 требует, чтобы операция логически не мутировалась. Это возможно, например, в типичном операторе доступа к члену, где вы указываете константу или не постоянную ссылку на некоторый элемент. Но в вашей ситуации вы не можете гарантировать, что вызов operator()(v) не изменяется на *this!

Поэтому ваши две функции действительно отличаются друг от друга, хотя они выглядят формально похожими. Вы не можете выразить один в терминах другого.

Возможно, вы можете увидеть это по-другому: ваши две функции на самом деле не совпадают. В псевдокоде они:

void operator()(Visitor & v) {
  v( (Aggregate *)->i );
  v( (Aggregate *)->d );
}

void operator()(Visitor & v) const {
  v( (const Aggregate *)->i );
  v( (const Aggregate *)->d );
}

Собственно, подумав об этом, возможно, если вы захотите немного изменить подпись, что-то можно сделать:

template <bool C = false>
void visit(Visitor & 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 & v) { visit<>(v); }
void operator()(Visitor & v) const { const_cast<Aggregate *>(this)->visit<true>()(v); }

Ответ 4

Обычно с этим типом вещей, возможно, лучше использовать методы, которые имеют смысл. Например, load() и save(). Они говорят что-то конкретное о операции, которая должна быть выполнена через посетителя. Как правило, предоставляются как константа, так и неконстантная версия (например, для аксессуаров), поэтому она кажется дублирующей, но может сэкономить вам некоторую отладку головной боли позже по линии. Если вы действительно хотели обходной путь (который я бы не советовал), нужно объявить метод const и все члены mutable.

Ответ 5

Добавить свойство посетителя, чтобы определить, изменяется ли оно (const или non-const use). Это используется итераторами STL.

Ответ 6

Вы можете использовать const_cast и изменить подпись метода VisitorRead, чтобы он также принимал const T & как параметр, но я думаю, что это уродливое решение.

Ответ 7

Другое решение - требует, чтобы класс Visitor имел метафунгмент, который добавляет const, когда он применяется:

template <class Visitor>
static void visit(Visitor &v, typename Visitor::ApplyConst<Aggregate>::Type &a)
{
    v(a.i);
    v(a.d);
}