Полиморфный (чистый абстрактный) ключ карты жертвует безопасностью типов?

Некоторый контекст: (Не стесняйтесь пропустить вперед) У меня есть модуль, который обрабатывает сложные данные, но должен знать только некоторую его семантику. Данные можно рассматривать как пакет: модуль должен рассуждать только о непрозрачной строке полезной нагрузки, но в конечном итоге он все это передаст человеку, которому нужна дополнительная информация. Однако он должен ... "связывать" пакеты относительно некоторой неизвестной информации о пакете, поэтому я придумал следующее:

struct PacketInfo {
  virtual void operator==(PacketInfo const&) const = 0;
  virtual void operator<(PacketInfo const&) const = 0;
  virtual ~PacketInfo() {}
};

class Processor {
  private:
    template <typename T> struct pless {
      bool operator()(T const* a, T const* b) const {
        assert(a && b);
        return *a < *b;
      }
    };
    // this is where the party takes place:
    std::map<PacketInfo const*,X,pless<PacketInfo> > packets;
  public:
    void addPacket(PacketInfo const*,X const&);
};

Теперь идея состоит в том, что пользователь реализует свою PacketInfo семантику и передает ее через мой класс. Например:
(пожалуйста, внимательно прочтите конец вопроса, прежде чем отвечать)

struct CustomInfo : public PacketInfo {
  uint32_t source;
  uint32_t dest;
  void operator==(PacketInfo const& b) const {
    return static_cast<CustomInfo const&>(b).dest == dest
    && static_cast<CustomInfo const&>(b).source == source;
  }
  // operator< analogous
};

В тот момент, когда я использую static_cast, большинство людей будут использовать dynamic_cast, но rtti деактивирован как политика проекта. Конечно, я могу самостоятельно подготовить информацию о типе, и я делал это раньше, но вопрос не в этом.

Возникает вопрос: как я могу получить то, что хочу (т. Е. Иметь ключ карты, не зная его содержимого), не жертвуя безопасностью типов, то есть вообще без приведения? Я бы очень хотел, чтобы класс Processor не был шаблоном.


person bitmask    schedule 28.10.2011    source источник


Ответы (3)


В общем, ответ должен включать двойную отправку. Идея состоит в том, что если у вас n разные подклассы PacketInfo, вам понадобятся n * (n - 1) / 2 реализации оператора сравнения. В самом деле, что произойдет, если вы сравните CustomInfo с AwesomePersonalInfo? Для этого необходимо заранее знать всю иерархию, и пример кода представлен в этом вопросе SO .

Если вы уверены, что можете применить карту с идентичными типами внутри (и поэтому уверены, что вам нужны только реализации оператора n), тогда нет смысла иметь map<PacketInfo, X>. Просто используйте map<ConcretePacketInfo, X>.

Есть несколько способов сделать это. Самое простое, что можно сделать здесь, - это создать шаблон Processor для типа пакета, возможно, сделав его наследованием от класса BasicProcessor, если вы хотите где-то «стереть» параметр шаблона и учесть общий код.

Еще одно дешевое решение: оставить код как есть, но сделать Processor шаблон, который определяет только соответствующие addPacket:

class BasicProcessor
{
private:
    template <typename T> struct pless 
    {
        bool operator()(T const* a, T const* b) const 
        {
            assert(a && b);
            return *a < *b;
        }
    };

protected:
    std::map<PacketInfo const*, X, pless<PacketInfo>> packets;
};

// You only need these lines in a public header file.
template <typename Packet>
class Processor : public BasicProcessor
{
public:
     void addPacket(Packet const* p, X const& x)
     {
         this->packets[p] = x;
     }
};

Это гарантирует, что вызывающий абонент будет манипулировать объектом Processor<CustomPacket> и добавлять только правильный тип пакета. На мой взгляд, класс Processor должен быть классом-шаблоном.

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

person Alexandre C.    schedule 28.10.2011
comment
Во-первых, PacketInfoChild1 не нужно сравнивать с PacketInfoChild2. На карте всегда должны быть ключи только одного и того же самого производного типа. Во-вторых: Processor не имеет ни малейшего представления о том, как ConcretePacketInfo может выглядеть, и здесь ему нечего делать. ... Меня очень беспокоит разделение модулей ... - person bitmask; 28.10.2011
comment
@bitmask: вы можете разложить общий код на все процессоры шаблонов в базовом классе или иметь объект-посредник шаблона, представляющий базовые указатели на класс процессора и сохраняющий карту как частный объект. Дело в том, что карта должна обрабатывать определенные типы, для которых имеет смысл сравнение. Метод addPacket тоже должен быть конкретным. - person Alexandre C.; 28.10.2011
comment
Но у меня все равно большая часть (почти вся) моей Processor логики будет в заголовке. Конечно, создание шаблонов Processor тривиально решает проблему, но я бы хотел избежать этого решения. - person bitmask; 28.10.2011
comment
@bitmask: я не думаю, что вам следует избегать этого: это безопасность типов, о которой вы просили! Клиенту должно быть разрешено помещать в процессор только определенный тип: шаблон процессора позволяет это сделать. Вам не нужно многое менять в своем коде: просто внедрите подходящий addPacket метод в класс Processor<T>, оставив всю работу, которую вы уже проделали, в классе BasicProcessor. См. Мой обновленный ответ. - person Alexandre C.; 28.10.2011

Вы не можете. Вы либо знаете типы во время компиляции, либо проверяете их во время выполнения. Серебряной пули нет.

person n. 1.8e9-where's-my-share m.    schedule 28.10.2011

Самая очевидная проблема, которую я вижу, заключается в том, что ваши функции operator< и operator== не являются константными. Таким образом, вы не можете вызывать их через указатель на const или ссылку на const. Они должны быть:

virtual voie operator==(PacketInfo const& other) const = 0;
virtual voie operator<(PacketInfo const& other) const = 0;

Кроме того, логически, если вы определяете эти два, вы должны определить другие четыре. Обычно я с этим справляюсь, определяя полиморфную функцию-член compare, которая возвращает значение <, == или > 0, в зависимости от того, является ли ее объект this меньше, равен или больше, чем другой объект. Таким образом, производные классы должны реализовать только одну функцию.

Кроме того, вам определенно понадобится какой-то тип RTTI или двойная отправка, чтобы гарантировать, что оба объекта имеют один и тот же тип при их сравнении (и как вы заказываете сравнение, когда они этого не делают).

person James Kanze    schedule 28.10.2011
comment
Извините, я забыл квалификатор при вводе вопроса (исправляем сейчас). Я имел в виду const столько же, сколько вы имели в виду voie быть void. Кроме того, карта не заботится ни о каком сравнении, кроме <, потому что я сказал ей использовать pless. Дело в том, что мне все равно нужно кастовать, даже если у меня есть RTTI. - person bitmask; 28.10.2011
comment
@bitmask Да, вам еще нужно разыграть материал. Разница в том, что dynamic_cast безопасно; другие нет. - person James Kanze; 28.10.2011