2016-11-11 3 views
3

Для класса C++ я пытаюсь создать иерархию классов, которая обрабатывает двоичные операции += и -=. Желаемая иерархия (для каждого вопроса) описывается следующим образом. У нас есть два класса: Addition и Subtraction. Это базовые классы для класса Binops. Затем класс Operations наследует от Binops. Таким образом, схема будет выглядеть следующим образомМножественное наследование. Какова идея этой модели наследования для реализации `+ =`?

 Operations 
      | 
      ↓ 
     Binops 
     | | 
     | | 
    +---+ +---+ 
    ↓   ↓ 
    Addition Subtraction 

Здесь Binops является другим классом Operations. Следующие функции-члены обязаны: Operations реализует частные функции

void add(Operations const &rhs); 
void sub(Operations const &rhs); 

и потребностей класса Addition реализовать

Addition::operator+=(Operations const &rhs) 

Аналогично для Subtraction класса.

У меня есть вопросы и о реализации этого дизайна, и о его идее.

Как я вижу это, как только эта основа будет готова, еще один класс, как, например, Matrix класс может наследовать от класса Operations, а затем мы делаем Matrix друг Operations так Matrix можно использовать += и т.д. Тогда мы бы просто должны выполнить функцию add в Operations, а операция += будет работать для класса Matrix. Но тогда почему бы нам просто не реализовать оператора += в Operations или даже Matrix? Возможно, идея состоит в том, что мы можем также определить оператора = в Addition с использованием функции addOperations, так что после внедрения add оба += и + работают за один раз.

С точки зрения реализации: Каким должен быть тип возврата += в Addition? Я считаю, что это должно быть Operations, но тогда заголовок класса Addition должен включать заголовок Operations, который приводит к циклическим зависимостям.

Кроме того, для Addition, чтобы иметь возможность использовать add из Operations, есть какой-то способ, которым мы можем сделать это без Addition друг Operations тоже? Я не думаю, что просто сделать Addition Другом Binops достаточно, поскольку дружба не транзитивна.

Извините за длинный вопрос. Заранее благодарим за любые идеи!

+5

Ваше наследство кажется обратным. Дополнение - это особый тип двоичной операции, а не наоборот. – chepner

+1

Неправильное место, чтобы спросить. http://softwareengineering.stackexchange.com/ может быть лучше (но, вероятно, нет). Подробнее о умных указателях http://en.cppreference.com/w/cpp/memory –

+2

Неясно. «класс Operations наследуется от этого Binops» и «Binops - класс друзей для операций». Итак, что это. Это класс друзей или подкласс. Подкласс, который также является классом друзей, не является хорошим дизайном. Тот факт, что, как было указано, наследование назад, похоже, усиливает представление о том, что общий дизайн классов ошибочен. –

ответ

5

Похоже, эти имена классов немного разрядились. Мое психическое декодирование состоит в том, что Addition - HasAddition. Итак, у нас есть HasOperations наследует от HasBinOps, который наследует и от HasAddition, и от HasSubtraction.

Итак, я получаю базовый план. Но я собираюсь ответить, как это сделать правильно. Это может не совпадать с вашим заданием, но это, честно говоря, проблема вашего задания не моя!

Мы не хотим, чтобы виртуальная диспетчеризация и динамическое распределение выполнялись для всех основных операций. Нам нужен статический полиморфизм, а не динамический полиморфизм.

К счастью, в C++ у нас есть статический полиморфизм. Типичный способ его реализации - через CRTP - любопытно повторяющийся шаблон шаблона.

Здесь нет необходимости использовать CRTP. Мы можем вместо этого полагаться на поиск Koenig!

Koenig lookup - это тот факт, что при определении того, что operator+ для вызова, рассматриваются родительские классы friend. Мы вводим friend operator+, который соответствует производным типам, делая его template внутри has_addition.

Когда у нас есть наш matrix:has_addition, и мы вызываем +. этот шаблон найден. Затем мы подставляем тип аргументов - полный тип, а не родительский тип has_addition.

В этом полном виде у нас есть метод .

Таким образом, мы можем наследовать от такого типа, что operator+ в этом типе имеет различную реализацию, основанную на том, какой тип мы получаем от него, но эта отправка выполняется статически во время компиляции.

Во время выполнения, has_addition в основном исчезает. Вместо этого мы просто получаем пучок +, направленный на .add.

Итак, без дальнейших церемоний, вот has_addition:

struct has_addition { 
    // implement + in terms of += on the lhs: 
    template<class L, class R> 
    friend std::decay_t<L> operator+(L&& lhs, R&& rhs) { 
    if (!std::is_reference<L>{}) { // rvalue lhs 
     return std::forward<L>(lhs) += rhs; 
    } else if (!std::is_reference<R>{}) { // rvalue rhs 
     return std::forward<R>(rhs) += lhs; // assumes + commutes 
    } else { // rvalue neither 
     auto tmp = std::forward<L>(lhs); 
     return tmp += rhs; 
    } 
    } 
    // notice += on an rvalue returns a copy. 
    // This permits reference lifetime extension: 
    template<class L, class R> 
    friend L operator+=(L&& lhs, R&& rhs) { 
    lhs.add(std::forward<R>(rhs)); 
    return std::forward<L>(lhs); 
    } 
}; 

вы используете его с помощью:

struct bob : has_addition { 
    int x = 0; 
    void add(bob const& rhs) { 
    x += rhs.x; 
    } 
}; 

Live example.

Теперь как +, так и += реализованы для вас на основе вашего метода add. Что еще, есть несколько перегрузок rvalue и lvalue из них. Если вы реализуете move-construct, вы получаете автоматическое повышение производительности. Если вы реализуете add, который принимает rvalue с правой стороны, вы получаете автоматическое повышение производительности.

Если вы не можете переписать значение rvalue add и move-construct, все будет работать. Мы отделили факторы (добавив что-то, что вы можете отбросить, и утилизируйте свое хранилище, и микро-оптимизацию работы +) друг от друга. В результате проще написать код с грудами встроенных микро оптимизаций.

В настоящее время большинство микрооптимизаций в has_addition::operator+ не требуется для первого прохода.

struct has_addition { 
    // implement + in terms of += on the lhs: 
    template<class L, class R> 
    friend L operator+(L lhs, R&& rhs) { 
    return std::move(lhs) += std::forward<R>(rhs); 
    } 
    template<class L, class R> 
    friend L operator+=(L&& lhs, R&& rhs) { 
    lhs.add(std::forward<R>(rhs)); 
    return std::forward<L>(lhs); 
    } 
}; 

, который намного чище и почти оптимален.


Затем расширить это с

struct has_subtraction; // implement 
struct has_binops: 
    has_subtraction, 
    has_addition 
{}; 

struct has_operations: 
    has_binops 
{}; 

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

Вы можете использовать SFINAE (отказ замена не является ошибкой), чтобы обнаружить, если add, subtact, multiply, divide, order, equals и т.д. реализованы в вашем типе, и писать maybe_has_addition<D>, что делает тест SFINAE на D, чтобы определить, он имеет D.add(D const&). Если и только если has_addition унаследовано от maybe_has_addition<D>.

Затем вы можете настроить его так, что в целом множество операторных перегрузок написано делать:.

struct matrix: maybe_has_operations<matrix> 

, где, как вы реализуете новые операции на matrix, все больше и больше перегруженные операторы пнуть в

Это, однако, другая проблема.

Выполнение этого с помощью динамического полиморфизма (виртуальных функций) является беспорядком. И действительно, вы хотите перепрыгнуть через несколько vtables, динамических распределений и потерять всю безопасность времени компиляции при написании matrix1 = matrix2 + matrix3? Это не Java.


Друг бит довольно прост. Обратите внимание, как has_addition звонки D.add(D const&). Мы можем сделать add частным в пределах D, но только если мы friend struct has_addition; в корпусе D.

So has_addition является одновременно родителем D и другом D.

Сам, я просто оставляю add выставлен, потому что он безвреден.


Этот метод имеет недостатки, как то, что происходит, когда вы добавляете два различных класса оба из которых has_addition.

Вы можете увидеть более полную версию этого кода в boost.operators, в котором также используются связанные с ней методы.

+0

Спасибо за подробный ответ! Это похоже на очень хороший способ делать вещи и, конечно, гораздо приятнее, чем иметь вложенную структуру наследования, как описано в вопросе. Идиома CRTP - это то, что я раньше не видел, но она выглядит действительно полезной! Еще раз спасибо! – Slugger

+0

@Slugger Btw, я заметил, что у него были проблемы, и теперь он переработан и более чист. CRTP здесь не требуется! – Yakk

Смежные вопросы