Дизайн иерархии шахматных фигур: поля наследования и типов

У меня есть базовый класс для штук

 class piece;

и массив, содержащий производные объекты

piece* board[8][8];

Преимущество, чистый дизайн с помощью виртуальных функций. Недостаток: если мне нужно найти фигуру на доске или сравнить ее, мне придется вернуться к динамическому литью (или typeid). Это уродливо и может снизить производительность при выполнении миллионов запросов.

С другой стороны, если я создаю массив из класса одной части, в котором есть поле типа для идентификации частей, у меня нет этой проблемы (и это должно быть быстрее), но я должен делать очень уродливые операторы переключения. Я предполагаю, что, поскольку количество штук ограничено, и я не думаю, что буду делать так много переключателей, в конце концов это может быть лучшим выбором, как вы думаете?

Это для развлечения (поэтому без битборда).

Читая некоторые ответы, я думаю, что использование полей типа только для перегрузки оператора (==, !=, ...) может принести лучшее из обоих слов.

boost::variant тоже выглядит очень интересно.


person joey_89    schedule 31.12.2010    source источник
comment
Использование поля типа и переключателя - неправильный путь. Вы просто сделаете код полным беспорядком, чтобы прочитать его. Придерживайтесь принципов объектно-ориентированного программирования, они действительно работают.   -  person Martin York    schedule 31.12.2010
comment
Прочитав ответы ниже, я не уверен, как вы пришли к такому выводу, что все они, кажется, предлагают (те, у кого есть голоса), используя подход OO.   -  person Martin York    schedule 31.12.2010
comment
Да, есть подавляющее большинство голосов за наследование, и я не собираюсь против этого, я просто думаю, что использование поля типа «только» для равенства частей или обхода массива доски может быть более эффективным, чем dynamic_casting.   -  person joey_89    schedule 31.12.2010


Ответы (6)


Я бы пошел с иерархией классов.

Для поиска части вы можете вести отдельный список для каждого типа части. Так что вы знаете, где искать каждый тип части.

Для сравнения вы также можете полагаться на виртуальные методы.

Другой подход заключается в использовании компонентной архитектуры (как описано здесь: http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/), но я думаю, что это слишком много для шахматной игры, где ты четко знаешь типы и знаешь, что эти типы не скоро изменятся :).

person bcsanches    schedule 31.12.2010

Как вариант, если ваш набор занятий ограничен - т.е. вы знаете количество, используйте вариант и посетителей. Например, boost::variant<king, queen, bishop, knight ...> И плата состоит из 2D-массива этого типа. Теперь для допроса можно использовать посетителей...

person Nim    schedule 31.12.2010
comment
+1, очень чисто. Кроме того, у вас нет накладных расходов на виртуальную диспетчеризацию, что полезно при сканировании миллиардов позиций. Это, вероятно, скомпилируется как случай переключения, но без уродства. - person Alexandre C.; 31.12.2010
comment
и переключатель быстрее, чем виртуальная функция, потому что ...? - person wilhelmtell; 31.12.2010
comment
@wilhelmtell, из-за предсказателя ветвления? (в некоторых случаях) - person Roman L; 31.12.2010
comment
@7vies вызов виртуальной функции стоит одного разыменования. Опять же: насколько коммутатор дешевле вызова виртуальной функции? - person wilhelmtell; 31.12.2010
comment
@wilhelmtell, что вы подразумеваете под одним разыменованием? Для меня вызов виртуальной функции — это ассемблерный call с динамическим параметром, подразумевающий как минимум сохранение регистров и выполнение jmp в динамическом месте. За исключением случаев, когда это может быть оптимизировано компилятором, что, похоже, здесь не так. - person Roman L; 31.12.2010
comment
@7vies: предположительно в точке переключения (для любого нетривиального кода) вызывается функция (в противном случае вы помещаете весь код в одну и ту же функцию. Таким образом, переключатель — это jmp, за которым следует вызов функции. Любой аргумент, что вызов виртуальной функции медленнее, чем обычная функция, это фигня (за исключением очень узкого цикла).Во всех других ситуациях дополнительные затраты на виртуальную диспетчеризацию тривиальны по сравнению со всеми другими вещами, которые могут привести к остановке процессора. - person Martin York; 31.12.2010
comment
@Martin, у вас также может быть очень простая логика под переключателем, и в этом случае влияние вызова функции может стать важным (хотя я согласен, что это, вероятно, не самый распространенный случай). Кроме того, обычный вызов функции может быть встроен, тогда он больше не будет вызовом функции. - person Roman L; 31.12.2010
comment
@wilhelmtell: здесь мы говорим о нано-оптимизации, но я думаю, что для вызова виртуального метода необходимо разыменовать два указателя: один на объект и один на таблицу указателей виртуальных методов; переход идет на адрес, хранящийся в этой таблице; что может считаться другим разыменованием. - person Niki; 31.12.2010
comment
@Martin: Чтобы уточнить, я полностью согласен с тем, что в большинстве случаев накладные расходы на вызов функций незначительны. Я просто хотел уточнить, что есть разница, пусть и небольшая. - person Roman L; 31.12.2010

Я бы выбрал иерархию, и если я хочу знать тип (почему?), Имею виртуальный метод, который идентифицирует тип.

person Motti    schedule 31.12.2010
comment
Например, я хотел бы убедиться, что на доске [0][0] нет белой пешки, чтобы поставить в это место еще одну белую фигуру. Прямо сейчас я делаю сравнение через шаблон, который содержит dynamic_cast. - person joey_89; 31.12.2010
comment
@joey_89: Я бы предложил иметь другую структуру данных в дополнение к доске, на которой хранятся фигуры, чтобы вы могли проверить это на наличие определенных ограничений в отношении фигур (например, side[white].haveAllPawnsBeenTaked()) - person Martin York; 31.12.2010
comment
Я заменил массив доски на unordered_map‹int,piece_type›, чтобы упростить поиск позиции. - person joey_89; 10.01.2011

Я никогда не писал шахматную программу, но полагаю, что наиболее распространенными операциями будут такие вещи, как:

  • показать / распечатать доску
  • получить набор возможных ходов для каждой фигуры
  • суммировать значения всех фигур на доске, возможно, суммировать некую «ценность позиции», которая зависит от фигуры (ладья на открытой линии и тому подобное)

Кроме того, у некоторых фигур есть «состояние» (король может рокироваться только в том случае, если он не ходил раньше, пешка может нанести мимолетный удар, если другая пешка только что переместилась на два поля), которое применимо только к одному типу фигур.

Все это кричит мне об иерархии классов. (Предполагая, что вам не нужна битовая производительность)

С другой стороны, маловероятно, что вам когда-либо придется добавлять новые типы фигур или что вы когда-либо сможете повторно использовать один из типов фигур отдельно. то есть расширяемость и модульность на самом деле не проблема. Поэтому, если вы обнаружите, что какая-то важная часть вашего алгоритма, которая действительно должна быть в одном месте, разбросана по нескольким классам частей, используйте оператор switch. Просто добавьте абстрактный метод класса Piece, который возвращает перечисление PieceType, и включите его.

person Niki    schedule 31.12.2010

Вы можете не беспокоиться о производительности и коде для удовольствия одновременно :)

Рассмотрите возможность использования «nibbleboard» (или, по крайней мере, byteboard) вместо битборда, где каждый nibbleboard представляет один тип фрагмента. Каждый фрагмент также является индексом в таблице одноэлементных объектов, которые работают с этим типом фрагмента.

class Empty : public Piece {};
class Rook : public Piece {};
...

const int wrook = 1;
...
const int bpawn = 12;

Piece* Operator[13] = {new Empty(), new Rook(), ..., new Pawn()};

byte table[64] = {
    wrook, wbishop, wknight, wking, wqueen, wknight, wbishop, wrook,
    wpawn, wpawn, wpawn, wpawn, wpawn, wpawn, wpawn, wpawn, 
    0, 0, 0, 0, 0, 0, 0, 0, 
    0, 0, 0, 0, 0, 0, 0, 0, 
    0, 0, 0, 0, 0, 0, 0, 0, 
    0, 0, 0, 0, 0, 0, 0, 0, 
    bpawn, bpawn, bpawn, bpawn, bpawn, bpawn, bpawn, bpawn, 
    brook, bbishop, bknight, bking, bqueen, bknight, bbishop, brook};

// Given some position and some operation DoSomething we would have this:
Operator[table[position]]->DoSomething(table, position, <other parameters>);

// Possible return value of DoSomething might be new table
person Dialecticus    schedule 31.12.2010
comment
Вы можете не беспокоиться о производительности и писать код для удовольствия одновременно :) Touché - person joey_89; 31.12.2010

«Суперуродливый оператор switch» — это правильная техника. Это не уродливо. Это называется функциональным программированием.

Наследование — совершенно неправильный метод. Каждая из частей движется по-разному, имеет различную графику и другие свойства. Нет ничего общего. Шахматные фигуры не абстрактны. Это конкретный набор дискретных объектов.

Вы должны сделать что-то общее путем унификации: создать то, что называется типом суммы. В Окамле:

type shape = Pawn | Rook | Knight | Bishop | Queen | King
type color = Black | White
type piece = shape * color
type pos = { row:int;  col:int }

let check_white_move piece board from to = match piece with
| Pawn -> on_board to && (from.row = 2 && to.row = 4 or to.row = from.row + 1)
| ....

В С++ нет правильного типа суммы, вместо этого вы можете использовать:

enum shape { pawn, rook, knight, bishop, queen, king};
..
bool check_white_move (..) { switch piece {
 case pawn: ...

Это более неуклюже. Пожаловаться в комитеты C и C++. Но используйте правильную концепцию. Типы суммы (размеченные объединения, варианты) — это способ унифицировать дискретный набор конкретных типов. Классы и наследование используются для представления абстракций и их реализации.

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

[Кстати: да, вы можете попробовать вариант повышения, хотя я не могу рекомендовать его для этого приложения, так как части не имеют связанных данных, перечисление идеально]

person Yttrill    schedule 31.12.2010
comment
Написав шахматную программу на C++, я совершенно не согласен. Все произведения являются конкретным представлением абстрактного произведения. Все фигуры имеют одинаковые свойства (т.е. они двигаются()/отображаются()) - person Martin York; 31.12.2010
comment
На самом деле, части имеют связанные данные. Положение фигур на доске не является полным описанием состояния игры в любой момент (вспомните проходной удар, рокировку!) - person Niki; 31.12.2010
comment
Шахматные фигуры не абстрактны: это звучит как странная интерпретация ООП: обычно класс представляет что-то абстрактное (например, идею/концепцию человека или ладьи в платоническом смысле). Экземпляр класса обычно представляет что-то конкретное, например, Джона Смита или левого коня на моей шахматной доске с отсутствующим ухом. Таким образом, пешка, ладья, конь являются идеальными кандидатами на классы, а шахматная фигура является надклассом, потому что все они попадают под этот класс в силу того, что они пешка, ладья или конь. - person Niki; 31.12.2010