Шаблон C ++ для покрытия const и неконстантного метода - PullRequest
14 голосов
/ 17 октября 2011

У меня проблема с дублированием идентичного кода для const и не 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 и не const версий.

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

У меня есть одно решение:

template <class Visitor, class Struct>
void visit(Visitor &v, Struct &s) 
{
    v(s.i);
    v(d.i);
}

static void test2()
{
    Aggregate a;
    visit(VisitorRead(), a);
    const Aggregate b(a);
    visit(VisitorWrite(), b);
}

Это означает, что ни Aggregate::operator() не требуется, и нетбез дублированияНо меня не устраивает тот факт, что visit() является общим, без упоминания типа Aggregate.

Есть ли лучший способ?

Ответы [ 7 ]

5 голосов
/ 17 октября 2011
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, который (например, Скотт Мейерс) защищал от дублирования в методах получения, компилятор обеспечит правильную константность обеих открытых функций.

4 голосов
/ 17 октября 2011

Мне нравятся простые решения, поэтому я бы выбрал подход свободной функции, возможно, добавив 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 на самом деле просто реализовать, если у вас нет компилятора с поддержкой C ++ 0x (или вы можете позаимствовать их из boost type_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 );
}
3 голосов
/ 17 октября 2011

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

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

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

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

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); }
1 голос
/ 18 июля 2017

Добавить признак посетителя, чтобы указать, является ли он модифицированным (постоянным или неконстантным). Это используется итераторами STL.

1 голос
/ 17 октября 2011

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

0 голосов
/ 17 октября 2011

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

template <class Visitor>
static void visit(Visitor &v, typename Visitor::ApplyConst<Aggregate>::Type &a)
{
    v(a.i);
    v(a.d);
}
0 голосов
/ 17 октября 2011

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

...